ruby_everywhere 0.7.0 → 0.8.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/exe/every +2 -13
  3. data/exe/rbe +2 -13
  4. data/lib/everywhere/boot.rb +9 -2
  5. data/lib/everywhere/builders/android.rb +16 -42
  6. data/lib/everywhere/builders/base.rb +53 -0
  7. data/lib/everywhere/builders/desktop.rb +7 -23
  8. data/lib/everywhere/builders/ios.rb +49 -52
  9. data/lib/everywhere/builders/native_sources.rb +38 -0
  10. data/lib/everywhere/child_processes.rb +4 -4
  11. data/lib/everywhere/child_supervision.rb +172 -0
  12. data/lib/everywhere/clock.rb +12 -0
  13. data/lib/everywhere/commands/build.rb +20 -9
  14. data/lib/everywhere/commands/dev.rb +59 -284
  15. data/lib/everywhere/commands/doctor.rb +6 -6
  16. data/lib/everywhere/commands/install.rb +1 -0
  17. data/lib/everywhere/commands/platform/auth_status.rb +1 -0
  18. data/lib/everywhere/commands/platform/build.rb +5 -6
  19. data/lib/everywhere/commands/platform/login.rb +4 -4
  20. data/lib/everywhere/commands/platform/logout.rb +1 -0
  21. data/lib/everywhere/commands/platform/runner.rb +62 -7
  22. data/lib/everywhere/commands/preview.rb +24 -97
  23. data/lib/everywhere/commands/publish.rb +2 -0
  24. data/lib/everywhere/commands/release.rb +14 -11
  25. data/lib/everywhere/commands/shell_dir.rb +2 -0
  26. data/lib/everywhere/commands/updates_keygen.rb +25 -1
  27. data/lib/everywhere/config/app.rb +126 -0
  28. data/lib/everywhere/config/auth.rb +108 -0
  29. data/lib/everywhere/config/data.rb +50 -0
  30. data/lib/everywhere/config/deep_linking.rb +107 -0
  31. data/lib/everywhere/config/desktop_ui.rb +153 -0
  32. data/lib/everywhere/config/mobile.rb +211 -0
  33. data/lib/everywhere/config/native_desktop.rb +168 -0
  34. data/lib/everywhere/config/native_mobile.rb +337 -0
  35. data/lib/everywhere/config/shell.rb +57 -0
  36. data/lib/everywhere/config/updates.rb +63 -0
  37. data/lib/everywhere/config.rb +30 -1423
  38. data/lib/everywhere/desktop_dev_app.rb +138 -0
  39. data/lib/everywhere/dock/state.rb +3 -1
  40. data/lib/everywhere/entrypoint.rb +24 -0
  41. data/lib/everywhere/error.rb +8 -0
  42. data/lib/everywhere/framework.rb +2 -2
  43. data/lib/everywhere/host.rb +11 -0
  44. data/lib/everywhere/ignore.rb +11 -5
  45. data/lib/everywhere/line_pump.rb +3 -1
  46. data/lib/everywhere/minisign.rb +1 -0
  47. data/lib/everywhere/native_platform.rb +44 -0
  48. data/lib/everywhere/paths.rb +23 -7
  49. data/lib/everywhere/platform/client.rb +19 -1
  50. data/lib/everywhere/platform/snapshot.rb +6 -4
  51. data/lib/everywhere/plist.rb +17 -0
  52. data/lib/everywhere/png.rb +1 -0
  53. data/lib/everywhere/raw_tty.rb +51 -0
  54. data/lib/everywhere/s3.rb +1 -0
  55. data/lib/everywhere/shell_pages.rb +109 -0
  56. data/lib/everywhere/tab_filter.rb +36 -0
  57. data/lib/everywhere/task_pool.rb +3 -4
  58. data/lib/everywhere/ui.rb +6 -1
  59. data/lib/everywhere/version.rb +6 -4
  60. data/lib/everywhere.rb +1 -2
  61. data/support/mobile/ios/App.xcodeproj/project.pbxproj +0 -2
  62. data/support/release/macos/notarize.sh +3 -0
  63. metadata +24 -1
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/cli"
4
- require "socket"
5
4
  require "shellwords"
