ruby_everywhere 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/every +12 -1
- data/exe/rbe +12 -1
- data/lib/everywhere/agents_guide.rb +3 -1
- data/lib/everywhere/blake2b.rb +17 -1
- data/lib/everywhere/boot.rb +21 -4
- data/lib/everywhere/builders/android.rb +144 -30
- data/lib/everywhere/builders/desktop.rb +25 -17
- data/lib/everywhere/builders/ios.rb +244 -9
- data/lib/everywhere/cli.rb +2 -0
- data/lib/everywhere/commands/build.rb +113 -58
- data/lib/everywhere/commands/clean.rb +8 -3
- data/lib/everywhere/commands/dev.rb +85 -11
- data/lib/everywhere/commands/doctor.rb +138 -21
- data/lib/everywhere/commands/install.rb +48 -8
- data/lib/everywhere/commands/platform/build.rb +136 -27
- data/lib/everywhere/commands/platform/login.rb +16 -2
- data/lib/everywhere/commands/platform/runner.rb +113 -23
- data/lib/everywhere/commands/preview.rb +359 -0
- data/lib/everywhere/commands/publish.rb +20 -2
- data/lib/everywhere/commands/release.rb +145 -28
- data/lib/everywhere/commands/shell_dir.rb +2 -2
- data/lib/everywhere/config.rb +88 -3
- data/lib/everywhere/console.rb +2 -1
- data/lib/everywhere/engine.rb +24 -0
- data/lib/everywhere/framework.rb +21 -4
- data/lib/everywhere/ignore.rb +3 -1
- data/lib/everywhere/jump.rb +121 -0
- data/lib/everywhere/mobile_config_endpoint.rb +47 -0
- data/lib/everywhere/mobile_configs_controller.rb +46 -0
- data/lib/everywhere/paths.rb +13 -6
- data/lib/everywhere/platform/client.rb +27 -6
- data/lib/everywhere/platform/credentials.rb +18 -8
- data/lib/everywhere/platform/snapshot.rb +10 -0
- data/lib/everywhere/receipt.rb +55 -10
- data/lib/everywhere/shellout.rb +19 -0
- data/lib/everywhere/simulator.rb +12 -1
- data/lib/everywhere/version.rb +1 -1
- data/support/mobile/android/app/build.gradle.kts +29 -1
- data/support/mobile/ios/App/EverywhereConfig.swift +17 -5
- data/support/mobile/ios/App/SceneDelegate.swift +75 -8
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +2 -2
- data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -1
- data/support/mobile/ios/README.md +13 -6
- metadata +17 -1
|
@@ -58,7 +58,7 @@ module Everywhere
|
|
|
58
58
|
def call(port: "3000", shell_dir: nil, target: nil, desktop: false, ios: false, android: false,
|
|
59
59
|
mobile: false, browser: false, dev_url: nil, **)
|
|
60
60
|
@shell_dir = shell_dir
|
|
61
|
-
@port = port
|
|
61
|
+
port = @port = validate_port!(port)
|
|
62
62
|
@dev_url_override = dev_url
|
|
63
63
|
# Children are spawned from pool threads as well as the main one.
|
|
64
64
|
@pids = {}
|
|
@@ -147,8 +147,8 @@ module Everywhere
|
|
|
147
147
|
else
|
|
148
148
|
UI.step("starting #{UI.bold(framework.name)} dev server #{UI.dim("(" + framework.dev_command + ")")}")
|
|
149
149
|
@dock.set(:web, :booting)
|
|
150
|
-
supervise(:web, { "PORT" => port.to_s }, framework.dev_command, chdir: framework.root)
|
|
151
|
-
wait_for_port(port)
|
|
150
|
+
pid = supervise(:web, { "PORT" => port.to_s }, framework.dev_command, chdir: framework.root)
|
|
151
|
+
wait_for_port(port, pid: pid)
|
|
152
152
|
@dock.set(:web, :running, detail: ":#{port}")
|
|
153
153
|
end
|
|
154
154
|
end
|
|
@@ -272,6 +272,12 @@ module Everywhere
|
|
|
272
272
|
end
|
|
273
273
|
|
|
274
274
|
def launch_desktop
|
|
275
|
+
# Checked before anything is staged: without cargo the build dies deep
|
|
276
|
+
# in a relayed `sh: cargo: command not found`, after a dock entry and a
|
|
277
|
+
# half-written dev bundle say a shell is on its way.
|
|
278
|
+
Shellout.ensure_tool!("cargo", "the desktop shell is built with Rust — install it from " \
|
|
279
|
+
"https://rustup.rs, then press d again")
|
|
280
|
+
|
|
275
281
|
# A bad `window:` or a broken assets/ tree marks the desktop red and
|
|
276
282
|
# leaves the dev server and the mobile shells alone — the same deal
|
|
277
283
|
# every other target gets when its build fails. Dying here would take
|
|
@@ -365,6 +371,15 @@ module Everywhere
|
|
|
365
371
|
# covers plain http for local networking. Pressing i again rebuilds and
|
|
366
372
|
# relaunches, so it doubles as a native-shell reload.
|
|
367
373
|
def launch_ios
|
|
374
|
+
# xcodebuild and simctl are macOS-only, so `i` elsewhere can only ever
|
|
375
|
+
# produce a long build that fails at the first tool. Android stays
|
|
376
|
+
# available everywhere — its toolchain genuinely is cross-platform.
|
|
377
|
+
unless RUBY_PLATFORM.include?("darwin")
|
|
378
|
+
UI.warn("iOS builds need macOS and Xcode — #{UI.dim("nothing to do on this platform")}")
|
|
379
|
+
@dock.set(:ios, :idle, detail: "needs macOS")
|
|
380
|
+
return
|
|
381
|
+
end
|
|
382
|
+
|
|
368
383
|
@dock.set(:ios, :building)
|
|
369
384
|
app = Builders::Ios.new(config: @config, root: @config.root, target: IOS_TARGET)
|
|
370
385
|
.build!(dist_dir: File.expand_path("dist", @config.root), dev: true)
|
|
@@ -406,7 +421,22 @@ module Everywhere
|
|
|
406
421
|
|
|
407
422
|
def launch_browser
|
|
408
423
|
UI.step("opening browser → #{UI.cyan(@dev_url)}")
|
|
424
|
+
# `start` is a cmd builtin, not an executable, so it can't be probed or
|
|
425
|
+
# spawned directly; the empty "" is the window title argument, without
|
|
426
|
+
# which cmd reads the URL as one.
|
|
427
|
+
if Gem.win_platform?
|
|
428
|
+
system("cmd", "/c", "start", "", @dev_url)
|
|
429
|
+
return
|
|
430
|
+
end
|
|
431
|
+
|
|
409
432
|
opener = RUBY_PLATFORM.include?("darwin") ? "open" : "xdg-open"
|
|
433
|
+
# A headless Linux box (or WSL) has no xdg-open, and run? would just
|
|
434
|
+
# return nil — the key would look broken. Print the URL instead.
|
|
435
|
+
unless Shellout.tool?(opener)
|
|
436
|
+
UI.warn("couldn't find #{opener} — open #{UI.cyan(@dev_url)} yourself")
|
|
437
|
+
return
|
|
438
|
+
end
|
|
439
|
+
|
|
410
440
|
Shellout.run?(opener, @dev_url, quiet: true)
|
|
411
441
|
end
|
|
412
442
|
|
|
@@ -506,8 +536,11 @@ module Everywhere
|
|
|
506
536
|
# while one is going — including `q`.
|
|
507
537
|
def interact
|
|
508
538
|
@dock.announce_keys
|
|
509
|
-
|
|
510
|
-
|
|
539
|
+
# stty is POSIX-only, and the non-pty spawn path otherwise works on
|
|
540
|
+
# Windows: without it the menu simply stays line-buffered (press d,
|
|
541
|
+
# then Enter) rather than the session dying on Errno::ENOENT.
|
|
542
|
+
saved_tty = saved_tty_state
|
|
543
|
+
system("stty", "-icanon", "-echo") if saved_tty
|
|
511
544
|
trap_exit_signals
|
|
512
545
|
loop do
|
|
513
546
|
case $stdin.getc
|
|
@@ -522,7 +555,19 @@ module Everywhere
|
|
|
522
555
|
end
|
|
523
556
|
end
|
|
524
557
|
ensure
|
|
525
|
-
system("stty", saved_tty)
|
|
558
|
+
system("stty", saved_tty) if saved_tty
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# The tty settings to put back on the way out, or nil when there's no
|
|
562
|
+
# usable stty to read them with — the one signal both the raw-mode setup
|
|
563
|
+
# and its restore key off.
|
|
564
|
+
def saved_tty_state
|
|
565
|
+
return nil unless Shellout.tool?("stty")
|
|
566
|
+
|
|
567
|
+
state = `stty -g`.chomp
|
|
568
|
+
state.empty? ? nil : state
|
|
569
|
+
rescue StandardError
|
|
570
|
+
nil
|
|
526
571
|
end
|
|
527
572
|
|
|
528
573
|
# The dock hides the cursor and the key loop puts the tty in -icanon
|
|
@@ -613,17 +658,46 @@ module Everywhere
|
|
|
613
658
|
|
|
614
659
|
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
615
660
|
|
|
661
|
+
# Checked once, up front: Integer() would otherwise raise from inside
|
|
662
|
+
# port_open? — an ArgumentError backtrace after the dock has already
|
|
663
|
+
# taken over the terminal.
|
|
664
|
+
def validate_port!(port)
|
|
665
|
+
number = begin
|
|
666
|
+
Integer(port.to_s, 10)
|
|
667
|
+
rescue ArgumentError, TypeError
|
|
668
|
+
UI.die!("--port must be a number (got #{port.inspect})")
|
|
669
|
+
end
|
|
670
|
+
UI.die!("--port must be between 1 and 65535 (got #{number})") unless (1..65_535).cover?(number)
|
|
671
|
+
number.to_s
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Any connect error means "nothing listening yet" for our purposes —
|
|
675
|
+
# ECONNREFUSED is the common one, but a firewall or an exhausted fd table
|
|
676
|
+
# answers differently and must not abort the session.
|
|
616
677
|
def port_open?(port)
|
|
617
678
|
TCPSocket.new("127.0.0.1", Integer(port)).close
|
|
618
679
|
true
|
|
619
|
-
rescue
|
|
680
|
+
rescue SystemCallError
|
|
620
681
|
false
|
|
621
682
|
end
|
|
622
683
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
684
|
+
# Watches the child as well as the port: a dev server that dies on
|
|
685
|
+
# startup (no `bundle install`, a bin/dev whose foreman isn't installed)
|
|
686
|
+
# never opens the port, and waiting out the full timeout for it buries
|
|
687
|
+
# the error it already printed under a minute of silence.
|
|
688
|
+
def wait_for_port(port, pid: nil, timeout: 60)
|
|
689
|
+
deadline = monotonic + timeout
|
|
690
|
+
until port_open?(port)
|
|
691
|
+
if pid && !ChildProcesses.alive?(pid)
|
|
692
|
+
UI.die!("the dev server exited before it finished booting — scroll up for its output " \
|
|
693
|
+
"(did `bundle install` run? does bin/dev's foreman exist?)")
|
|
694
|
+
end
|
|
695
|
+
if monotonic > deadline
|
|
696
|
+
UI.die!("dev server never came up on port #{port} after #{timeout}s — it may just be slow " \
|
|
697
|
+
"to boot; check its output above")
|
|
698
|
+
end
|
|
699
|
+
sleep 0.25
|
|
700
|
+
end
|
|
627
701
|
end
|
|
628
702
|
end
|
|
629
703
|
end
|
|
@@ -3,48 +3,166 @@
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require_relative "../shellout"
|
|
5
5
|
require_relative "../paths"
|
|
6
|
+
require_relative "../config"
|
|
6
7
|
require_relative "../android_sdk"
|
|
7
8
|
|
|
8
9
|
module Everywhere
|
|
9
10
|
module Commands
|
|
10
|
-
# Check that this machine can build native apps.
|
|
11
|
-
# run
|
|
12
|
-
#
|
|
11
|
+
# Check that this machine can build native apps. Every section always
|
|
12
|
+
# shows, so one run answers "what can this machine build?" — but only the
|
|
13
|
+
# sections the app's build.targets declare (or --target names) gate the
|
|
14
|
+
# exit status: failing doctor over a toolchain the app doesn't use would
|
|
15
|
+
# be a lie, the same reasoning as the cmdline-tools warning below.
|
|
13
16
|
class Doctor < Dry::CLI::Command
|
|
14
17
|
desc "Check the toolchain (rbe-tebako, rust, brew deps; Xcode for iOS, SDK/JDK for Android)"
|
|
15
18
|
|
|
16
|
-
option :root, default: ".", desc: "App root (its build.targets pick
|
|
17
|
-
option :target, default: nil, desc: "
|
|
19
|
+
option :root, default: ".", desc: "App root (its build.targets pick which sections are required)"
|
|
20
|
+
option :target, default: nil, desc: "Require a specific target's toolchain (e.g. ios-arm64)"
|
|
18
21
|
|
|
19
22
|
def call(root: ".", target: nil, **)
|
|
20
|
-
ok =
|
|
21
|
-
ok &= check("rbe-tebako installed") { Shellout.run?("rbe-tebako help > /dev/null 2>&1") }
|
|
22
|
-
ok &= check("cargo installed") { Shellout.run?("cargo --version > /dev/null 2>&1") }
|
|
23
|
-
ok &= check("homebrew bison") { !`brew --prefix bison 2>/dev/null`.strip.empty? }
|
|
23
|
+
ok = config_check(root)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
@optional_gaps = []
|
|
26
|
+
sections = [
|
|
27
|
+
["Desktop", desktop_wanted?(root, target), method(:desktop_checks)],
|
|
28
|
+
["iOS", ios_wanted?(root, target), method(:ios_checks)],
|
|
29
|
+
["Android", android_wanted?(root, target), method(:android_checks)]
|
|
30
|
+
]
|
|
31
|
+
# The sections that gate the run are the ones the user came to fix, so
|
|
32
|
+
# they come first; the informational ones read as an appendix.
|
|
33
|
+
# partition, not sort_by — Ruby's sort isn't stable.
|
|
34
|
+
required_sections, info_sections = sections.partition { |_, required, _| required }
|
|
35
|
+
(required_sections + info_sections).each do |name, required, checks|
|
|
36
|
+
ok &= section(name, required: required) { |req| checks.call(req) }
|
|
37
|
+
end
|
|
27
38
|
|
|
28
39
|
puts
|
|
29
|
-
ok
|
|
40
|
+
if ok
|
|
41
|
+
note = @optional_gaps.any? ? " #{UI.dim("(#{@optional_gaps.join(" and ")} would need setup — see above)")}" : ""
|
|
42
|
+
UI.success("ready to build#{note}")
|
|
43
|
+
else
|
|
44
|
+
UI.bad("fix the items above (see https://rubyeverywhere.com/docs/diy/requirements)")
|
|
45
|
+
end
|
|
30
46
|
exit(1) unless ok
|
|
31
47
|
end
|
|
32
48
|
|
|
33
49
|
private
|
|
34
50
|
|
|
51
|
+
# A section always renders; only a required one can fail the run. An
|
|
52
|
+
# optional section's gaps are remembered for the summary line instead.
|
|
53
|
+
def section(name, required:)
|
|
54
|
+
passed = yield(required)
|
|
55
|
+
@optional_gaps << name if !passed && !required
|
|
56
|
+
required ? passed : true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def section_label(name, required)
|
|
60
|
+
required ? name : "#{name} #{UI.dim("(not in build.targets — informational)")}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def macos? = RUBY_PLATFORM.include?("darwin")
|
|
64
|
+
|
|
65
|
+
# Desktop is the implicit default: an app with no build.targets presses
|
|
66
|
+
# for the host. Any non-mobile target (or --target) counts as desktop,
|
|
67
|
+
# mirroring how build/dev route targets.
|
|
68
|
+
def desktop_wanted?(root, target)
|
|
69
|
+
return true if target && !%w[ios android].include?(Everywhere::Config.os_of(target))
|
|
70
|
+
|
|
71
|
+
targets = config(root).targets
|
|
72
|
+
targets.empty? || targets.any? { |t| !%w[ios android].include?(Everywhere::Config.os_of(t)) }
|
|
73
|
+
rescue StandardError
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def desktop_checks(required = true)
|
|
78
|
+
puts
|
|
79
|
+
UI.step(section_label("Desktop target", required))
|
|
80
|
+
# Mirrors the gate in `every build`: pressing is macOS-only today, so
|
|
81
|
+
# probing for its toolchain elsewhere would only mislead.
|
|
82
|
+
unless macos?
|
|
83
|
+
return fix_check("macOS", "desktop builds are macOS-only for now") { false } if required
|
|
84
|
+
|
|
85
|
+
UI.warn("desktop builds are macOS-only for now — skipped")
|
|
86
|
+
return false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
ok = fix_check("rbe-tebako installed",
|
|
90
|
+
"it ships as a dependency of ruby_everywhere: `gem install rbe-tebako` " \
|
|
91
|
+
"(or reinstall the gem)") { Shellout.run?("rbe-tebako help > /dev/null 2>&1") }
|
|
92
|
+
ok &= fix_check("cargo installed", "install Rust: https://rustup.rs") do
|
|
93
|
+
Shellout.run?("cargo --version > /dev/null 2>&1")
|
|
94
|
+
end
|
|
95
|
+
ok &= fix_check(bison_label, bison_hint) { bison? }
|
|
96
|
+
ok
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# macOS ships a bison far too old for tebako's build, so there the brew
|
|
100
|
+
# one is the only one that counts — and asking brew is the only way to see
|
|
101
|
+
# it, since it stays off PATH (keg-only). Everywhere else the package
|
|
102
|
+
# manager installs a current bison normally.
|
|
103
|
+
def bison?
|
|
104
|
+
return Shellout.tool?("bison") unless macos?
|
|
105
|
+
|
|
106
|
+
out, status = Shellout.capture("brew", "--prefix", "bison")
|
|
107
|
+
!status.nil? && status.success? && !out.strip.empty?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def bison_label = macos? ? "homebrew bison" : "bison"
|
|
111
|
+
def bison_hint = macos? ? "brew install bison" : "install bison with your package manager"
|
|
112
|
+
|
|
113
|
+
# Config.load raises on an unparseable everywhere.yml; doctor reports it
|
|
114
|
+
# as its first check and carries on with an empty config, so the machine
|
|
115
|
+
# that most needs the rest of the report still gets it. The load happens
|
|
116
|
+
# once, here, and every later reader gets what it produced.
|
|
117
|
+
def config_check(root)
|
|
118
|
+
return true unless File.exist?(File.join(File.expand_path(root), Everywhere::Config::FILE))
|
|
119
|
+
|
|
120
|
+
config(root)
|
|
121
|
+
if @config_error
|
|
122
|
+
UI.bad(@config_error)
|
|
123
|
+
return false
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
UI.ok(Everywhere::Config::FILE)
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def config(root)
|
|
131
|
+
return @config if defined?(@config)
|
|
132
|
+
|
|
133
|
+
@config_error = nil
|
|
134
|
+
@config = begin
|
|
135
|
+
Everywhere::Config.load(File.expand_path(root))
|
|
136
|
+
rescue StandardError, SystemExit => e
|
|
137
|
+
@config_error = e.message
|
|
138
|
+
Everywhere::Config.new({}, File.expand_path(root))
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
35
142
|
def ios_wanted?(root, target)
|
|
36
143
|
return true if target && Everywhere::Config.os_of(target) == "ios"
|
|
37
144
|
|
|
38
|
-
Everywhere::Config.
|
|
39
|
-
.any? { |t| Everywhere::Config.os_of(t) == "ios" }
|
|
145
|
+
config(root).targets.any? { |t| Everywhere::Config.os_of(t) == "ios" }
|
|
40
146
|
rescue StandardError
|
|
41
147
|
false
|
|
42
148
|
end
|
|
43
149
|
|
|
44
|
-
def ios_checks
|
|
150
|
+
def ios_checks(required = true)
|
|
45
151
|
puts
|
|
46
|
-
UI.step("iOS target")
|
|
47
|
-
|
|
152
|
+
UI.step(section_label("iOS target", required))
|
|
153
|
+
# Off macOS every tool below is guaranteed absent — one honest line
|
|
154
|
+
# beats four unfixable ✗s.
|
|
155
|
+
unless macos?
|
|
156
|
+
return fix_check("macOS with Xcode", "iOS apps can only be built on a Mac") { false } if required
|
|
157
|
+
|
|
158
|
+
UI.warn("needs macOS and Xcode — skipped")
|
|
159
|
+
return false
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
ok = fix_check("full Xcode selected",
|
|
163
|
+
"install Xcode and run `sudo xcode-select -s /Applications/Xcode.app`") do
|
|
164
|
+
Shellout.run?("xcodebuild -version > /dev/null 2>&1")
|
|
165
|
+
end
|
|
48
166
|
ok &= check("iOS Simulator SDK") do
|
|
49
167
|
Shellout.run?("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path", quiet: true)
|
|
50
168
|
end
|
|
@@ -69,8 +187,7 @@ module Everywhere
|
|
|
69
187
|
def android_wanted?(root, target)
|
|
70
188
|
return true if target && Everywhere::Config.os_of(target) == "android"
|
|
71
189
|
|
|
72
|
-
Everywhere::Config.
|
|
73
|
-
.any? { |t| Everywhere::Config.os_of(t) == "android" }
|
|
190
|
+
config(root).targets.any? { |t| Everywhere::Config.os_of(t) == "android" }
|
|
74
191
|
rescue StandardError
|
|
75
192
|
false
|
|
76
193
|
end
|
|
@@ -80,9 +197,9 @@ module Everywhere
|
|
|
80
197
|
# without exporting any of them, so a `which adb` would report a broken
|
|
81
198
|
# machine that in fact builds fine. Nothing auto-installs — SDK components
|
|
82
199
|
# are gigabytes — so each failure carries the exact command to run.
|
|
83
|
-
def android_checks
|
|
200
|
+
def android_checks(required = true)
|
|
84
201
|
puts
|
|
85
|
-
UI.step("Android target")
|
|
202
|
+
UI.step(section_label("Android target", required))
|
|
86
203
|
sdk = Everywhere::AndroidSdk
|
|
87
204
|
root = sdk.sdk_root
|
|
88
205
|
|
|
@@ -56,6 +56,7 @@ module Everywhere
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def install_sinatra
|
|
59
|
+
add_puma_gem
|
|
59
60
|
vendor_bridge_to_public
|
|
60
61
|
guide_rack_manual_steps(
|
|
61
62
|
db: "point your production SQLite path at ENV.fetch(\"NATIVE_STORAGE_DIR\", \"storage\")",
|
|
@@ -65,6 +66,7 @@ module Everywhere
|
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
def install_hanami
|
|
69
|
+
add_puma_gem
|
|
68
70
|
vendor_bridge_to_public
|
|
69
71
|
guide_rack_manual_steps(
|
|
70
72
|
db: "override DATABASE_URL from ENV[\"NATIVE_STORAGE_DIR\"] in config.ru when NATIVE_PACKAGED",
|
|
@@ -100,14 +102,35 @@ module Everywhere
|
|
|
100
102
|
made ? UI.ok(label) : UI.step("#{label} #{UI.dim("(already done, skipped)")}")
|
|
101
103
|
end
|
|
102
104
|
|
|
105
|
+
# A step whose file this app doesn't have. Every one of these is a shape a
|
|
106
|
+
# real app legitimately ships in (API-only, DATABASE_URL, no
|
|
107
|
+
# config/puma.rb), so say what was skipped — never raise on it.
|
|
108
|
+
def skip(label, reason)
|
|
109
|
+
UI.step("#{label} #{UI.dim("(skipped — #{reason})")}")
|
|
110
|
+
end
|
|
111
|
+
|
|
103
112
|
def add_gem
|
|
113
|
+
UI.die!("no Gemfile in #{@root} — run `every install` inside your app") unless File.exist?(app_file("Gemfile"))
|
|
114
|
+
|
|
115
|
+
add_gemfile_gem("ruby_everywhere",
|
|
116
|
+
comment: "# Desktop apps from this #{@framework.name.capitalize} app — https://rubyeverywhere.com")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Packaged Rack apps serve through Puma at runtime (Framework#serve_rack
|
|
120
|
+
# requires it), so a missing gem surfaces as a LoadError on the end user's
|
|
121
|
+
# machine at first launch rather than here.
|
|
122
|
+
def add_puma_gem
|
|
123
|
+
add_gemfile_gem("puma", comment: "# Packaged RubyEverywhere apps serve through puma.")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def add_gemfile_gem(name, comment:)
|
|
104
127
|
gemfile = app_file("Gemfile")
|
|
105
128
|
contents = File.read(gemfile)
|
|
106
|
-
return change("add
|
|
129
|
+
return change("add #{name} to Gemfile", false) if contents.match?(/gem ["']#{name}["']/)
|
|
107
130
|
|
|
108
|
-
File.write(gemfile, "#{contents.chomp}\n\n#
|
|
131
|
+
File.write(gemfile, "#{contents.chomp}\n\n#{comment}\ngem \"#{name}\"\n")
|
|
109
132
|
Shellout.run!({}, "bundle", "install", "--quiet", chdir: @root)
|
|
110
|
-
change("add
|
|
133
|
+
change("add #{name} to Gemfile", true)
|
|
111
134
|
end
|
|
112
135
|
|
|
113
136
|
def write_config
|
|
@@ -214,9 +237,10 @@ module Everywhere
|
|
|
214
237
|
# targets: # the platforms you ship — the CI matrix, expressed once
|
|
215
238
|
# - macos-arm64
|
|
216
239
|
# # - ios-arm64 # Hotwire Native shell; remote mode only. Local builds
|
|
217
|
-
# # # (`every build` / `every dev --ios`)
|
|
218
|
-
# # #
|
|
219
|
-
# # -
|
|
240
|
+
# # # (`every build` / `every dev --ios`) and hosted
|
|
241
|
+
# # # platform builds are both live.
|
|
242
|
+
# # - ios-arm64:testflight # optional per-target distribution channel
|
|
243
|
+
# # - android-arm64 # Hotwire Native shell; remote mode only, hosted builds live
|
|
220
244
|
# capabilities: [notifications, filesystem:read, clipboard] # desktop OS integration
|
|
221
245
|
|
|
222
246
|
# Per-platform overrides. Keys are platform names (macos / ios / android /
|
|
@@ -321,6 +345,8 @@ module Everywhere
|
|
|
321
345
|
|
|
322
346
|
def guard_bootsnap
|
|
323
347
|
boot = app_file("config", "boot.rb")
|
|
348
|
+
return skip("guard bootsnap for packaged runs", "no config/boot.rb") unless File.exist?(boot)
|
|
349
|
+
|
|
324
350
|
contents = File.read(boot)
|
|
325
351
|
return change("guard bootsnap for packaged runs", false) if contents.include?("NATIVE_PACKAGED")
|
|
326
352
|
|
|
@@ -328,12 +354,16 @@ module Everywhere
|
|
|
328
354
|
"# Bootsnap writes its cache to tmp/, which is read-only inside the packaged filesystem.\n" \
|
|
329
355
|
"require \"bootsnap/setup\" unless ENV[\"NATIVE_PACKAGED\"]"
|
|
330
356
|
end
|
|
331
|
-
|
|
332
|
-
|
|
357
|
+
return skip("guard bootsnap for packaged runs", "this app doesn't load bootsnap") unless made
|
|
358
|
+
|
|
359
|
+
File.write(boot, contents)
|
|
360
|
+
change("guard bootsnap for packaged runs", true)
|
|
333
361
|
end
|
|
334
362
|
|
|
335
363
|
def disable_force_ssl
|
|
336
364
|
production = app_file("config", "environments", "production.rb")
|
|
365
|
+
return skip("disable force_ssl for localhost", "no config/environments/production.rb") unless File.exist?(production)
|
|
366
|
+
|
|
337
367
|
contents = File.read(production)
|
|
338
368
|
made = false
|
|
339
369
|
made |= !!contents.gsub!(/^(\s*config\.assume_ssl\s*=\s*)true/) { "#{Regexp.last_match(1)}false # desktop: webview talks plain HTTP to 127.0.0.1" }
|
|
@@ -416,6 +446,11 @@ module Everywhere
|
|
|
416
446
|
# connection was lost" (-1005). Single local user: keep-alives off.
|
|
417
447
|
def disable_puma_keep_alives
|
|
418
448
|
puma = app_file("config", "puma.rb")
|
|
449
|
+
unless File.exist?(puma)
|
|
450
|
+
return skip("disable puma keep-alives when packaged",
|
|
451
|
+
"no config/puma.rb — add `enable_keep_alives false if ENV[\"NATIVE_PACKAGED\"]` to yours if you add one")
|
|
452
|
+
end
|
|
453
|
+
|
|
419
454
|
contents = File.read(puma)
|
|
420
455
|
return change("disable puma keep-alives when packaged", false) if contents.include?("enable_keep_alives")
|
|
421
456
|
|
|
@@ -431,6 +466,11 @@ module Everywhere
|
|
|
431
466
|
|
|
432
467
|
def point_database_at_app_data
|
|
433
468
|
db = app_file("config", "database.yml")
|
|
469
|
+
unless File.exist?(db)
|
|
470
|
+
return skip("point production SQLite at app-data dir",
|
|
471
|
+
"no config/database.yml — point the packaged database at ENV[\"NATIVE_STORAGE_DIR\"] yourself")
|
|
472
|
+
end
|
|
473
|
+
|
|
434
474
|
contents = File.read(db)
|
|
435
475
|
return change("point production SQLite at app-data dir", false) if contents.include?("NATIVE_STORAGE_DIR")
|
|
436
476
|
|