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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/exe/every +12 -1
  3. data/exe/rbe +12 -1
  4. data/lib/everywhere/agents_guide.rb +3 -1
  5. data/lib/everywhere/blake2b.rb +17 -1
  6. data/lib/everywhere/boot.rb +21 -4
  7. data/lib/everywhere/builders/android.rb +144 -30
  8. data/lib/everywhere/builders/desktop.rb +25 -17
  9. data/lib/everywhere/builders/ios.rb +244 -9
  10. data/lib/everywhere/cli.rb +2 -0
  11. data/lib/everywhere/commands/build.rb +113 -58
  12. data/lib/everywhere/commands/clean.rb +8 -3
  13. data/lib/everywhere/commands/dev.rb +85 -11
  14. data/lib/everywhere/commands/doctor.rb +138 -21
  15. data/lib/everywhere/commands/install.rb +48 -8
  16. data/lib/everywhere/commands/platform/build.rb +136 -27
  17. data/lib/everywhere/commands/platform/login.rb +16 -2
  18. data/lib/everywhere/commands/platform/runner.rb +113 -23
  19. data/lib/everywhere/commands/preview.rb +359 -0
  20. data/lib/everywhere/commands/publish.rb +20 -2
  21. data/lib/everywhere/commands/release.rb +145 -28
  22. data/lib/everywhere/commands/shell_dir.rb +2 -2
  23. data/lib/everywhere/config.rb +88 -3
  24. data/lib/everywhere/console.rb +2 -1
  25. data/lib/everywhere/engine.rb +24 -0
  26. data/lib/everywhere/framework.rb +21 -4
  27. data/lib/everywhere/ignore.rb +3 -1
  28. data/lib/everywhere/jump.rb +121 -0
  29. data/lib/everywhere/mobile_config_endpoint.rb +47 -0
  30. data/lib/everywhere/mobile_configs_controller.rb +46 -0
  31. data/lib/everywhere/paths.rb +13 -6
  32. data/lib/everywhere/platform/client.rb +27 -6
  33. data/lib/everywhere/platform/credentials.rb +18 -8
  34. data/lib/everywhere/platform/snapshot.rb +10 -0
  35. data/lib/everywhere/receipt.rb +55 -10
  36. data/lib/everywhere/shellout.rb +19 -0
  37. data/lib/everywhere/simulator.rb +12 -1
  38. data/lib/everywhere/version.rb +1 -1
  39. data/support/mobile/android/app/build.gradle.kts +29 -1
  40. data/support/mobile/ios/App/EverywhereConfig.swift +17 -5
  41. data/support/mobile/ios/App/SceneDelegate.swift +75 -8
  42. data/support/mobile/ios/App.xcodeproj/project.pbxproj +2 -2
  43. data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -1
  44. data/support/mobile/ios/README.md +13 -6
  45. 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
- saved_tty = `stty -g`.chomp
510
- system("stty", "-icanon", "-echo")
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) unless saved_tty.to_s.empty?
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 Errno::ECONNREFUSED, Errno::ETIMEDOUT
680
+ rescue SystemCallError
620
681
  false
621
682
  end
622
683
 
623
- def wait_for_port(port, timeout: 60)
624
- deadline = Time.now + timeout
625
- sleep 0.5 until port_open?(port) || Time.now > deadline
626
- UI.die!("dev server never came up on port #{port}") unless port_open?(port)
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. Desktop checks always
11
- # run; the iOS and Android sections join in when the app's build.targets
12
- # include such a target, or explicitly via --target ios-* / android-*.
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 the sections)"
17
- option :target, default: nil, desc: "Also check a specific target's toolchain (e.g. ios-arm64)"
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 = true
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
- ok &= ios_checks if ios_wanted?(root, target)
26
- ok &= android_checks if android_wanted?(root, target)
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 ? UI.success("ready to build") : UI.bad("fix the items above (see https://rubyeverywhere.com/docs/diy/requirements)")
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.load(File.expand_path(root)).targets
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
- ok = check("full Xcode selected") { Shellout.run?("xcodebuild -version > /dev/null 2>&1") }
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.load(File.expand_path(root)).targets
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 ruby_everywhere to Gemfile", false) if contents.match?(/gem ["']ruby_everywhere["']/)
129
+ return change("add #{name} to Gemfile", false) if contents.match?(/gem ["']#{name}["']/)
107
130
 
108
- File.write(gemfile, "#{contents.chomp}\n\n# Desktop apps from this #{@framework.name.capitalize} app — https://rubyeverywhere.com\ngem \"ruby_everywhere\"\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 ruby_everywhere to Gemfile", true)
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`) work today;
218
- # # # hosted platform builds for iOS aren't live yet.
219
- # # - android-arm64 # future mobile is remote mode only
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
- File.write(boot, contents) if made
332
- change("guard bootsnap for packaged runs", !!made)
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