6
5
  require "fileutils"
7
6
  require_relative "../shellout"
8
- require_relative "../paths"
9
- require_relative "../icon"
10
- require_relative "../desktop_assets"
7
+ require_relative "../host"
8
+ require_relative "../desktop_dev_app"
9
+ require_relative "../raw_tty"
11
10
  require_relative "../builders/ios"
12
11
  require_relative "../builders/android"
13
12
  require_relative "../builders/desktop"
@@ -15,8 +14,8 @@ require_relative "../simulator"
15
14
  require_relative "../emulator"
16
15
  require_relative "../console"
17
16
  require_relative "../child_processes"
17
+ require_relative "../child_supervision"
18
18
  require_relative "../task_pool"
19
- require_relative "../relay"
20
19
  require_relative "../dock"
21
20
  require_relative "../log_filter"
22
21
  require_relative "logs"
@@ -37,6 +36,8 @@ module Everywhere
37
36
  # Dock keeps the keymap and every target's state pinned to the bottom of the
38
37
  # terminal while the logs scroll above it.
39
38
  class Dev < Dry::CLI::Command
39
+ include ChildSupervision
40
+
40
41
  desc "Run the dev server with native shells on demand (live reload, no packaging)"
41
42
 
42
43
  IOS_TARGET = "ios-arm64"
@@ -60,10 +61,7 @@ module Everywhere
60
61
  @shell_dir = shell_dir
61
62
  port = @port = validate_port!(port)
62
63
  @dev_url_override = dev_url
63
- # Children are spawned from pool threads as well as the main one.
64
- @pids = {}
65
- @relays = {}
66
- @children = Mutex.new
64
+ init_children!
67
65
  targets = requested_targets(target, desktop: desktop, ios: ios, android: android,
68
66
  mobile: mobile, browser: browser)
69
67
  @framework, @config = detect_app
@@ -127,6 +125,12 @@ module Everywhere
127
125
  UI.die!("no supported framework here (Rails, Hanami, Sinatra) and no remote.url in " \
128
126
  "config/everywhere.yml — nothing to run")
129
127
  end
128
+
129
+ # The bundle id keys every per-app dir under ~/.rubyeverywhere that the
130
+ # session stages into, so it's checked before anything is written.
131
+ errors = config.identity_errors
132
+ UI.die!("app: in config/everywhere.yml:\n#{errors.join("\n")}") unless errors.empty?
133
+
130
134
  [framework, config]
131
135
  end
132
136
 
@@ -186,6 +190,15 @@ module Everywhere
186
190
  end
187
191
  end
188
192
 
193
+ # Everything the running desktop shell is staged out of: the assets, the
194
+ # Dock icon, the throwaway .app it runs from. Memoized because a restart
195
+ # restages all of it against the same config.
196
+ def desktop_dev_app = @desktop_dev_app ||= DesktopDevApp.new(config: @config)
197
+
198
+ def dev_bundle_name = desktop_dev_app.dev_bundle_name
199
+
200
+ def dev_info_plist = desktop_dev_app.dev_info_plist
201
+
189
202
  # `d` relaunches, the same way `i` and `a` do — quit the running window and
190
203
  # open a fresh one. Refusing with "already running" made the desktop the
191
204
  # odd one out, and left no way to restart a shell that had wedged or was
@@ -193,84 +206,6 @@ module Everywhere
193
206
  #
194
207
  # The rebuild is free either way: a warm `cargo run` re-checks the crate
195
208
  # graph in well under a second before handing off.
