ruby_everywhere 0.3.0 → 0.5.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/README.md +339 -0
- data/bridge/README.md +70 -1
- data/bridge/everywhere/bridge.js +211 -3
- data/bridge/everywhere/native.css +61 -0
- data/lib/everywhere/android_resources.rb +217 -0
- data/lib/everywhere/android_sdk.rb +330 -0
- data/lib/everywhere/asset_catalog.rb +185 -0
- data/lib/everywhere/auth_handoff.rb +404 -0
- data/lib/everywhere/auth_token.rb +118 -0
- data/lib/everywhere/builders/android.rb +844 -0
- data/lib/everywhere/builders/desktop.rb +326 -0
- data/lib/everywhere/builders/ios.rb +40 -3
- data/lib/everywhere/child_processes.rb +74 -0
- data/lib/everywhere/commands/build.rb +95 -14
- data/lib/everywhere/commands/clean.rb +10 -5
- data/lib/everywhere/commands/dev.rb +462 -57
- data/lib/everywhere/commands/doctor.rb +89 -3
- data/lib/everywhere/commands/install.rb +21 -1
- data/lib/everywhere/commands/logs.rb +117 -8
- data/lib/everywhere/commands/release.rb +2 -2
- data/lib/everywhere/config.rb +703 -13
- data/lib/everywhere/console.rb +117 -0
- data/lib/everywhere/desktop_assets.rb +150 -0
- data/lib/everywhere/dock/footer.rb +150 -0
- data/lib/everywhere/dock/screen.rb +114 -0
- data/lib/everywhere/dock/state.rb +59 -0
- data/lib/everywhere/dock.rb +238 -0
- data/lib/everywhere/emulator.rb +405 -0
- data/lib/everywhere/engine.rb +12 -0
- data/lib/everywhere/fatal.rb +20 -0
- data/lib/everywhere/icon.rb +196 -0
- data/lib/everywhere/line_pump.rb +89 -0
- data/lib/everywhere/log_filter.rb +111 -0
- data/lib/everywhere/native_helper.rb +39 -9
- data/lib/everywhere/omniauth.rb +88 -0
- data/lib/everywhere/paths.rb +109 -11
- data/lib/everywhere/relay.rb +77 -0
- data/lib/everywhere/shellout.rb +90 -15
- data/lib/everywhere/task_pool.rb +123 -0
- data/lib/everywhere/ui.rb +54 -11
- data/lib/everywhere/version.rb +1 -1
- data/support/desktop/README.md +121 -0
- data/support/{shell → desktop}/src-tauri/Cargo.lock +23 -0
- data/support/{shell → desktop}/src-tauri/Cargo.toml +19 -1
- data/support/{shell → desktop}/src-tauri/capabilities/default.json +7 -0
- data/support/desktop/src-tauri/gen/schemas/capabilities.json +1 -0
- data/support/desktop/src-tauri/src/extension_host.rs +53 -0
- data/support/desktop/src-tauri/src/extensions/mod.rs +39 -0
- data/support/{shell → desktop}/src-tauri/src/main.rs +355 -10
- data/support/{shell → desktop}/src-tauri/tauri.conf.json +2 -2
- data/support/mobile/android/README.md +217 -0
- data/support/mobile/android/app/build.gradle.kts +156 -0
- data/support/mobile/android/app/everywhere.properties +8 -0
- data/support/mobile/android/app/native-packages.gradle.kts +16 -0
- data/support/mobile/android/app/proguard-rules.pro +21 -0
- data/support/mobile/android/app/src/debug/res/xml/network_security_config.xml +30 -0
- data/support/mobile/android/app/src/main/AndroidManifest.xml +42 -0
- data/support/mobile/android/app/src/main/assets/everywhere.json +9 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.codepoints +2235 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.ttf +0 -0
- data/support/mobile/android/app/src/main/assets/json/path-configuration.json +19 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/AuthFlow.kt +268 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereApplication.kt +178 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereConfig.kt +409 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereEvents.kt +68 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereHost.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/IconFont.kt +211 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainActivity.kt +975 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainTabs.kt +201 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebBottomSheetFragment.kt +13 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebFragment.kt +36 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/BiometricsComponent.kt +535 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/HapticsComponent.kt +141 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/MenuComponent.kt +361 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/NotificationComponent.kt +151 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/PermissionsComponent.kt +271 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/StorageComponent.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/WebControlChannel.kt +204 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/extensions/EverywhereExtensions.kt +51 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_active_indicator.xml +4 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_item.xml +11 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +26 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_tab_placeholder.xml +28 -0
- data/support/mobile/android/app/src/main/res/layout/activity_main.xml +116 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet.xml +83 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet.xml +48 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +12 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- data/support/mobile/android/app/src/main/res/values/colors.xml +21 -0
- data/support/mobile/android/app/src/main/res/values/ic_launcher_background.xml +5 -0
- data/support/mobile/android/app/src/main/res/values/strings.xml +18 -0
- data/support/mobile/android/app/src/main/res/values/styles.xml +32 -0
- data/support/mobile/android/app/src/main/res/values/themes.xml +53 -0
- data/support/mobile/android/app/src/main/res/values-night/colors.xml +11 -0
- data/support/mobile/android/app/src/main/res/values-night/themes.xml +33 -0
- data/support/mobile/android/app/src/main/res/xml/network_security_config.xml +12 -0
- data/support/mobile/android/app/src/stamped/AndroidManifest.xml +34 -0
- data/support/mobile/android/build.gradle.kts +8 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.properties +10 -0
- data/support/mobile/android/gradle.properties +14 -0
- data/support/mobile/android/gradlew +251 -0
- data/support/mobile/android/gradlew.bat +94 -0
- data/support/mobile/android/settings.gradle.kts +28 -0
- data/support/mobile/ios/App/AppDelegate.swift +5 -0
- data/support/mobile/ios/App/AuthFlow.swift +101 -0
- data/support/mobile/ios/App/EverywhereConfig.swift +84 -0
- data/support/mobile/ios/App/SceneDelegate.swift +103 -0
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +4 -0
- data/support/mobile/ios/README.md +4 -1
- data/support/{macos → release/macos}/notarize.sh +2 -2
- metadata +97 -17
- data/support/github/build.yml +0 -85
- data/support/shell/src-tauri/gen/schemas/capabilities.json +0 -1
- /data/support/{shell → desktop}/splash/index.html +0 -0
- /data/support/{shell → desktop}/src-tauri/build.rs +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/acl-manifests.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/desktop-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/macOS-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/icons/icon.png +0 -0
- /data/support/{shell → desktop}/src-tauri/src/updater.rs +0 -0
- /data/support/{macos → release/macos}/entitlements.plist +0 -0
|
@@ -3,10 +3,22 @@
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require "socket"
|
|
5
5
|
require "shellwords"
|
|
6
|
+
require "fileutils"
|
|
6
7
|
require_relative "../shellout"
|
|
7
8
|
require_relative "../paths"
|
|
9
|
+
require_relative "../icon"
|
|
10
|
+
require_relative "../desktop_assets"
|
|
8
11
|
require_relative "../builders/ios"
|
|
12
|
+
require_relative "../builders/android"
|
|
13
|
+
require_relative "../builders/desktop"
|
|
9
14
|
require_relative "../simulator"
|
|
15
|
+
require_relative "../emulator"
|
|
16
|
+
require_relative "../console"
|
|
17
|
+
require_relative "../child_processes"
|
|
18
|
+
require_relative "../task_pool"
|
|
19
|
+
require_relative "../relay"
|
|
20
|
+
require_relative "../dock"
|
|
21
|
+
require_relative "../log_filter"
|
|
10
22
|
require_relative "logs"
|
|
11
23
|
|
|
12
24
|
module Everywhere
|
|
@@ -18,23 +30,49 @@ module Everywhere
|
|
|
18
30
|
#
|
|
19
31
|
# Target flags are additive (--desktop --ios --browser opens all three);
|
|
20
32
|
# with no flags the server boots and a key menu launches shells on demand.
|
|
33
|
+
#
|
|
34
|
+
# Everything runs concurrently and nothing a shell does can end the session:
|
|
35
|
+
# each build owns a TaskPool thread that catches its own failures, each
|
|
36
|
+
# child's output is relayed through Console tagged with its source, and a
|
|
37
|
+
# Dock keeps the keymap and every target's state pinned to the bottom of the
|
|
38
|
+
# terminal while the logs scroll above it.
|
|
21
39
|
class Dev < Dry::CLI::Command
|
|
22
40
|
desc "Run the dev server with native shells on demand (live reload, no packaging)"
|
|
23
41
|
|
|
42
|
+
IOS_TARGET = "ios-arm64"
|
|
43
|
+
ANDROID_TARGET = "android-arm64"
|
|
44
|
+
|
|
45
|
+
# Key → the target it acts on, for the "press i to try again" hints.
|
|
46
|
+
KEY_FOR = { ios: "i", android: "a", desktop: "d", web: "r" }.freeze
|
|
47
|
+
|
|
24
48
|
option :port, default: "3000", desc: "Dev server port"
|
|
25
49
|
option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
|
|
26
|
-
option :target, default: nil, desc: "Single shell to open: macos (desktop)
|
|
50
|
+
option :target, default: nil, desc: "Single shell to open: macos (desktop), ios (Simulator) or android (emulator)"
|
|
27
51
|
option :desktop, type: :boolean, default: false, desc: "Open the desktop shell"
|
|
28
52
|
option :ios, type: :boolean, default: false, desc: "Open the iOS Simulator shell"
|
|
53
|
+
option :android, type: :boolean, default: false, desc: "Open the Android emulator shell"
|
|
54
|
+
option :mobile, type: :boolean, default: false, desc: "Open both mobile shells (same as --ios --android)"
|
|
29
55
|
option :browser, type: :boolean, default: false, desc: "Open the app in the default browser"
|
|
56
|
+
option :dev_url, default: nil, desc: "Explicit URL for the device to load (e.g. a LAN address for a physical Android device)"
|
|
30
57
|
|
|
31
|
-
def call(port: "3000", shell_dir: nil, target: nil, desktop: false, ios: false,
|
|
58
|
+
def call(port: "3000", shell_dir: nil, target: nil, desktop: false, ios: false, android: false,
|
|
59
|
+
mobile: false, browser: false, dev_url: nil, **)
|
|
32
60
|
@shell_dir = shell_dir
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
@port = port
|
|
62
|
+
@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
|
|
67
|
+
targets = requested_targets(target, desktop: desktop, ios: ios, android: android,
|
|
68
|
+
mobile: mobile, browser: browser)
|
|
69
|
+
@framework, @config = detect_app
|
|
35
70
|
|
|
36
|
-
|
|
37
|
-
@
|
|
71
|
+
@pool = TaskPool.new(on_error: method(:report_failure))
|
|
72
|
+
@dock = Dock.open(dock_targets(targets), keys: $stdin.tty?)
|
|
73
|
+
|
|
74
|
+
start_server(@framework, port)
|
|
75
|
+
@dev_url = if @framework
|
|
38
76
|
"http://127.0.0.1:#{port}#{@config.entry_path}"
|
|
39
77
|
else
|
|
40
78
|
"#{@config.remote_url}#{@config.entry_path}"
|
|
@@ -45,7 +83,7 @@ module Everywhere
|
|
|
45
83
|
targets << "desktop" if targets.empty? && !$stdin.tty?
|
|
46
84
|
targets.each { |t| launch(t) }
|
|
47
85
|
|
|
48
|
-
$stdin.tty? ? interact :
|
|
86
|
+
$stdin.tty? ? interact : run_headless
|
|
49
87
|
rescue Interrupt
|
|
50
88
|
nil
|
|
51
89
|
ensure
|
|
@@ -54,14 +92,28 @@ module Everywhere
|
|
|
54
92
|
|
|
55
93
|
private
|
|
56
94
|
|
|
57
|
-
|
|
95
|
+
# --target names ONE shell; the boolean flags are additive. Desktop is the
|
|
96
|
+
# fallback for any --target that isn't a mobile os, which keeps
|
|
97
|
+
# `--target macos-arm64` (and a bare `--target`) meaning what it always did.
|
|
98
|
+
# --mobile is exactly --ios --android.
|
|
99
|
+
def requested_targets(target, desktop:, ios:, android:, mobile:, browser:)
|
|
100
|
+
target_os = target && Everywhere::Config.os_of(target)
|
|
58
101
|
targets = []
|
|
59
|
-
targets << "desktop" if desktop || (target &&
|
|
60
|
-
targets << "ios" if ios ||
|
|
102
|
+
targets << "desktop" if desktop || (target && !%w[ios android].include?(target_os))
|
|
103
|
+
targets << "ios" if ios || mobile || target_os == "ios"
|
|
104
|
+
targets << "android" if android || mobile || target_os == "android"
|
|
61
105
|
targets << "browser" if browser
|
|
62
106
|
targets
|
|
63
107
|
end
|
|
64
108
|
|
|
109
|
+
# What the dock lists. On a terminal every shell is one keypress away, so
|
|
110
|
+
# show them all and let the idle ones sit dim; headless, only show what was
|
|
111
|
+
# actually asked for.
|
|
112
|
+
def dock_targets(requested)
|
|
113
|
+
shells = $stdin.tty? ? %i[desktop ios android] : requested.map(&:to_sym)
|
|
114
|
+
([@framework ? :web : nil] + shells).compact.uniq
|
|
115
|
+
end
|
|
116
|
+
|
|
65
117
|
# The framework (nil for remote-only roots) and config. A root with
|
|
66
118
|
# neither a framework nor remote.url is a genuine error.
|
|
67
119
|
def detect_app
|
|
@@ -78,6 +130,8 @@ module Everywhere
|
|
|
78
130
|
[framework, config]
|
|
79
131
|
end
|
|
80
132
|
|
|
133
|
+
# --- the dev server -------------------------------------------------------
|
|
134
|
+
|
|
81
135
|
def start_server(framework, port)
|
|
82
136
|
unless framework
|
|
83
137
|
# Remote-mode app with no local code (just everywhere.yml): nothing
|
|
@@ -89,33 +143,220 @@ module Everywhere
|
|
|
89
143
|
if port_open?(port)
|
|
90
144
|
UI.warn "something is ALREADY RUNNING on port #{port} — attaching to it instead of starting"
|
|
91
145
|
UI.warn "#{UI.dim("your Procfile.dev/watchers are NOT running; stop that server or use --port if this isn't yours")}"
|
|
146
|
+
@dock.set(:web, :running, detail: "port #{port} (attached)")
|
|
92
147
|
else
|
|
93
148
|
UI.step("starting #{UI.bold(framework.name)} dev server #{UI.dim("(" + framework.dev_command + ")")}")
|
|
94
|
-
@
|
|
95
|
-
|
|
149
|
+
@dock.set(:web, :booting)
|
|
150
|
+
supervise(:web, { "PORT" => port.to_s }, framework.dev_command, chdir: framework.root)
|
|
96
151
|
wait_for_port(port)
|
|
152
|
+
@dock.set(:web, :running, detail: ":#{port}")
|
|
97
153
|
end
|
|
98
154
|
end
|
|
99
155
|
|
|
156
|
+
# Kill and respawn just the dev server — for a Gemfile or initializer
|
|
157
|
+
# change, where a full restart of the session would mean rebuilding the
|
|
158
|
+
# native shells for nothing.
|
|
159
|
+
def restart_server
|
|
160
|
+
return UI.warn("nothing to restart — this app runs in remote mode") unless @framework
|
|
161
|
+
|
|
162
|
+
@dock.set(:web, :booting, detail: "restarting")
|
|
163
|
+
UI.step("restarting the dev server")
|
|
164
|
+
stop_child(:web)
|
|
165
|
+
start_server(@framework, @port)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# --- shells ---------------------------------------------------------------
|
|
169
|
+
|
|
100
170
|
def launch(target)
|
|
101
171
|
case target
|
|
102
|
-
when "ios" then launch_ios
|
|
172
|
+
when "ios" then background(:ios) { launch_ios }
|
|
173
|
+
when "android" then background(:android) { launch_android }
|
|
103
174
|
when "browser" then launch_browser
|
|
104
|
-
else launch_desktop
|
|
175
|
+
else background(:desktop) { launch_desktop }
|
|
105
176
|
end
|
|
106
177
|
end
|
|
107
178
|
|
|
179
|
+
# A second press while a build is still running is refused, not queued:
|
|
180
|
+
# Builders::Ios rm_rf's and re-copies a shared work dir on every run, so
|
|
181
|
+
# two overlapping builds would corrupt each other.
|
|
182
|
+
def background(name, &block)
|
|
183
|
+
case @pool.start(name, &block)
|
|
184
|
+
when :busy
|
|
185
|
+
UI.warn "#{Dock::State::LABELS[name]} is already working — #{UI.dim("give it a moment")}"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# `d` relaunches, the same way `i` and `a` do — quit the running window and
|
|
190
|
+
# open a fresh one. Refusing with "already running" made the desktop the
|
|
191
|
+
# odd one out, and left no way to restart a shell that had wedged or was
|
|
192
|
+
# holding stale NATIVE_CONFIG.
|
|
193
|
+
#
|
|
194
|
+
# The rebuild is free either way: a warm `cargo run` re-checks the crate
|
|
195
|
+
# 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
|
+
|
|
108
274
|
def launch_desktop
|
|
109
|
-
|
|
110
|
-
|
|
275
|
+
# A bad `window:` or a broken assets/ tree marks the desktop red and
|
|
276
|
+
# leaves the dev server and the mobile shells alone — the same deal
|
|
277
|
+
# every other target gets when its build fails. Dying here would take
|
|
278
|
+
# the whole session down over a misspelled title bar, and you'd fix it
|
|
279
|
+
# and have to start over.
|
|
280
|
+
errors = @config.window_errors
|
|
281
|
+
unless errors.empty?
|
|
282
|
+
errors.each { |error| UI.warn(error) }
|
|
283
|
+
@dock.set(:desktop, :failed, detail: "window: in config/everywhere.yml")
|
|
111
284
|
return
|
|
112
285
|
end
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
286
|
+
|
|
287
|
+
assets_dir, asset_errors = stage_desktop_assets
|
|
288
|
+
unless asset_errors.empty?
|
|
289
|
+
asset_errors.each { |error| UI.warn(error) }
|
|
290
|
+
@dock.set(:desktop, :failed, detail: "native/desktop/assets")
|
|
291
|
+
return
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Stamps the per-app shell when the app declares native.desktop, and
|
|
295
|
+
# hands back the gem's copy when it doesn't. Done before the restart
|
|
296
|
+
# dance so a config error doesn't first kill a working window.
|
|
297
|
+
prepared = Builders::Desktop.new(config: @config, root: @config.root,
|
|
298
|
+
template_dir: @shell_dir).prepare!
|
|
299
|
+
|
|
300
|
+
pid = child_pid(:desktop)
|
|
301
|
+
restarting = pid && ChildProcesses.alive?(pid)
|
|
302
|
+
if restarting
|
|
303
|
+
@dock.set(:desktop, :booting, detail: "restarting")
|
|
304
|
+
UI.step("quitting the running desktop shell")
|
|
305
|
+
stop_child(:desktop, grace: 3)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
UI.step("#{restarting ? "reopening" : "opening"} desktop shell → #{UI.cyan(@dev_url)}")
|
|
309
|
+
# A cold cargo build is minutes long, so the dock says "building" until
|
|
310
|
+
# cargo itself reports it has handed off to the app. There is no other
|
|
311
|
+
# signal that the compile turned into a running window.
|
|
312
|
+
@dock.set(:desktop, :building)
|
|
313
|
+
# CARGO_TERM_COLOR: cargo drops color when its stdout isn't a terminal.
|
|
314
|
+
# It IS one here (we relay over a pty), but the variable costs nothing
|
|
315
|
+
# and keeps the output right if the pty path ever falls back to a pipe.
|
|
316
|
+
# NATIVE_ASSETS_DIR: `cargo run` has no .app around it, so there's no
|
|
317
|
+
# Resources/assets for the shell to find. It gets the staged copy
|
|
318
|
+
# instead, which is re-stamped just above on every launch — so editing a
|
|
319
|
+
# PNG and pressing `d` is enough to see it.
|
|
320
|
+
supervise(:desktop,
|
|
321
|
+
{ "NATIVE_DEV_URL" => @dev_url, "NATIVE_CONFIG" => @config.to_shell_json,
|
|
322
|
+
"NATIVE_ASSETS_DIR" => assets_dir,
|
|
323
|
+
"NATIVE_ICON_PATH" => stage_desktop_icon,
|
|
324
|
+
"CARGO_TARGET_DIR" => prepared.target_dir,
|
|
325
|
+
"CARGO_TERM_COLOR" => "always" },
|
|
326
|
+
desktop_command(prepared), chdir: prepared.dir,
|
|
327
|
+
filter: LogFilter.for(:cargo), log: dist_log("desktop-dev.log"),
|
|
328
|
+
# cargo colors its status lines, so the marker has to be
|
|
329
|
+
# matched past the escape codes. "Finished" is cargo's last
|
|
330
|
+
# word before we hand off to the app; on the `cargo run` path
|
|
331
|
+
# it was "Running".
|
|
332
|
+
on_line: lambda { |line|
|
|
333
|
+
@dock.set(:desktop, :running) if UI.strip_ansi(line).match?(/\A\s*(Running|Finished)\b/)
|
|
334
|
+
})
|
|
335
|
+
end
|
|
336
|
+
|
|
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(" && ")
|
|
119
360
|
end
|
|
120
361
|
|
|
121
362
|
# Build the shell (Debug, warm after the first run), put it on a booted
|
|
@@ -124,14 +365,43 @@ module Everywhere
|
|
|
124
365
|
# covers plain http for local networking. Pressing i again rebuilds and
|
|
125
366
|
# relaunches, so it doubles as a native-shell reload.
|
|
126
367
|
def launch_ios
|
|
127
|
-
|
|
368
|
+
@dock.set(:ios, :building)
|
|
369
|
+
app = Builders::Ios.new(config: @config, root: @config.root, target: IOS_TARGET)
|
|
128
370
|
.build!(dist_dir: File.expand_path("dist", @config.root), dev: true)
|
|
129
371
|
|
|
372
|
+
@dock.set(:ios, :booting, detail: "Simulator")
|
|
130
373
|
udid = Simulator.boot_default!
|
|
374
|
+
@dock.set(:ios, :installing)
|
|
131
375
|
UI.step("installing #{UI.bold(File.basename(app))}")
|
|
132
376
|
Simulator.install(udid, app)
|
|
133
377
|
UI.step("launching → #{UI.cyan(@dev_url)}")
|
|
134
|
-
Simulator.launch(udid, @config.bundle_id(target:
|
|
378
|
+
Simulator.launch(udid, @config.bundle_id(target: IOS_TARGET),
|
|
379
|
+
env: { "EVERYWHERE_DEV_URL" => @dev_url })
|
|
380
|
+
@dock.set(:ios, :running, detail: "Simulator")
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# The Android counterpart of launch_ios: build Debug (warm after the first
|
|
384
|
+
# run), put it on a booted emulator and launch it. Unlike the Simulator,
|
|
385
|
+
# which shares the Mac's loopback, an Android device reaches the host
|
|
386
|
+
# through `adb reverse` — so the URL is resolved per-device rather than
|
|
387
|
+
# reused from @dev_url (see Emulator.dev_url for the fallback ladder).
|
|
388
|
+
# Pressing a again rebuilds and relaunches, so it doubles as a reload.
|
|
389
|
+
def launch_android
|
|
390
|
+
@dock.set(:android, :booting, detail: "emulator")
|
|
391
|
+
serial = Emulator.boot_default!
|
|
392
|
+
url = Emulator.dev_url(@dev_url, serial: serial, port: @port, override: @dev_url_override)
|
|
393
|
+
|
|
394
|
+
@dock.set(:android, :building)
|
|
395
|
+
app = Builders::Android.new(config: @config, root: @config.root, target: ANDROID_TARGET)
|
|
396
|
+
.build!(dist_dir: File.expand_path("dist", @config.root), dev: true, dev_url: url)
|
|
397
|
+
|
|
398
|
+
application_id = @config.bundle_id(target: ANDROID_TARGET)
|
|
399
|
+
@dock.set(:android, :installing)
|
|
400
|
+
UI.step("installing #{UI.bold(File.basename(app))}")
|
|
401
|
+
Emulator.install(serial, app, application_id: application_id)
|
|
402
|
+
UI.step("launching → #{UI.cyan(url)}")
|
|
403
|
+
Emulator.launch(serial, application_id)
|
|
404
|
+
@dock.set(:android, :running, detail: serial)
|
|
135
405
|
end
|
|
136
406
|
|
|
137
407
|
def launch_browser
|
|
@@ -140,12 +410,12 @@ module Everywhere
|
|
|
140
410
|
Shellout.run?(opener, @dev_url, quiet: true)
|
|
141
411
|
end
|
|
142
412
|
|
|
143
|
-
# Toggle the `every logs --ios` stream, interleaved with the
|
|
144
|
-
# output on this same terminal.
|
|
413
|
+
# Toggle the `every logs --ios` stream, tagged and interleaved with the
|
|
414
|
+
# server output on this same terminal.
|
|
145
415
|
def toggle_logs
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
416
|
+
pid = child_pid(:logs)
|
|
417
|
+
if pid && ChildProcesses.alive?(pid)
|
|
418
|
+
stop_child(:logs)
|
|
149
419
|
UI.step("stopped iOS Simulator logs")
|
|
150
420
|
return
|
|
151
421
|
end
|
|
@@ -156,24 +426,98 @@ module Everywhere
|
|
|
156
426
|
return
|
|
157
427
|
end
|
|
158
428
|
UI.step("watching iOS Simulator logs #{UI.dim("(press l again to stop)")}")
|
|
159
|
-
|
|
429
|
+
supervise(:logs, {}, Logs.new.argv(udid).shelljoin, chdir: Dir.pwd)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# --- child supervision ----------------------------------------------------
|
|
433
|
+
|
|
434
|
+
# Spawn a long-lived child on a pty and relay its output, one tagged line
|
|
435
|
+
# at a time. A pty rather than a pipe so foreman, cargo and friends still
|
|
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
|
|
160
454
|
end
|
|
161
455
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
456
|
+
def relay_thread(key, io, pid, filter:, log:, on_line:)
|
|
457
|
+
Thread.new do
|
|
458
|
+
Thread.current.name = "relay-#{key}"
|
|
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
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# TERM the child's whole process group, escalate to KILL, and wait for the
|
|
476
|
+
# relay to drain so its last lines aren't lost. The pid is dropped from
|
|
477
|
+
# the registry FIRST, so the relay knows this death was asked for.
|
|
478
|
+
def stop_child(key, grace: 5)
|
|
479
|
+
pid, thread = @children.synchronize { [@pids.delete(key), @relays.delete(key)] }
|
|
480
|
+
return unless pid
|
|
481
|
+
|
|
482
|
+
reap([pid], grace: grace)
|
|
483
|
+
thread&.join(1)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# TERM each process group, wait for them together rather than serially,
|
|
487
|
+
# then KILL the stragglers.
|
|
488
|
+
def reap(pids, grace: 5)
|
|
489
|
+
live = pids.compact.select { |pid| ChildProcesses.alive?(pid) }
|
|
490
|
+
return if live.empty?
|
|
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") }
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def child_pid(key) = @children.synchronize { @pids[key] }
|
|
499
|
+
|
|
500
|
+
# --- the key menu ---------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
# io/console's getch holds full raw mode while blocked, which clears OPOST
|
|
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
|
|
506
|
+
# while one is going — including `q`.
|
|
166
507
|
def interact
|
|
167
|
-
|
|
168
|
-
"#{UI.bold("l")} iOS logs · #{UI.bold("q")} quit", marker: "⌨", color: :cyan)
|
|
508
|
+
@dock.announce_keys
|
|
169
509
|
saved_tty = `stty -g`.chomp
|
|
170
510
|
system("stty", "-icanon", "-echo")
|
|
511
|
+
trap_exit_signals
|
|
171
512
|
loop do
|
|
172
513
|
case $stdin.getc
|
|
173
|
-
when "d"
|
|
174
|
-
when "i"
|
|
175
|
-
when "
|
|
176
|
-
when "
|
|
514
|
+
when "d" then background(:desktop) { launch_desktop }
|
|
515
|
+
when "i" then background(:ios) { launch_ios }
|
|
516
|
+
when "a" then background(:android) { launch_android }
|
|
517
|
+
when "b" then launch_browser
|
|
518
|
+
when "l" then toggle_logs
|
|
519
|
+
when "r" then background(:web) { restart_server }
|
|
520
|
+
when "?" then @dock.toggle_help
|
|
177
521
|
when "q", "\u0004", nil then break # q, Ctrl-D, EOF (Ctrl-C stays a real SIGINT)
|
|
178
522
|
end
|
|
179
523
|
end
|
|
@@ -181,33 +525,94 @@ module Everywhere
|
|
|
181
525
|
system("stty", saved_tty) unless saved_tty.to_s.empty?
|
|
182
526
|
end
|
|
183
527
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
186
537
|
end
|
|
187
538
|
|
|
188
|
-
|
|
189
|
-
|
|
539
|
+
# --- headless -------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
# No terminal to take keystrokes from, so there's no menu to return to:
|
|
542
|
+
# wait for the builds, and make a failure an actual non-zero exit.
|
|
543
|
+
def run_headless
|
|
544
|
+
@pool.wait_all
|
|
545
|
+
failed = @pool.failed
|
|
546
|
+
UI.die!("#{failed.join(", ")} failed — see the log above") if failed.any?
|
|
547
|
+
|
|
548
|
+
watcher = @children.synchronize { @relays[:web] || @relays[:desktop] }
|
|
549
|
+
return watcher.join if watcher
|
|
550
|
+
|
|
551
|
+
# No pty (or no relay): fall back to waiting on the process itself.
|
|
552
|
+
pid = child_pid(:web) || child_pid(:desktop)
|
|
553
|
+
sleep(1) while pid && ChildProcesses.alive?(pid)
|
|
190
554
|
end
|
|
191
555
|
|
|
192
|
-
|
|
193
|
-
return unless alive?(pid)
|
|
556
|
+
# --- failure --------------------------------------------------------------
|
|
194
557
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
558
|
+
# A build failing is news, not the end of the session. UI.die! has already
|
|
559
|
+
# printed the red ✗ at the point of failure, correctly interleaved with the
|
|
560
|
+
# build's own output, so a Fatal needs no second message — just the dock
|
|
561
|
+
# state and a reminder that everything else is still up.
|
|
562
|
+
def report_failure(name, error, stopping)
|
|
563
|
+
return if stopping
|
|
564
|
+
|
|
565
|
+
@dock.set(name, :failed, detail: failure_detail(name))
|
|
566
|
+
UI.bad("#{Dock::State::LABELS[name]} failed: #{error.message}") unless error.is_a?(Everywhere::Fatal)
|
|
567
|
+
return unless (key = KEY_FOR[name])
|
|
568
|
+
|
|
569
|
+
UI.note("everything else is still running — press #{UI.bold(key)} to try again",
|
|
570
|
+
marker: "⌨", color: :cyan)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# nil for targets with no build log of their own — the dock's ✗ already
|
|
574
|
+
# says "failed", so repeating the word adds nothing.
|
|
575
|
+
def failure_detail(name)
|
|
576
|
+
log = { ios: "dist/ios-build.log", android: "dist/android-build.log" }[name]
|
|
577
|
+
"see #{log}" if log
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# --- teardown -------------------------------------------------------------
|
|
581
|
+
|
|
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
|
+
def teardown
|
|
586
|
+
@stopping = true
|
|
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}")
|
|
199
601
|
end
|
|
200
602
|
end
|
|
201
603
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
604
|
+
# --- helpers --------------------------------------------------------------
|
|
605
|
+
|
|
606
|
+
def dist_log(name)
|
|
607
|
+
dir = File.expand_path("dist", @config.root)
|
|
608
|
+
FileUtils.mkdir_p(dir)
|
|
609
|
+
File.join(dir, name)
|
|
610
|
+
rescue StandardError
|
|
611
|
+
nil
|
|
209
612
|
end
|
|
210
613
|
|
|
614
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
615
|
+
|
|
211
616
|
def port_open?(port)
|
|
212
617
|
TCPSocket.new("127.0.0.1", Integer(port)).close
|
|
213
618
|
true
|