ruby_everywhere 0.6.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.
- checksums.yaml +4 -4
- data/exe/every +2 -2
- data/exe/rbe +2 -2
- data/lib/everywhere/agents_guide.rb +3 -1
- data/lib/everywhere/blake2b.rb +17 -1
- data/lib/everywhere/boot.rb +30 -6
- data/lib/everywhere/builders/android.rb +152 -64
- data/lib/everywhere/builders/base.rb +53 -0
- data/lib/everywhere/builders/desktop.rb +32 -40
- data/lib/everywhere/builders/ios.rb +268 -36
- data/lib/everywhere/builders/native_sources.rb +38 -0
- data/lib/everywhere/child_processes.rb +4 -4
- data/lib/everywhere/child_supervision.rb +172 -0
- data/lib/everywhere/cli.rb +2 -0
- data/lib/everywhere/clock.rb +12 -0
- data/lib/everywhere/commands/build.rb +132 -66
- data/lib/everywhere/commands/clean.rb +8 -3
- data/lib/everywhere/commands/dev.rb +91 -242
- data/lib/everywhere/commands/doctor.rb +138 -21
- data/lib/everywhere/commands/install.rb +49 -8
- data/lib/everywhere/commands/platform/auth_status.rb +1 -0
- data/lib/everywhere/commands/platform/build.rb +141 -33
- data/lib/everywhere/commands/platform/login.rb +20 -6
- data/lib/everywhere/commands/platform/logout.rb +1 -0
- data/lib/everywhere/commands/platform/runner.rb +170 -25
- data/lib/everywhere/commands/preview.rb +286 -0
- data/lib/everywhere/commands/publish.rb +22 -2
- data/lib/everywhere/commands/release.rb +155 -35
- data/lib/everywhere/commands/shell_dir.rb +4 -2
- data/lib/everywhere/commands/updates_keygen.rb +25 -1
- data/lib/everywhere/config/app.rb +126 -0
- data/lib/everywhere/config/auth.rb +108 -0
- data/lib/everywhere/config/data.rb +50 -0
- data/lib/everywhere/config/deep_linking.rb +107 -0
- data/lib/everywhere/config/desktop_ui.rb +153 -0
- data/lib/everywhere/config/mobile.rb +211 -0
- data/lib/everywhere/config/native_desktop.rb +168 -0
- data/lib/everywhere/config/native_mobile.rb +337 -0
- data/lib/everywhere/config/shell.rb +57 -0
- data/lib/everywhere/config/updates.rb +63 -0
- data/lib/everywhere/config.rb +59 -1367
- data/lib/everywhere/console.rb +2 -1
- data/lib/everywhere/desktop_dev_app.rb +138 -0
- data/lib/everywhere/dock/state.rb +3 -1
- data/lib/everywhere/engine.rb +24 -0
- data/lib/everywhere/entrypoint.rb +24 -0
- data/lib/everywhere/error.rb +8 -0
- data/lib/everywhere/framework.rb +23 -6
- data/lib/everywhere/host.rb +11 -0
- data/lib/everywhere/ignore.rb +13 -5
- data/lib/everywhere/jump.rb +121 -0
- data/lib/everywhere/line_pump.rb +3 -1
- data/lib/everywhere/minisign.rb +1 -0
- data/lib/everywhere/mobile_config_endpoint.rb +47 -0
- data/lib/everywhere/mobile_configs_controller.rb +46 -0
- data/lib/everywhere/native_platform.rb +44 -0
- data/lib/everywhere/paths.rb +36 -13
- data/lib/everywhere/platform/client.rb +46 -7
- data/lib/everywhere/platform/credentials.rb +18 -8
- data/lib/everywhere/platform/snapshot.rb +12 -0
- data/lib/everywhere/plist.rb +17 -0
- data/lib/everywhere/png.rb +1 -0
- data/lib/everywhere/raw_tty.rb +51 -0
- data/lib/everywhere/receipt.rb +55 -10
- data/lib/everywhere/s3.rb +1 -0
- data/lib/everywhere/shell_pages.rb +109 -0
- data/lib/everywhere/shellout.rb +19 -0
- data/lib/everywhere/simulator.rb +12 -1
- data/lib/everywhere/tab_filter.rb +36 -0
- data/lib/everywhere/task_pool.rb +3 -4
- data/lib/everywhere/ui.rb +6 -1
- data/lib/everywhere/version.rb +6 -4
- data/lib/everywhere.rb +1 -2
- 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 -4
- data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -1
- data/support/mobile/ios/README.md +13 -6
- data/support/release/macos/notarize.sh +3 -0
- metadata +40 -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 "../
|
|
9
|
-
require_relative "../
|
|
10
|
-
require_relative "../
|
|
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"
|
|
@@ -58,12 +59,9 @@ module Everywhere
|
|
|
58
59
|
def call(port: "3000", shell_dir: nil, target: nil, desktop: false, ios: false, android: false,
|
|
59
60
|
mobile: false, browser: false, dev_url: nil, **)
|
|
60
61
|
@shell_dir = shell_dir
|
|
61
|
-
@port = port
|
|
62
|
+
port = @port = validate_port!(port)
|
|
62
63
|
@dev_url_override = dev_url
|
|
63
|
-
|
|
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
|
|
|
@@ -147,8 +151,8 @@ module Everywhere
|
|
|
147
151
|
else
|
|
148
152
|
UI.step("starting #{UI.bold(framework.name)} dev server #{UI.dim("(" + framework.dev_command + ")")}")
|
|
149
153
|
@dock.set(:web, :booting)
|
|
150
|
-
supervise(:web, { "PORT" => port.to_s }, framework.dev_command, chdir: framework.root)
|
|
151
|
-
wait_for_port(port)
|
|
154
|
+
pid = supervise(:web, { "PORT" => port.to_s }, framework.dev_command, chdir: framework.root)
|
|
155
|
+
wait_for_port(port, pid: pid)
|
|
152
156
|
@dock.set(:web, :running, detail: ":#{port}")
|
|
153
157
|
end
|
|
154
158
|
end
|
|
@@ -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,85 +206,13 @@ 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
|
|
210
|
+
# Checked before anything is staged: without cargo the build dies deep
|
|
211
|
+
# in a relayed `sh: cargo: command not found`, after a dock entry and a
|
|
212
|
+
# half-written dev bundle say a shell is on its way.
|
|
213
|
+
Shellout.ensure_tool!("cargo", "the desktop shell is built with Rust — install it from " \
|
|
214
|
+
"https://rustup.rs, then press d again")
|
|
215
|
+
|
|
275
216
|
# A bad `window:` or a broken assets/ tree marks the desktop red and
|
|
276
217
|
# leaves the dev server and the mobile shells alone — the same deal
|
|
277
218
|
# every other target gets when its build fails. Dying here would take
|
|
@@ -284,7 +225,7 @@ module Everywhere
|
|
|
284
225
|
return
|
|
285
226
|
end
|
|
286
227
|
|
|
287
|
-
assets_dir, asset_errors = stage_desktop_assets
|
|
228
|
+
assets_dir, asset_errors = desktop_dev_app.stage_desktop_assets
|
|
288
229
|
unless asset_errors.empty?
|
|
289
230
|
asset_errors.each { |error| UI.warn(error) }
|
|
290
231
|
@dock.set(:desktop, :failed, detail: "native/desktop/assets")
|
|
@@ -320,10 +261,10 @@ module Everywhere
|
|
|
320
261
|
supervise(:desktop,
|
|
321
262
|
{ "NATIVE_DEV_URL" => @dev_url, "NATIVE_CONFIG" => @config.to_shell_json,
|
|
322
263
|
"NATIVE_ASSETS_DIR" => assets_dir,
|
|
323
|
-
"NATIVE_ICON_PATH" => stage_desktop_icon,
|
|
264
|
+
"NATIVE_ICON_PATH" => desktop_dev_app.stage_desktop_icon,
|
|
324
265
|
"CARGO_TARGET_DIR" => prepared.target_dir,
|
|
325
266
|
"CARGO_TERM_COLOR" => "always" },
|
|
326
|
-
desktop_command(prepared), chdir: prepared.dir,
|
|
267
|
+
desktop_dev_app.desktop_command(prepared), chdir: prepared.dir,
|
|
327
268
|
filter: LogFilter.for(:cargo), log: dist_log("desktop-dev.log"),
|
|
328
269
|
# cargo colors its status lines, so the marker has to be
|
|
329
270
|
# matched past the escape codes. "Finished" is cargo's last
|
|
@@ -334,37 +275,21 @@ module Everywhere
|
|
|
334
275
|
})
|
|
335
276
|
end
|
|
336
277
|
|
|
337
|
-
# macOS runs the shell from inside a throwaway .app so it has a name;
|
|
338
|
-
# everywhere else `cargo run` is still the whole story.
|
|
339
|
-
#
|
|
340
|
-
# Three steps in one command rather than three Ruby calls, because the
|
|
341
|
-
# link has to happen between the compile and the launch and we want ONE
|
|
342
|
-
# supervised child: cargo's progress relays into the dock, and exec then
|
|
343
|
-
# replaces it with the app, so the pid we're tracking is the window.
|
|
344
|
-
#
|
|
345
|
-
# A hard link, not a copy: same volume (both under ~/.rubyeverywhere), so
|
|
346
|
-
# it's free and instant, where copying a debug Tauri binary is ~100MB on
|
|
347
|
-
# every launch. cargo replaces the file on rebuild rather than writing
|
|
348
|
-
# through it, so the link is remade each time — hence `ln -f`.
|
|
349
|
-
def desktop_command(prepared)
|
|
350
|
-
return "cargo run" unless RUBY_PLATFORM.include?("darwin")
|
|
351
|
-
|
|
352
|
-
_app, exe = stage_dev_bundle
|
|
353
|
-
built = File.join(prepared.target_dir, "debug", "example-shell")
|
|
354
|
-
[
|
|
355
|
-
"cargo build",
|
|
356
|
-
"{ ln -f #{built.shellescape} #{exe.shellescape} 2>/dev/null || " \
|
|
357
|
-
"cp -f #{built.shellescape} #{exe.shellescape}; }",
|
|
358
|
-
"exec #{exe.shellescape}"
|
|
359
|
-
].join(" && ")
|
|
360
|
-
end
|
|
361
|
-
|
|
362
278
|
# Build the shell (Debug, warm after the first run), put it on a booted
|
|
363
279
|
# Simulator and hand it the dev URL. The Simulator shares the Mac's
|
|
364
280
|
# loopback, so 127.0.0.1 works as-is; the template's ATS exception
|
|
365
281
|
# covers plain http for local networking. Pressing i again rebuilds and
|
|
366
282
|
# relaunches, so it doubles as a native-shell reload.
|
|
367
283
|
def launch_ios
|
|
284
|
+
# xcodebuild and simctl are macOS-only, so `i` elsewhere can only ever
|
|
285
|
+
# produce a long build that fails at the first tool. Android stays
|
|
286
|
+
# available everywhere — its toolchain genuinely is cross-platform.
|
|
287
|
+
unless Host.darwin?
|
|
288
|
+
UI.warn("iOS builds need macOS and Xcode — #{UI.dim("nothing to do on this platform")}")
|
|
289
|
+
@dock.set(:ios, :idle, detail: "needs macOS")
|
|
290
|
+
return
|
|
291
|
+
end
|
|
292
|
+
|
|
368
293
|
@dock.set(:ios, :building)
|
|
369
294
|
app = Builders::Ios.new(config: @config, root: @config.root, target: IOS_TARGET)
|
|
370
295
|
.build!(dist_dir: File.expand_path("dist", @config.root), dev: true)
|
|
@@ -406,7 +331,22 @@ module Everywhere
|
|
|
406
331
|
|
|
407
332
|
def launch_browser
|
|
408
333
|
UI.step("opening browser → #{UI.cyan(@dev_url)}")
|
|
409
|
-
|
|
334
|
+
# `start` is a cmd builtin, not an executable, so it can't be probed or
|
|
335
|
+
# spawned directly; the empty "" is the window title argument, without
|
|
336
|
+
# which cmd reads the URL as one.
|
|
337
|
+
if Gem.win_platform?
|
|
338
|
+
system("cmd", "/c", "start", "", @dev_url)
|
|
339
|
+
return
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
opener = Host.darwin? ? "open" : "xdg-open"
|
|
343
|
+
# A headless Linux box (or WSL) has no xdg-open, and run? would just
|
|
344
|
+
# return nil — the key would look broken. Print the URL instead.
|
|
345
|
+
unless Shellout.tool?(opener)
|
|
346
|
+
UI.warn("couldn't find #{opener} — open #{UI.cyan(@dev_url)} yourself")
|
|
347
|
+
return
|
|
348
|
+
end
|
|
349
|
+
|
|
410
350
|
Shellout.run?(opener, @dev_url, quiet: true)
|
|
411
351
|
end
|
|
412
352
|
|
|
@@ -431,109 +371,51 @@ module Everywhere
|
|
|
431
371
|
|
|
432
372
|
# --- child supervision ----------------------------------------------------
|
|
433
373
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
# believe they're on a terminal and keep their colors; PTY.spawn also
|
|
437
|
-
# setsids, so a Ctrl-C at our terminal can't reach them and teardown
|
|
438
|
-
# decides when they die.
|
|
439
|
-
def supervise(key, env, cmd, chdir:, filter: nil, log: nil, on_line: nil)
|
|
440
|
-
if Shellout.pty?
|
|
441
|
-
io, pid = Shellout.spawn_pty(env, cmd, chdir: chdir)
|
|
442
|
-
thread = relay_thread(key, io, pid, filter: filter, log: log, on_line: on_line)
|
|
443
|
-
else
|
|
444
|
-
# Windows, or any platform without a pty: fall back to inherited fds.
|
|
445
|
-
# No tags and no dock framing, but everything else works.
|
|
446
|
-
pid = Shellout.spawn(env, cmd, chdir: chdir)
|
|
447
|
-
end
|
|
448
|
-
Process.detach(pid)
|
|
449
|
-
@children.synchronize do
|
|
450
|
-
@pids[key] = pid
|
|
451
|
-
@relays[key] = thread if thread
|
|
452
|
-
end
|
|
453
|
-
pid
|
|
454
|
-
end
|
|
374
|
+
def spawn_child(env, cmd, chdir:)
|
|
375
|
+
return Shellout.spawn_pty(env, cmd, chdir: chdir) if Shellout.pty?
|
|
455
376
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
Thread.current.report_on_exception = false
|
|
460
|
-
begin
|
|
461
|
-
Relay.new(io, source: key, log: log, filter: filter, on_line: on_line).run
|
|
462
|
-
rescue StandardError => e
|
|
463
|
-
# Losing a relay costs us that child's output, nothing more. It must
|
|
464
|
-
# not surface later out of the join in teardown.
|
|
465
|
-
Kernel.warn("#{key} output relay stopped: #{e.class}: #{e.message}")
|
|
466
|
-
end
|
|
467
|
-
# Only report a death we didn't ask for. stop_child removes the pid
|
|
468
|
-
# BEFORE it signals, so a deliberate restart can never match here —
|
|
469
|
-
# what does match is a shell that quit or crashed on its own, which is
|
|
470
|
-
# exactly what the dock should show.
|
|
471
|
-
@dock.set(key, :stopped) if !@stopping && @children.synchronize { @pids[key] } == pid
|
|
472
|
-
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)]
|
|
473
380
|
end
|
|
474
381
|
|
|
475
|
-
#
|
|
476
|
-
#
|
|
477
|
-
#
|
|
478
|
-
def
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
reap([pid], grace: grace)
|
|
483
|
-
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
|
|
484
389
|
end
|
|
485
390
|
|
|
486
|
-
#
|
|
487
|
-
#
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
live.each { |pid| ChildProcesses.signal(pid, "TERM") }
|
|
493
|
-
deadline = monotonic + grace
|
|
494
|
-
sleep(0.1) while live.any? { |pid| ChildProcesses.alive?(pid) } && monotonic < deadline
|
|
495
|
-
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
|
|
496
397
|
end
|
|
497
398
|
|
|
498
|
-
def child_pid(key) = @children.synchronize { @pids[key] }
|
|
499
|
-
|
|
500
399
|
# --- the key menu ---------------------------------------------------------
|
|
501
400
|
|
|
502
|
-
#
|
|
503
|
-
# and stair-steps every log line we relay; stty -icanon -echo disables only
|
|
504
|
-
# line buffering and echo, leaving output processing (and Ctrl-C as SIGINT)
|
|
505
|
-
# 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
|
|
506
402
|
# while one is going — including `q`.
|
|
507
403
|
def interact
|
|
508
404
|
@dock.announce_keys
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
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
|
|
522
417
|
end
|
|
523
418
|
end
|
|
524
|
-
ensure
|
|
525
|
-
system("stty", saved_tty) unless saved_tty.to_s.empty?
|
|
526
|
-
end
|
|
527
|
-
|
|
528
|
-
# The dock hides the cursor and the key loop puts the tty in -icanon
|
|
529
|
-
# -echo; a SIGTERM that skipped our ensure blocks would leave the user's
|
|
530
|
-
# shell in that state. exit(1) from a trap unwinds normally, which is all
|
|
531
|
-
# this needs to do — a handler must stay this small, because anything that
|
|
532
|
-
# takes a lock (Console, the dock) raises ThreadError in trap context.
|
|
533
|
-
def trap_exit_signals
|
|
534
|
-
%w[TERM HUP].each { |sig| Signal.trap(sig) { exit(1) } }
|
|
535
|
-
rescue ArgumentError
|
|
536
|
-
nil
|
|
537
419
|
end
|
|
538
420
|
|
|
539
421
|
# --- headless -------------------------------------------------------------
|
|
@@ -579,26 +461,8 @@ module Everywhere
|
|
|
579
461
|
|
|
580
462
|
# --- teardown -------------------------------------------------------------
|
|
581
463
|
|
|
582
|
-
# Teardown must never raise — it runs from an ensure, where an exception
|
|
583
|
-
# would mask whatever actually ended the session — and one failed step
|
|
584
|
-
# must not skip the rest, or a child survives the CLI that spawned it.
|
|
585
464
|
def teardown
|
|
586
|
-
@
|
|
587
|
-
pids, relays = @children ? @children.synchronize { [@pids.values, @relays.values] } : [[], []]
|
|
588
|
-
[
|
|
589
|
-
-> { @dock&.close },
|
|
590
|
-
-> { @pool&.shutdown },
|
|
591
|
-
# TERM everything, then give it a moment and KILL whatever is left.
|
|
592
|
-
# The CLI exits the instant this returns, so anything still alive is
|
|
593
|
-
# orphaned — and a windowed shell is exactly the kind of child that
|
|
594
|
-
# doesn't drop dead on the first signal.
|
|
595
|
-
-> { reap(pids, grace: 2) },
|
|
596
|
-
-> { relays.each { |thread| thread.join(1) } }
|
|
597
|
-
].each do |step|
|
|
598
|
-
step.call
|
|
599
|
-
rescue StandardError => e
|
|
600
|
-
Kernel.warn("teardown: #{e.class}: #{e.message}")
|
|
601
|
-
end
|
|
465
|
+
teardown_children(before: [-> { @dock&.close }, -> { @pool&.shutdown }])
|
|
602
466
|
end
|
|
603
467
|
|
|
604
468
|
# --- helpers --------------------------------------------------------------
|
|
@@ -610,21 +474,6 @@ module Everywhere
|
|
|
610
474
|
rescue StandardError
|
|
611
475
|
nil
|
|
612
476
|
end
|
|
613
|
-
|
|
614
|
-
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
615
|
-
|
|
616
|
-
def port_open?(port)
|
|
617
|
-
TCPSocket.new("127.0.0.1", Integer(port)).close
|
|
618
|
-
true
|
|
619
|
-
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
|
|
620
|
-
false
|
|
621
|
-
end
|
|
622
|
-
|
|
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)
|
|
627
|
-
end
|
|
628
477
|
end
|
|
629
478
|
end
|
|
630
479
|
end
|
|
@@ -2,49 +2,167 @@
|
|
|
2
2
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require_relative "../shellout"
|
|
5
|
+
require_relative "../host"
|
|
5
6
|
require_relative "../paths"
|
|
7
|
+
require_relative "../config"
|
|
6
8
|
require_relative "../android_sdk"
|
|
7
9
|
|
|
8
10
|
module Everywhere
|
|
9
11
|
module Commands
|
|
10
|
-
# Check that this machine can build native apps.
|
|
11
|
-
# run
|
|
12
|
-
#
|
|
12
|
+
# Check that this machine can build native apps. Every section always
|
|
13
|
+
# shows, so one run answers "what can this machine build?" — but only the
|
|
14
|
+
# sections the app's build.targets declare (or --target names) gate the
|
|
15
|
+
# exit status: failing doctor over a toolchain the app doesn't use would
|
|
16
|
+
# be a lie, the same reasoning as the cmdline-tools warning below.
|
|
13
17
|
class Doctor < Dry::CLI::Command
|
|
14
18
|
desc "Check the toolchain (rbe-tebako, rust, brew deps; Xcode for iOS, SDK/JDK for Android)"
|
|
15
19
|
|
|
16
|
-
option :root, default: ".", desc: "App root (its build.targets pick
|
|
17
|
-
option :target, default: nil, desc: "
|
|
20
|
+
option :root, default: ".", desc: "App root (its build.targets pick which sections are required)"
|
|
21
|
+
option :target, default: nil, desc: "Require a specific target's toolchain (e.g. ios-arm64)"
|
|
18
22
|
|
|
19
23
|
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? }
|
|
24
|
+
ok = config_check(root)
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
@optional_gaps = []
|
|
27
|
+
sections = [
|
|
28
|
+
["Desktop", desktop_wanted?(root, target), method(:desktop_checks)],
|
|
29
|
+
["iOS", ios_wanted?(root, target), method(:ios_checks)],
|
|
30
|
+
["Android", android_wanted?(root, target), method(:android_checks)]
|
|
31
|
+
]
|
|
32
|
+
# The sections that gate the run are the ones the user came to fix, so
|
|
33
|
+
# they come first; the informational ones read as an appendix.
|
|
34
|
+
# partition, not sort_by — Ruby's sort isn't stable.
|
|
35
|
+
required_sections, info_sections = sections.partition { |_, required, _| required }
|
|
36
|
+
(required_sections + info_sections).each do |name, required, checks|
|
|
37
|
+
ok &= section(name, required: required) { |req| checks.call(req) }
|
|
38
|
+
end
|
|
27
39
|
|
|
28
40
|
puts
|
|
29
|
-
ok
|
|
41
|
+
if ok
|
|
42
|
+
note = @optional_gaps.any? ? " #{UI.dim("(#{@optional_gaps.join(" and ")} would need setup — see above)")}" : ""
|
|
43
|
+
UI.success("ready to build#{note}")
|
|
44
|
+
else
|
|
45
|
+
UI.bad("fix the items above (see https://rubyeverywhere.com/docs/diy/requirements)")
|
|
46
|
+
end
|
|
30
47
|
exit(1) unless ok
|
|
31
48
|
end
|
|
32
49
|
|
|
33
50
|
private
|
|
34
51
|
|
|
52
|
+
# A section always renders; only a required one can fail the run. An
|
|
53
|
+
# optional section's gaps are remembered for the summary line instead.
|
|
54
|
+
def section(name, required:)
|
|
55
|
+
passed = yield(required)
|
|
56
|
+
@optional_gaps << name if !passed && !required
|
|
57
|
+
required ? passed : true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def section_label(name, required)
|
|
61
|
+
required ? name : "#{name} #{UI.dim("(not in build.targets — informational)")}"
|
|
62
|
+
end
|
|
63
|
+
|
|
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 Host.darwin?
|
|
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 Host.darwin?
|
|
105
|
+
|
|
106
|
+
out, status = Shellout.capture("brew", "--prefix", "bison")
|
|
107
|
+
!status.nil? && status.success? && !out.strip.empty?
|
|
108
|
+
end
|
|
109
|
+
|
|
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
|
+
|
|
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 Host.darwin?
|
|
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
|
|