196
- # native/desktop/assets/** → the staged flat folder the running shell
197
- # reads. Runs the same compiler `every build` does, so an asset that
198
- # resolves in dev resolves in the packaged app too. Returns
199
- # [dir, errors]; the dir is returned even when the app ships no assets,
200
- # because the shell only has to cope with it not existing.
201
- def stage_desktop_assets
202
- dest = Everywhere::Paths.desktop_assets_dir(@config.bundle_id)
203
- source = File.join(@config.root, "native", "desktop", "assets")
204
- _names, errors = Everywhere::DesktopAssets.write!(source, dest)
205
- [dest, errors]
206
- end
207
-
208
- # A minimal .app around the dev binary, so macOS has a CFBundleName to
209
- # read. Without it every menu that names the app says "example-shell" —
210
- # the crate name — because an unbundled process has nothing else to go on.
211
- # (Setting NSProcessInfo's processName at runtime does NOT fix this: the
212
- # menu bar and the predefined About/Hide/Quit items are built from the
213
- # bundle, and macOS has already decided by the time any Rust runs.)
214
- #
215
- # Only the plist matters here. No icon is stamped: the shell sets the Dock
216
- # icon at runtime from NATIVE_ICON_PATH, because Tauri applies its own
217
- # compiled-in icon after launch and would win over a bundle icns anyway.
218
- # `every build` writes the real, complete plist — this is the dev subset.
219
- def stage_dev_bundle
220
- app = File.join(Everywhere::Paths.desktop_dev_app_dir(@config.bundle_id),
221
- "#{@config.name}.app")
222
- macos = File.join(app, "Contents", "MacOS")
223
- FileUtils.mkdir_p(macos)
224
- File.write(File.join(app, "Contents", "Info.plist"), dev_info_plist)
225
- [app, File.join(macos, dev_exe_name)]
226
- end
227
-
228
- # Spaces are legal in CFBundleExecutable but a nuisance in every shell
229
- # command that names it, so the executable follows the packaged app's
230
- # convention and drops them.
231
- def dev_exe_name = @config.name.gsub(/\s+/, "")
232
-
233
- def dev_info_plist
234
- <<~PLIST
235
- <?xml version="1.0" encoding="UTF-8"?>
236
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
237
- <plist version="1.0">
238
- <dict>
239
- <key>CFBundleName</key><string>#{@config.name}</string>
240
- <key>CFBundleDisplayName</key><string>#{@config.name}</string>
241
- <key>CFBundleExecutable</key><string>#{dev_exe_name}</string>
242
- <key>CFBundleIdentifier</key><string>#{@config.bundle_id}</string>
243
- <key>CFBundlePackageType</key><string>APPL</string>
244
- <key>CFBundleShortVersionString</key><string>#{@config.version}</string>
245
- <key>NSHighResolutionCapable</key><true/>
246
- </dict>
247
- </plist>
248
- PLIST
249
- end
250
-
251
- # The Dock icon for the running shell. `cargo run` produces a bare binary
252
- # with no .app around it, so macOS has nothing to read an icon from and
253
- # Tauri's compiled-in placeholder wins — which is how you end up staring
254
- # at the gem's icon while testing your own app.
255
- #
256
- # Shaped with the same Big Sur inset + squircle the packaged .icns gets
257
- # (Icon.macos_master), so the Dock looks identical in dev and in a release
258
- # build rather than subtly rounder in one of them. Returns "" when the app
259
- # has no icon — the shell reads that as "leave it alone".
260
- def stage_desktop_icon
261
- source = @config.icon
262
- return "" unless source && File.exist?(source)
263
-
264
- dest = File.join(Everywhere::Paths.desktop_icon_dir(@config.bundle_id), "icon.png")
265
- FileUtils.mkdir_p(File.dirname(dest))
266
- FileUtils.cp(source, dest) unless Everywhere::Icon.macos_master(source, dest)
267
- dest
268
- rescue StandardError => e
269
- # A Dock icon is never worth failing a dev session over.
270
- UI.warn("couldn't prepare the Dock icon (#{e.class}) — using the default")
271
- ""
272
- end
273
-
274
209
  def launch_desktop
275
210
  # Checked before anything is staged: without cargo the build dies deep
276
211
  # in a relayed `sh: cargo: command not found`, after a dock entry and a
@@ -290,7 +225,7 @@ module Everywhere
290
225
  return
291
226
  end
292
227
 
293
- assets_dir, asset_errors = stage_desktop_assets
228
+ assets_dir, asset_errors = desktop_dev_app.stage_desktop_assets
294
229
  unless asset_errors.empty?
295
230
  asset_errors.each { |error| UI.warn(error) }
296
231
  @dock.set(:desktop, :failed, detail: "native/desktop/assets")
@@ -326,10 +261,10 @@ module Everywhere
326
261
  supervise(:desktop,
327
262
  { "NATIVE_DEV_URL" => @dev_url, "NATIVE_CONFIG" => @config.to_shell_json,
328
263
  "NATIVE_ASSETS_DIR" => assets_dir,
329
- "NATIVE_ICON_PATH" => stage_desktop_icon,
264
+ "NATIVE_ICON_PATH" => desktop_dev_app.stage_desktop_icon,
330
265
  "CARGO_TARGET_DIR" => prepared.target_dir,
331
266
  "CARGO_TERM_COLOR" => "always" },
332
- desktop_command(prepared), chdir: prepared.dir,
267
+ desktop_dev_app.desktop_command(prepared), chdir: prepared.dir,
333
268
  filter: LogFilter.for(:cargo), log: dist_log("desktop-dev.log"),
334
269
  # cargo colors its status lines, so the marker has to be
335
270
  # matched past the escape codes. "Finished" is cargo's last
@@ -340,31 +275,6 @@ module Everywhere
340
275
  })
341
276
  end
342
277
 
343
- # macOS runs the shell from inside a throwaway .app so it has a name;
344
- # everywhere else `cargo run` is still the whole story.
345
- #
346
- # Three steps in one command rather than three Ruby calls, because the
347
- # link has to happen between the compile and the launch and we want ONE
348
- # supervised child: cargo's progress relays into the dock, and exec then
349
- # replaces it with the app, so the pid we're tracking is the window.
350
- #
351
- # A hard link, not a copy: same volume (both under ~/.rubyeverywhere), so
352
- # it's free and instant, where copying a debug Tauri binary is ~100MB on
353
- # every launch. cargo replaces the file on rebuild rather than writing
354
- # through it, so the link is remade each time — hence `ln -f`.
355
- def desktop_command(prepared)
356
- return "cargo run" unless RUBY_PLATFORM.include?("darwin")
357
-
358
- _app, exe = stage_dev_bundle
359
- built = File.join(prepared.target_dir, "debug", "example-shell")
360
- [
361
- "cargo build",
362
- "{ ln -f #{built.shellescape} #{exe.shellescape} 2>/dev/null || " \
363
- "cp -f #{built.shellescape} #{exe.shellescape}; }",
364
- "exec #{exe.shellescape}"
365
- ].join(" && ")
366
- end
367
-
368
278
  # Build the shell (Debug, warm after the first run), put it on a booted
369
279
  # Simulator and hand it the dev URL. The Simulator shares the Mac's
370
280
  # loopback, so 127.0.0.1 works as-is; the template's ATS exception
@@ -374,7 +284,7 @@ module Everywhere
374
284
  # xcodebuild and simctl are macOS-only, so `i` elsewhere can only ever
375
285
  # produce a long build that fails at the first tool. Android stays
376
286
  # available everywhere — its toolchain genuinely is cross-platform.
377
- unless RUBY_PLATFORM.include?("darwin")
287
+ unless Host.darwin?
378
288
  UI.warn("iOS builds need macOS and Xcode — #{UI.dim("nothing to do on this platform")}")
379
289
  @dock.set(:ios, :idle, detail: "needs macOS")
380
290
  return
@@ -429,7 +339,7 @@ module Everywhere
429
339
  return
430
340
  end
431
341
 
432
- opener = RUBY_PLATFORM.include?("darwin") ? "open" : "xdg-open"
342
+ opener = Host.darwin? ? "open" : "xdg-open"
433
343
  # A headless Linux box (or WSL) has no xdg-open, and run? would just
434
344
  # return nil — the key would look broken. Print the URL instead.
435
345
  unless Shellout.tool?(opener)
@@ -461,124 +371,51 @@ module Everywhere
461
371
 
462
372
  # --- child supervision ----------------------------------------------------
463
373
 
464
- # Spawn a long-lived child on a pty and relay its output, one tagged line
465
- # at a time. A pty rather than a pipe so foreman, cargo and friends still
466
- # believe they're on a terminal and keep their colors; PTY.spawn also
467
- # setsids, so a Ctrl-C at our terminal can't reach them and teardown
468
- # decides when they die.
469
- def supervise(key, env, cmd, chdir:, filter: nil, log: nil, on_line: nil)
470
- if Shellout.pty?
471
- io, pid = Shellout.spawn_pty(env, cmd, chdir: chdir)
472
- thread = relay_thread(key, io, pid, filter: filter, log: log, on_line: on_line)
473
- else
474
- # Windows, or any platform without a pty: fall back to inherited fds.
475
- # No tags and no dock framing, but everything else works.
476
- pid = Shellout.spawn(env, cmd, chdir: chdir)
477
- end
478
- Process.detach(pid)
479
- @children.synchronize do
480
- @pids[key] = pid
481
- @relays[key] = thread if thread
482
- end
483
- pid
484
- end
374
+ def spawn_child(env, cmd, chdir:)
375
+ return Shellout.spawn_pty(env, cmd, chdir: chdir) if Shellout.pty?
485
376
 
486
- def relay_thread(key, io, pid, filter:, log:, on_line:)
487
- Thread.new do
488
- Thread.current.name = "relay-#{key}"
489
- Thread.current.report_on_exception = false
490
- begin
491
- Relay.new(io, source: key, log: log, filter: filter, on_line: on_line).run
492
- rescue StandardError => e
493
- # Losing a relay costs us that child's output, nothing more. It must
494
- # not surface later out of the join in teardown.
495
- Kernel.warn("#{key} output relay stopped: #{e.class}: #{e.message}")
496
- end
497
- # Only report a death we didn't ask for. stop_child removes the pid
498
- # BEFORE it signals, so a deliberate restart can never match here —
499
- # what does match is a shell that quit or crashed on its own, which is
500
- # exactly what the dock should show.
501
- @dock.set(key, :stopped) if !@stopping && @children.synchronize { @pids[key] } == pid
502
- end
377
+ # Windows, or any platform without a pty: fall back to inherited fds.
378
+ # No tags and no dock framing, but everything else works.
379
+ [nil, Shellout.spawn(env, cmd, chdir: chdir)]
503
380
  end
504
381
 
505
- # TERM the child's whole process group, escalate to KILL, and wait for the
506
- # relay to drain so its last lines aren't lost. The pid is dropped from
507
- # the registry FIRST, so the relay knows this death was asked for.
508
- def stop_child(key, grace: 5)
509
- pid, thread = @children.synchronize { [@pids.delete(key), @relays.delete(key)] }
510
- return unless pid
511
-
512
- reap([pid], grace: grace)
513
- thread&.join(1)
382
+ # The extra advice an `every dev` failure carries: this is the app's own
383
+ # bin/dev, run from the app's own checkout, so the two usual explanations
384
+ # are worth naming.
385
+ def wait_for_port(port, pid: nil, timeout: 60,
386
+ dead_hint: " (did `bundle install` run? does bin/dev's foreman exist?)",
387
+ slow_hint: " — it may just be slow to boot; check its output above")
388
+ super
514
389
  end
515
390
 
516
- # TERM each process group, wait for them together rather than serially,
517
- # then KILL the stragglers.
518
- def reap(pids, grace: 5)
519
- live = pids.compact.select { |pid| ChildProcesses.alive?(pid) }
520
- return if live.empty?
521
-
522
- live.each { |pid| ChildProcesses.signal(pid, "TERM") }
523
- deadline = monotonic + grace
524
- sleep(0.1) while live.any? { |pid| ChildProcesses.alive?(pid) } && monotonic < deadline
525
- live.select { |pid| ChildProcesses.alive?(pid) }.each { |pid| ChildProcesses.signal(pid, "KILL") }
391
+ # Only report a death we didn't ask for. stop_child removes the pid
392
+ # BEFORE it signals, so a deliberate restart can never match here —
393
+ # what does match is a shell that quit or crashed on its own, which is
394
+ # exactly what the dock should show.
395
+ def on_child_exit(key, pid)
396
+ @dock.set(key, :stopped) if !@stopping && @children.synchronize { @pids[key] } == pid
526
397
  end
527
398
 
528
- def child_pid(key) = @children.synchronize { @pids[key] }
529
-
530
399
  # --- the key menu ---------------------------------------------------------
531
400
 
532
- # io/console's getch holds full raw mode while blocked, which clears OPOST
533
- # and stair-steps every log line we relay; stty -icanon -echo disables only
534
- # line buffering and echo, leaving output processing (and Ctrl-C as SIGINT)
535
- # intact. Builds no longer run on this thread, so the menu stays responsive
401
+ # Builds no longer run on this thread, so the menu stays responsive
536
402
  # while one is going — including `q`.
537
403
  def interact
538
404
  @dock.announce_keys
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
544
- trap_exit_signals
545
- loop do
546
- case $stdin.getc
547
- when "d" then background(:desktop) { launch_desktop }
548
- when "i" then background(:ios) { launch_ios }
549
- when "a" then background(:android) { launch_android }
550
- when "b" then launch_browser
551
- when "l" then toggle_logs
552
- when "r" then background(:web) { restart_server }
553
- when "?" then @dock.toggle_help
554
- when "q", "\u0004", nil then break # q, Ctrl-D, EOF (Ctrl-C stays a real SIGINT)
405
+ RawTty.with do
406
+ loop do
407
+ case $stdin.getc
408
+ when "d" then background(:desktop) { launch_desktop }
409
+ when "i" then background(:ios) { launch_ios }
410
+ when "a" then background(:android) { launch_android }
411
+ when "b" then launch_browser
412
+ when "l" then toggle_logs
413
+ when "r" then background(:web) { restart_server }
414
+ when "?" then @dock.toggle_help
415
+ when "q", "\u0004", nil then break # q, Ctrl-D, EOF (Ctrl-C stays a real SIGINT)
416
+ end
555
417
  end
556
418
  end
557
- ensure
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
571
- end
572
-
573
- # The dock hides the cursor and the key loop puts the tty in -icanon
574
- # -echo; a SIGTERM that skipped our ensure blocks would leave the user's
575
- # shell in that state. exit(1) from a trap unwinds normally, which is all
576
- # this needs to do — a handler must stay this small, because anything that
577
- # takes a lock (Console, the dock) raises ThreadError in trap context.
578
- def trap_exit_signals
579
- %w[TERM HUP].each { |sig| Signal.trap(sig) { exit(1) } }
580
- rescue ArgumentError
581
- nil
582
419
  end
583
420
 
584
421
  # --- headless -------------------------------------------------------------
@@ -624,26 +461,8 @@ module Everywhere
624
461
 
625
462
  # --- teardown -------------------------------------------------------------
626
463
 
627
- # Teardown must never raise — it runs from an ensure, where an exception
628
- # would mask whatever actually ended the session — and one failed step
629
- # must not skip the rest, or a child survives the CLI that spawned it.
630
464
  def teardown
631
- @stopping = true
632
- pids, relays = @children ? @children.synchronize { [@pids.values, @relays.values] } : [[], []]
633
- [
634
- -> { @dock&.close },
635
- -> { @pool&.shutdown },
636
- # TERM everything, then give it a moment and KILL whatever is left.
637
- # The CLI exits the instant this returns, so anything still alive is
638
- # orphaned — and a windowed shell is exactly the kind of child that
639
- # doesn't drop dead on the first signal.
640
- -> { reap(pids, grace: 2) },
641
- -> { relays.each { |thread| thread.join(1) } }
642
- ].each do |step|
643
- step.call
644
- rescue StandardError => e
645
- Kernel.warn("teardown: #{e.class}: #{e.message}")
646
- end
465
+ teardown_children(before: [-> { @dock&.close }, -> { @pool&.shutdown }])
647
466
  end
648
467
 
649
468
  # --- helpers --------------------------------------------------------------
@@ -655,50 +474,6 @@ module Everywhere
655
474
  rescue StandardError
656
475
  nil
657
476
  end
658
-
659
- def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
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.
677
- def port_open?(port)
678
- TCPSocket.new("127.0.0.1", Integer(port)).close
679
- true
680
- rescue SystemCallError
681
- false
682
- end
683
-
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
701
- end
702
477
  end
703
478
  end
704
479
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "dry/cli"
4
4
  require_relative "../shellout"
5
+ require_relative "../host"
5
6
  require_relative "../paths"
6
7
  require_relative "../config"
7
8
  require_relative "../android_sdk"
@@ -60,7 +61,6 @@ module Everywhere
60
61
  required ? name : "#{name} #{UI.dim("(not in build.targets — informational)")}"
61
62
  end
62
63
 
63
- def macos? = RUBY_PLATFORM.include?("darwin")
64
64
 
65
65
  # Desktop is the implicit default: an app with no build.targets presses
66
66
  # for the host. Any non-mobile target (or --target) counts as desktop,
@@ -79,7 +79,7 @@ module Everywhere
79
79
  UI.step(section_label("Desktop target", required))
80
80
  # Mirrors the gate in `every build`: pressing is macOS-only today, so
81
81
  # probing for its toolchain elsewhere would only mislead.
82
- unless macos?
82
+ unless Host.darwin?
83
83
  return fix_check("macOS", "desktop builds are macOS-only for now") { false } if required
84
84
 
85
85
  UI.warn("desktop builds are macOS-only for now — skipped")
@@ -101,14 +101,14 @@ module Everywhere
101
101
  # it, since it stays off PATH (keg-only). Everywhere else the package
102
102
  # manager installs a current bison normally.
103
103
  def bison?
104
- return Shellout.tool?("bison") unless macos?
104
+ return Shellout.tool?("bison") unless Host.darwin?
105
105
 
106
106
  out, status = Shellout.capture("brew", "--prefix", "bison")
107
107
  !status.nil? && status.success? && !out.strip.empty?
108
108
  end
109
109
 
110
- def bison_label = macos? ? "homebrew bison" : "bison"
111
- def bison_hint = macos? ? "brew install bison" : "install bison with your package manager"
110
+ def bison_label = Host.darwin? ? "homebrew bison" : "bison"
111
+ def bison_hint = Host.darwin? ? "brew install bison" : "install bison with your package manager"
112
112
 
113
113
  # Config.load raises on an unparseable everywhere.yml; doctor reports it
114
114
  # as its first check and carries on with an empty config, so the machine
@@ -152,7 +152,7 @@ module Everywhere
152
152
  UI.step(section_label("iOS target", required))
153
153
  # Off macOS every tool below is guaranteed absent — one honest line
154
154
  # beats four unfixable ✗s.
155
- unless macos?
155
+ unless Host.darwin?
156
156
  return fix_check("macOS with Xcode", "iOS apps can only be built on a Mac") { false } if required
157
157
 
158
158
  UI.warn("needs macOS and Xcode — skipped")
@@ -4,6 +4,7 @@ require "dry/cli"
4
4
  require "fileutils"
5
5
  require_relative "../shellout"
6
6
  require_relative "../config"
7
+ require_relative "../framework"
7
8
  require_relative "../agents_guide"
8
9
 
9
10
  module Everywhere
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/cli"
4
+ require_relative "../../error"
4
5
  require_relative "../../ui"
5
6
  require_relative "../../platform/client"
6
7
  require_relative "../../platform/credentials"
@@ -3,6 +3,7 @@
3
3
  require "dry/cli"
4
4
  require "tmpdir"
5
5
  require "fileutils"
6
+ require_relative "../../clock"
6
7
  require_relative "../../ui"
7
8
  require_relative "../../config"
8
9
  require_relative "../../framework"
@@ -172,7 +173,7 @@ module Everywhere
172
173
  def poll_and_tail(client, build_id, build_url = nil)
173
174
  printed = Hash.new("")
174
175
  last = {}
175
- started = now
176
+ started = Clock.monotonic
176
177
  status = UI::Status.new
177
178
  errors = 0
178
179
 
@@ -220,10 +221,10 @@ module Everywhere
220
221
  # faster than we poll so the line reads as "alive" during the multi-minute
221
222
  # pre-runner wait rather than freezing between requests.
222
223
  def spin(status, artifacts, started, interval)
223
- deadline = now + interval
224
+ deadline = Clock.monotonic + interval
224
225
  loop do
225
- status.update("#{waiting_summary(artifacts)} · #{UI.elapsed(now - started)} elapsed")
226
- remaining = deadline - now
226
+ status.update("#{waiting_summary(artifacts)} · #{UI.elapsed(Clock.monotonic - started)} elapsed")
227
+ remaining = deadline - Clock.monotonic
227
228
  break if remaining <= 0
228
229
 
229
230
  sleep([remaining, SPINNER_TICK].min)
@@ -248,8 +249,6 @@ module Everywhere
248
249
  label.empty? ? artifact["status"].to_s : label
249
250
  end
250
251
 
251
- def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
252
-
253
252
  def tail_log(artifact, printed)
254
253
  full = artifact["log"].to_s
255
254
  prev = printed[artifact["id"]]
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "dry/cli"
4
4
  require "socket"
5
+ require_relative "../../clock"
6
+ require_relative "../../error"
5
7
  require_relative "../../ui"
6
8
  require_relative "../../platform/client"
7
9
  require_relative "../../platform/credentials"
@@ -51,10 +53,10 @@ module Everywhere
51
53
  private
52
54
 
53
55
  def poll(client, device_code, interval, expires_in)
54
- deadline = monotonic + (expires_in.positive? ? expires_in : 600)
56
+ deadline = Clock.monotonic + (expires_in.positive? ? expires_in : 600)
55
57
  loop do
56
58
  sleep(interval)
57
- UI.die!("login timed out — run `every platform login` again") if monotonic > deadline
59
+ UI.die!("login timed out — run `every platform login` again") if Clock.monotonic > deadline
58
60
 
59
61
  # The user is mid-approval in a browser; a dropped connection here must
60
62
  # not kill the login. Keep polling until the deadline instead.
@@ -75,8 +77,6 @@ module Everywhere
75
77
  end
76
78
  end
77
79
 
78
- def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
79
-
80
80
  # Shown on the approval page + used as the token's name, so a user can tell
81
81
  # their machines apart when reviewing/revoking tokens.
82
82
  def client_name
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/cli"
4
+ require_relative "../../error"
4
5
  require_relative "../../ui"
5
6
  require_relative "../../platform/client"
6
7
  require_relative "../../platform/credentials"