ruby_everywhere 0.1.14 → 0.2.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/bridge/README.md +57 -2
- data/bridge/everywhere/bridge.js +803 -9
- data/bridge/package.json +1 -1
- data/lib/everywhere/builders/ios.rb +364 -0
- data/lib/everywhere/cli.rb +2 -0
- data/lib/everywhere/commands/build.rb +17 -1
- data/lib/everywhere/commands/clean.rb +19 -10
- data/lib/everywhere/commands/dev.rb +182 -19
- data/lib/everywhere/commands/doctor.rb +45 -3
- data/lib/everywhere/commands/icon.rb +14 -0
- data/lib/everywhere/commands/install.rb +37 -6
- data/lib/everywhere/commands/logs.rb +56 -0
- data/lib/everywhere/commands/platform/build.rb +74 -15
- data/lib/everywhere/commands/platform/runner.rb +10 -1
- data/lib/everywhere/commands/release.rb +1 -1
- data/lib/everywhere/commands/shell_dir.rb +11 -4
- data/lib/everywhere/config.rb +366 -1
- data/lib/everywhere/engine.rb +33 -1
- data/lib/everywhere/icon.rb +50 -0
- data/lib/everywhere/log_filter.rb +28 -2
- data/lib/everywhere/mobile_config_endpoint.rb +75 -0
- data/lib/everywhere/mobile_configs_controller.rb +46 -0
- data/lib/everywhere/native_helper.rb +156 -0
- data/lib/everywhere/paths.rb +41 -0
- data/lib/everywhere/raster.rb +17 -0
- data/lib/everywhere/shellout.rb +3 -1
- data/lib/everywhere/simulator.rb +74 -0
- data/lib/everywhere/ui.rb +82 -0
- data/lib/everywhere/version.rb +1 -1
- data/support/mobile/ios/App/App.xcconfig +6 -0
- data/support/mobile/ios/App/AppDelegate.swift +163 -0
- data/support/mobile/ios/App/Assets.xcassets/AccentColor.colorset/Contents.json +20 -0
- data/support/mobile/ios/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
- data/support/mobile/ios/App/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- data/support/mobile/ios/App/Assets.xcassets/Contents.json +6 -0
- data/support/mobile/ios/App/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
- data/support/mobile/ios/App/Base.lproj/LaunchScreen.storyboard +32 -0
- data/support/mobile/ios/App/Bridge/BiometricsComponent.swift +276 -0
- data/support/mobile/ios/App/Bridge/HapticsComponent.swift +47 -0
- data/support/mobile/ios/App/Bridge/NotificationComponent.swift +56 -0
- data/support/mobile/ios/App/Bridge/PermissionsComponent.swift +142 -0
- data/support/mobile/ios/App/Bridge/StorageComponent.swift +63 -0
- data/support/mobile/ios/App/ErrorViewController.swift +64 -0
- data/support/mobile/ios/App/EverywhereConfig.swift +268 -0
- data/support/mobile/ios/App/EverywhereHost.swift +34 -0
- data/support/mobile/ios/App/Extensions/EverywhereExtensions.swift +32 -0
- data/support/mobile/ios/App/Info.plist +28 -0
- data/support/mobile/ios/App/Resources/everywhere.json +8 -0
- data/support/mobile/ios/App/Resources/path-configuration.json +19 -0
- data/support/mobile/ios/App/SceneDelegate.swift +443 -0
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +454 -0
- data/support/mobile/ios/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +14 -0
- data/support/mobile/ios/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme +77 -0
- data/support/mobile/ios/NativeExtensions/Package.swift +26 -0
- data/support/mobile/ios/NativeExtensions/Sources/NativeExtensions/Exports.swift +5 -0
- data/support/mobile/ios/README.md +66 -0
- data/support/shell/src-tauri/Cargo.lock +16 -0
- data/support/shell/src-tauri/Cargo.toml +1 -0
- data/support/shell/src-tauri/gen/schemas/acl-manifests.json +1 -1
- data/support/shell/src-tauri/gen/schemas/desktop-schema.json +42 -0
- data/support/shell/src-tauri/gen/schemas/macOS-schema.json +42 -0
- data/support/shell/src-tauri/src/main.rs +17 -0
- metadata +35 -1
|
@@ -2,48 +2,211 @@
|
|
|
2
2
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require "socket"
|
|
5
|
+
require "shellwords"
|
|
5
6
|
require_relative "../shellout"
|
|
6
7
|
require_relative "../paths"
|
|
8
|
+
require_relative "../builders/ios"
|
|
9
|
+
require_relative "../simulator"
|
|
10
|
+
require_relative "logs"
|
|
7
11
|
|
|
8
12
|
module Everywhere
|
|
9
13
|
module Commands
|
|
10
|
-
# Run the app's normal dev server and open
|
|
11
|
-
# No packaging: the shell skips the sidecar when NATIVE_DEV_URL is
|
|
12
|
-
#
|
|
14
|
+
# Run the app's normal dev server and open native shells pointed at it.
|
|
15
|
+
# No packaging: the desktop shell skips the sidecar when NATIVE_DEV_URL is
|
|
16
|
+
# set, and the iOS shell reads EVERYWHERE_DEV_URL at launch — so the
|
|
17
|
+
# edit-refresh loop is untouched Rails/Hanami either way.
|
|
18
|
+
#
|
|
19
|
+
# Target flags are additive (--desktop --ios --browser opens all three);
|
|
20
|
+
# with no flags the server boots and a key menu launches shells on demand.
|
|
13
21
|
class Dev < Dry::CLI::Command
|
|
14
|
-
desc "Run the dev server
|
|
22
|
+
desc "Run the dev server with native shells on demand (live reload, no packaging)"
|
|
15
23
|
|
|
16
24
|
option :port, default: "3000", desc: "Dev server port"
|
|
17
25
|
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) or ios (Simulator)"
|
|
27
|
+
option :desktop, type: :boolean, default: false, desc: "Open the desktop shell"
|
|
28
|
+
option :ios, type: :boolean, default: false, desc: "Open the iOS Simulator shell"
|
|
29
|
+
option :browser, type: :boolean, default: false, desc: "Open the app in the default browser"
|
|
18
30
|
|
|
19
|
-
def call(port: "3000", shell_dir: nil, **)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
def call(port: "3000", shell_dir: nil, target: nil, desktop: false, ios: false, browser: false, **)
|
|
32
|
+
@shell_dir = shell_dir
|
|
33
|
+
targets = requested_targets(target, desktop: desktop, ios: ios, browser: browser)
|
|
34
|
+
framework, @config = detect_app
|
|
35
|
+
|
|
36
|
+
start_server(framework, port)
|
|
37
|
+
@dev_url = if framework
|
|
38
|
+
"http://127.0.0.1:#{port}#{@config.entry_path}"
|
|
39
|
+
else
|
|
40
|
+
"#{@config.remote_url}#{@config.entry_path}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Headless (non-tty) runs can't take keystrokes, so keep the old
|
|
44
|
+
# default of the desktop shell.
|
|
45
|
+
targets << "desktop" if targets.empty? && !$stdin.tty?
|
|
46
|
+
targets.each { |t| launch(t) }
|
|
47
|
+
|
|
48
|
+
$stdin.tty? ? interact : wait_for_children
|
|
49
|
+
rescue Interrupt
|
|
50
|
+
nil
|
|
51
|
+
ensure
|
|
52
|
+
teardown
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def requested_targets(target, desktop:, ios:, browser:)
|
|
58
|
+
targets = []
|
|
59
|
+
targets << "desktop" if desktop || (target && Everywhere::Config.os_of(target) != "ios")
|
|
60
|
+
targets << "ios" if ios || (target && Everywhere::Config.os_of(target) == "ios")
|
|
61
|
+
targets << "browser" if browser
|
|
62
|
+
targets
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The framework (nil for remote-only roots) and config. A root with
|
|
66
|
+
# neither a framework nor remote.url is a genuine error.
|
|
67
|
+
def detect_app
|
|
68
|
+
framework = begin
|
|
69
|
+
Framework.detect
|
|
70
|
+
rescue Everywhere::Error
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
config = Everywhere::Config.load(framework ? framework.root : Dir.pwd)
|
|
74
|
+
unless framework || config.remote?
|
|
75
|
+
UI.die!("no supported framework here (Rails, Hanami, Sinatra) and no remote.url in " \
|
|
76
|
+
"config/everywhere.yml — nothing to run")
|
|
77
|
+
end
|
|
78
|
+
[framework, config]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def start_server(framework, port)
|
|
82
|
+
unless framework
|
|
83
|
+
# Remote-mode app with no local code (just everywhere.yml): nothing
|
|
84
|
+
# to serve, point the shells straight at the deployed app.
|
|
85
|
+
UI.step("#{UI.bold("remote")} mode → #{UI.cyan(@config.remote_url)} #{UI.dim("(no local server)")}")
|
|
86
|
+
return
|
|
87
|
+
end
|
|
23
88
|
|
|
24
|
-
server_pid = nil
|
|
25
89
|
if port_open?(port)
|
|
26
90
|
UI.warn "something is ALREADY RUNNING on port #{port} — attaching to it instead of starting"
|
|
27
91
|
UI.warn "#{UI.dim("your Procfile.dev/watchers are NOT running; stop that server or use --port if this isn't yours")}"
|
|
28
92
|
else
|
|
29
93
|
UI.step("starting #{UI.bold(framework.name)} dev server #{UI.dim("(" + framework.dev_command + ")")}")
|
|
30
|
-
server_pid = Shellout.spawn({ "PORT" => port.to_s }, framework.dev_command, chdir: framework.root)
|
|
94
|
+
@server_pid = Shellout.spawn({ "PORT" => port.to_s }, framework.dev_command, chdir: framework.root)
|
|
95
|
+
@server_thread = Process.detach(@server_pid)
|
|
31
96
|
wait_for_port(port)
|
|
32
97
|
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def launch(target)
|
|
101
|
+
case target
|
|
102
|
+
when "ios" then launch_ios
|
|
103
|
+
when "browser" then launch_browser
|
|
104
|
+
else launch_desktop
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def launch_desktop
|
|
109
|
+
if @desktop_pid && alive?(@desktop_pid)
|
|
110
|
+
UI.warn "desktop shell already running"
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
dir = @shell_dir || Everywhere::Paths.shell_dir!
|
|
114
|
+
UI.step("opening desktop shell → #{UI.cyan(@dev_url)}")
|
|
115
|
+
@desktop_pid = Shellout.spawn({ "NATIVE_DEV_URL" => @dev_url, "NATIVE_CONFIG" => @config.to_shell_json,
|
|
116
|
+
"CARGO_TARGET_DIR" => Everywhere::Paths.cargo_target_dir },
|
|
117
|
+
"cargo run", chdir: dir)
|
|
118
|
+
@desktop_thread = Process.detach(@desktop_pid)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Build the shell (Debug, warm after the first run), put it on a booted
|
|
122
|
+
# Simulator and hand it the dev URL. The Simulator shares the Mac's
|
|
123
|
+
# loopback, so 127.0.0.1 works as-is; the template's ATS exception
|
|
124
|
+
# covers plain http for local networking. Pressing i again rebuilds and
|
|
125
|
+
# relaunches, so it doubles as a native-shell reload.
|
|
126
|
+
def launch_ios
|
|
127
|
+
app = Builders::Ios.new(config: @config, root: @config.root, target: "ios-arm64")
|
|
128
|
+
.build!(dist_dir: File.expand_path("dist", @config.root), dev: true)
|
|
33
129
|
|
|
34
|
-
|
|
35
|
-
UI.step("
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
130
|
+
udid = Simulator.boot_default!
|
|
131
|
+
UI.step("installing #{UI.bold(File.basename(app))}")
|
|
132
|
+
Simulator.install(udid, app)
|
|
133
|
+
UI.step("launching → #{UI.cyan(@dev_url)}")
|
|
134
|
+
Simulator.launch(udid, @config.bundle_id(target: "ios-arm64"), env: { "EVERYWHERE_DEV_URL" => @dev_url })
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def launch_browser
|
|
138
|
+
UI.step("opening browser → #{UI.cyan(@dev_url)}")
|
|
139
|
+
opener = RUBY_PLATFORM.include?("darwin") ? "open" : "xdg-open"
|
|
140
|
+
Shellout.run?(opener, @dev_url, quiet: true)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Toggle the `every logs --ios` stream, interleaved with the server
|
|
144
|
+
# output on this same terminal.
|
|
145
|
+
def toggle_logs
|
|
146
|
+
if @logs_pid && alive?(@logs_pid)
|
|
147
|
+
stop(@logs_pid)
|
|
148
|
+
@logs_pid = nil
|
|
149
|
+
UI.step("stopped iOS Simulator logs")
|
|
150
|
+
return
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
udid = Simulator.booted_udid
|
|
154
|
+
unless udid
|
|
155
|
+
UI.warn "no booted iOS Simulator — press #{UI.bold("i")} first"
|
|
156
|
+
return
|
|
157
|
+
end
|
|
158
|
+
UI.step("watching iOS Simulator logs #{UI.dim("(press l again to stop)")}")
|
|
159
|
+
@logs_pid = Shellout.spawn({}, Logs.new.argv(udid).shelljoin, chdir: Dir.pwd)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Key menu. io/console's getch holds full raw mode while blocked, which
|
|
163
|
+
# clears OPOST and stair-steps every log line the server prints; stty
|
|
164
|
+
# -icanon -echo disables only line buffering and echo, leaving output
|
|
165
|
+
# processing (and Ctrl-C as SIGINT) intact.
|
|
166
|
+
def interact
|
|
167
|
+
UI.note("press #{UI.bold("d")} desktop · #{UI.bold("i")} iOS Simulator · #{UI.bold("b")} browser · " \
|
|
168
|
+
"#{UI.bold("l")} iOS logs · #{UI.bold("q")} quit", marker: "⌨", color: :cyan)
|
|
169
|
+
saved_tty = `stty -g`.chomp
|
|
170
|
+
system("stty", "-icanon", "-echo")
|
|
171
|
+
loop do
|
|
172
|
+
case $stdin.getc
|
|
173
|
+
when "d" then launch("desktop")
|
|
174
|
+
when "i" then launch("ios")
|
|
175
|
+
when "b" then launch("browser")
|
|
176
|
+
when "l" then toggle_logs
|
|
177
|
+
when "q", "\u0004", nil then break # q, Ctrl-D, EOF (Ctrl-C stays a real SIGINT)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
39
180
|
ensure
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
181
|
+
system("stty", saved_tty) unless saved_tty.to_s.empty?
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def wait_for_children
|
|
185
|
+
(@server_thread || @desktop_thread)&.join
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def teardown
|
|
189
|
+
[@server_pid, @desktop_pid, @logs_pid].compact.each { |pid| stop(pid) }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def stop(pid)
|
|
193
|
+
return unless alive?(pid)
|
|
194
|
+
|
|
195
|
+
begin
|
|
196
|
+
Process.kill("TERM", -Process.getpgid(pid))
|
|
197
|
+
rescue StandardError
|
|
198
|
+
Process.kill("TERM", pid) rescue nil
|
|
43
199
|
end
|
|
44
200
|
end
|
|
45
201
|
|
|
46
|
-
|
|
202
|
+
def alive?(pid)
|
|
203
|
+
Process.kill(0, pid)
|
|
204
|
+
true
|
|
205
|
+
rescue Errno::ESRCH
|
|
206
|
+
false
|
|
207
|
+
rescue Errno::EPERM
|
|
208
|
+
true
|
|
209
|
+
end
|
|
47
210
|
|
|
48
211
|
def port_open?(port)
|
|
49
212
|
TCPSocket.new("127.0.0.1", Integer(port)).close
|
|
@@ -2,19 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require_relative "../shellout"
|
|
5
|
+
require_relative "../paths"
|
|
5
6
|
|
|
6
7
|
module Everywhere
|
|
7
8
|
module Commands
|
|
8
|
-
# Check that this machine can build
|
|
9
|
+
# Check that this machine can build native apps. Desktop checks always
|
|
10
|
+
# run; the iOS section joins in when the app's build.targets include an
|
|
11
|
+
# ios target, or explicitly via --target ios-*.
|
|
9
12
|
class Doctor < Dry::CLI::Command
|
|
10
|
-
desc "Check the toolchain (rbe-tebako, rust, brew deps)"
|
|
13
|
+
desc "Check the toolchain (rbe-tebako, rust, brew deps; Xcode for iOS targets)"
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
option :root, default: ".", desc: "App root (its build.targets pick the sections)"
|
|
16
|
+
option :target, default: nil, desc: "Also check a specific target's toolchain (e.g. ios-arm64)"
|
|
17
|
+
|
|
18
|
+
def call(root: ".", target: nil, **)
|
|
13
19
|
ok = true
|
|
14
20
|
ok &= check("rbe-tebako installed") { Shellout.run?("rbe-tebako help > /dev/null 2>&1") }
|
|
15
21
|
ok &= check("cargo installed") { Shellout.run?("cargo --version > /dev/null 2>&1") }
|
|
16
22
|
ok &= check("homebrew bison") { !`brew --prefix bison 2>/dev/null`.strip.empty? }
|
|
17
23
|
|
|
24
|
+
ok &= ios_checks if ios_wanted?(root, target)
|
|
25
|
+
|
|
18
26
|
puts
|
|
19
27
|
ok ? UI.success("ready to build") : UI.bad("fix the items above (see https://rubyeverywhere.com/docs/diy/requirements)")
|
|
20
28
|
exit(1) unless ok
|
|
@@ -22,6 +30,40 @@ module Everywhere
|
|
|
22
30
|
|
|
23
31
|
private
|
|
24
32
|
|
|
33
|
+
def ios_wanted?(root, target)
|
|
34
|
+
return true if target && Everywhere::Config.os_of(target) == "ios"
|
|
35
|
+
|
|
36
|
+
Everywhere::Config.load(File.expand_path(root)).targets
|
|
37
|
+
.any? { |t| Everywhere::Config.os_of(t) == "ios" }
|
|
38
|
+
rescue StandardError
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ios_checks
|
|
43
|
+
puts
|
|
44
|
+
UI.step("iOS target")
|
|
45
|
+
ok = check("full Xcode selected") { Shellout.run?("xcodebuild -version > /dev/null 2>&1") }
|
|
46
|
+
ok &= check("iOS Simulator SDK") do
|
|
47
|
+
Shellout.run?("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path", quiet: true)
|
|
48
|
+
end
|
|
49
|
+
# A runtime matching the SDK's iOS version specifically: a half-installed
|
|
50
|
+
# platform (download interrupted mid-registration) leaves older runtimes
|
|
51
|
+
# in place and fails builds with "No simulator runtime version … available"
|
|
52
|
+
# / "Platform Not Installed". `xcodebuild -downloadPlatform iOS` fixes it.
|
|
53
|
+
ok &= check("Simulator runtime matches the iOS SDK (xcodebuild -downloadPlatform iOS)") do
|
|
54
|
+
sdk, = Shellout.capture("xcrun", "--sdk", "iphonesimulator", "--show-sdk-version")
|
|
55
|
+
version = sdk.strip[/\A\d+\.\d+/]
|
|
56
|
+
# `runtime list` shows the disk images; `list runtimes` can lag on
|
|
57
|
+
# lazily-mounted ones, so accept a Ready image OR a registered runtime.
|
|
58
|
+
images, = Shellout.capture("xcrun", "simctl", "runtime", "list")
|
|
59
|
+
runtimes, = Shellout.capture("xcrun", "simctl", "list", "runtimes")
|
|
60
|
+
!version.nil? && (images.match?(/iOS #{Regexp.escape(version)}.*\(Ready\)/) ||
|
|
61
|
+
runtimes.include?("iOS #{version}"))
|
|
62
|
+
end
|
|
63
|
+
ok &= check("bundled iOS shell template") { !Everywhere::Paths.ios_dir.nil? }
|
|
64
|
+
ok
|
|
65
|
+
end
|
|
66
|
+
|
|
25
67
|
def check(label)
|
|
26
68
|
passed = yield
|
|
27
69
|
passed ? UI.ok(label) : UI.bad(label)
|
|
@@ -36,6 +36,11 @@ module Everywhere
|
|
|
36
36
|
Everywhere::Icon.write_icns(master, File.join(out, "AppIcon.icns"))
|
|
37
37
|
UI.ok("macOS icon AppIcon.icns")
|
|
38
38
|
|
|
39
|
+
background = ios_background(root_path)
|
|
40
|
+
Everywhere::Icon.write_ios_appiconset(source, File.join(out, "ios", "AppIcon.appiconset"),
|
|
41
|
+
background: background)
|
|
42
|
+
UI.ok("iOS icon ios/AppIcon.appiconset #{UI.dim("(full-bleed 1024px, alpha flattened)")}")
|
|
43
|
+
|
|
39
44
|
Everywhere::Icon.write_ico(source, File.join(out, "icon.ico"))
|
|
40
45
|
UI.ok("Windows icon icon.ico")
|
|
41
46
|
|
|
@@ -57,6 +62,15 @@ module Everywhere
|
|
|
57
62
|
default = File.join(root_path, "icon.png")
|
|
58
63
|
File.exist?(default) ? default : nil
|
|
59
64
|
end
|
|
65
|
+
|
|
66
|
+
# The iOS icon's flatten backdrop: the app's light background color when
|
|
67
|
+
# set, else Icon's white default.
|
|
68
|
+
def ios_background(root_path)
|
|
69
|
+
color = Everywhere::Config.load(root_path).background_color
|
|
70
|
+
Everywhere::Icon.hex_rgb(color && color["light"])
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
60
74
|
end
|
|
61
75
|
end
|
|
62
76
|
end
|
|
@@ -66,13 +66,15 @@ module Everywhere
|
|
|
66
66
|
# Rack frameworks vary too much to auto-edit their boot files safely, so we
|
|
67
67
|
# vendor the bridge and print the remaining wiring rather than guess.
|
|
68
68
|
def vendor_bridge_to_public
|
|
69
|
-
source = File.expand_path("../../../bridge/everywhere/bridge.js", __dir__)
|
|
70
69
|
target_dir = app_file("public")
|
|
71
70
|
FileUtils.mkdir_p(target_dir)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
%w[bridge.js native.css].each do |name|
|
|
72
|
+
source = File.expand_path("../../../bridge/everywhere/#{name}", __dir__)
|
|
73
|
+
target = File.join(target_dir, name)
|
|
74
|
+
fresh = !File.exist?(target) || File.read(target) != File.read(source)
|
|
75
|
+
FileUtils.cp(source, target) if fresh
|
|
76
|
+
change("vendor @rubyeverywhere/bridge to public/#{name}", fresh)
|
|
77
|
+
end
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
def guide_rack_manual_steps(db:, boot:, script:)
|
|
@@ -130,6 +132,13 @@ module Everywhere
|
|
|
130
132
|
light: "#FFFFFF"
|
|
131
133
|
dark: "#1C1B1A"
|
|
132
134
|
|
|
135
|
+
# Native permissions the app may request — only declared ones can prompt.
|
|
136
|
+
# camera/location values are the sentence iOS shows when asking (mandatory).
|
|
137
|
+
# permissions:
|
|
138
|
+
# notifications: true
|
|
139
|
+
# camera: "Scan QR codes to pair devices."
|
|
140
|
+
# location: "Find things near you."
|
|
141
|
+
|
|
133
142
|
# Native menu-bar items — each one visits a route in your app (no native code).
|
|
134
143
|
# menu:
|
|
135
144
|
# - label: Settings…
|
|
@@ -149,12 +158,34 @@ module Everywhere
|
|
|
149
158
|
# - label: Quit
|
|
150
159
|
# action: quit
|
|
151
160
|
|
|
161
|
+
# Mobile tab bar (iOS today, Android soon). Served live from
|
|
162
|
+
# /everywhere/ios_v1.json too, so tab changes deploy with the app —
|
|
163
|
+
# no app-store release. Icons: ios = SF Symbol name.
|
|
164
|
+
# tabs:
|
|
165
|
+
# - name: Home
|
|
166
|
+
# path: /
|
|
167
|
+
# icons:
|
|
168
|
+
# ios: house
|
|
169
|
+
# - name: Settings
|
|
170
|
+
# path: /settings
|
|
171
|
+
# icons:
|
|
172
|
+
# ios: gear
|
|
173
|
+
|
|
174
|
+
# iOS-specific knobs (and native Swift you compile into the shell —
|
|
175
|
+
# see /docs/diy/native-extensions).
|
|
176
|
+
# native:
|
|
177
|
+
# ios:
|
|
178
|
+
# lazy_load_tabs: true # defer each tab's first load until it's tapped
|
|
179
|
+
# # (false = load every tab up front; omit = default)
|
|
180
|
+
|
|
152
181
|
# Durable build knobs (CLI flags still override per-run).
|
|
153
182
|
# build:
|
|
154
183
|
# ruby: "4.0.6"
|
|
155
184
|
# targets: # the platforms you ship — the CI matrix, expressed once
|
|
156
185
|
# - macos-arm64
|
|
157
|
-
# # - ios-arm64 #
|
|
186
|
+
# # - ios-arm64 # Hotwire Native shell; remote mode only. Local builds
|
|
187
|
+
# # # (`every build` / `every dev --ios`) work today;
|
|
188
|
+
# # # hosted platform builds for iOS aren't live yet.
|
|
158
189
|
# # - android-arm64 # future — mobile is remote mode only
|
|
159
190
|
# permissions: [notifications, filesystem:read, clipboard]
|
|
160
191
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require_relative "../simulator"
|
|
5
|
+
require_relative "../ui"
|
|
6
|
+
|
|
7
|
+
module Everywhere
|
|
8
|
+
module Commands
|
|
9
|
+
# Tail the native shell's logs. iOS only so far: the Simulator's unified
|
|
10
|
+
# log, scoped to the shell process. Watches (log stream) by default;
|
|
11
|
+
# --last replays recent history (log show) and exits.
|
|
12
|
+
class Logs < Dry::CLI::Command
|
|
13
|
+
desc "Stream native shell logs (iOS Simulator; watches by default)"
|
|
14
|
+
|
|
15
|
+
option :ios, type: :boolean, default: false, desc: "iOS Simulator logs (the only target so far)"
|
|
16
|
+
option :last, default: nil, desc: 'Show recent history instead of watching (e.g. "5m", "1h")'
|
|
17
|
+
option :udid, default: nil, desc: "Simulator UDID (default: the booted one)"
|
|
18
|
+
option :verbose, type: :boolean, default: false, desc: "Everything the shell process logs, debug level included"
|
|
19
|
+
|
|
20
|
+
# The template contract fixes the product name (App.app), so the shell's
|
|
21
|
+
# process on the Simulator is always "App" — one predicate covers every
|
|
22
|
+
# Everywhere app without knowing which one is installed. Scoping to the
|
|
23
|
+
# process alone still yields ~100k lines/half hour of Apple-framework
|
|
24
|
+
# chatter (WebKit resource loads, activities); the default predicate
|
|
25
|
+
# keeps what a shell developer logs — NSLog (Foundation sender, no
|
|
26
|
+
# subsystem), Hotwire Native's subsystem, UIKit warnings — and drops the
|
|
27
|
+
# rest. --verbose falls back to the whole process.
|
|
28
|
+
PREDICATE = 'process == "App" AND eventType == logEvent AND NOT subsystem BEGINSWITH "com.apple"'
|
|
29
|
+
VERBOSE_PREDICATE = 'process == "App"'
|
|
30
|
+
|
|
31
|
+
def call(ios: false, last: nil, udid: nil, verbose: false, **)
|
|
32
|
+
UI.die!("only the iOS Simulator is supported so far — run: every logs --ios") unless ios
|
|
33
|
+
|
|
34
|
+
udid ||= Everywhere::Simulator.booted_udid
|
|
35
|
+
UI.die!("no booted iOS Simulator — start one with `every dev --ios`") unless udid
|
|
36
|
+
|
|
37
|
+
UI.step(last ? "iOS Simulator logs from the last #{UI.bold(last)}"
|
|
38
|
+
: "watching iOS Simulator logs #{UI.dim("(Ctrl-C to stop)")}")
|
|
39
|
+
exec(*argv(udid, last: last, verbose: verbose))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# simctl spawn runs the `log` tool inside the Simulator runtime — the
|
|
43
|
+
# supported way to read a Simulator's unified log from the host. `log show`
|
|
44
|
+
# spells verbosity --info/--debug where `log stream` takes --level.
|
|
45
|
+
def argv(udid, last: nil, verbose: false)
|
|
46
|
+
argv = ["xcrun", "simctl", "spawn", udid, "log"]
|
|
47
|
+
argv += if last
|
|
48
|
+
["show", "--last", last, *(verbose ? ["--info", "--debug"] : [])]
|
|
49
|
+
else
|
|
50
|
+
["stream", *(verbose ? ["--level", "debug"] : [])]
|
|
51
|
+
end
|
|
52
|
+
argv + ["--style", "compact", "--predicate", verbose ? VERBOSE_PREDICATE : PREDICATE]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -49,13 +49,17 @@ module Everywhere
|
|
|
49
49
|
).manifest
|
|
50
50
|
|
|
51
51
|
build = submit(client, targets, channel, signed_id, manifest)
|
|
52
|
-
UI.success("submitted #{UI.bold(build["id"])} for #{targets.join(", ")}")
|
|
52
|
+
UI.success("submitted #{UI.bold(build["id"])} for #{targets.map { |t| UI.target_label(t) }.join(", ")}")
|
|
53
53
|
if (build_url = build["url"] || "#{client.base_url}/builds/#{build["id"]}")
|
|
54
54
|
UI.substep("watch it live → #{UI.cyan(build_url)}")
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
return unless wait
|
|
58
58
|
|
|
59
|
+
# Set expectations before the quiet part: a hosted macOS runner is not
|
|
60
|
+
# instant, and knowing that up front is the difference between "slow"
|
|
61
|
+
# and "broken".
|
|
62
|
+
UI.substep(UI.dim("hosted macOS runners usually start within a few minutes"))
|
|
59
63
|
final = poll_and_tail(client, build["id"])
|
|
60
64
|
report(client, final, output || File.join(root_path, "dist"), download)
|
|
61
65
|
end
|
|
@@ -94,7 +98,7 @@ module Everywhere
|
|
|
94
98
|
UI.bad("can't build yet — missing signing credentials:")
|
|
95
99
|
Array(res.body["targets"]).each do |t|
|
|
96
100
|
tgt = t["target"] || {}
|
|
97
|
-
UI.step("#{tgt["os"]}-#{tgt["arch"]}/#{tgt["distribution_channel"]}: add #{Array(t["missing"]).join(", ")}")
|
|
101
|
+
UI.step("#{UI.os_label(tgt["os"])}-#{tgt["arch"]}/#{tgt["distribution_channel"]}: add #{Array(t["missing"]).join(", ")}")
|
|
98
102
|
end
|
|
99
103
|
UI.step("set them up → #{UI.cyan(res.body["manage_url"])}")
|
|
100
104
|
exit 1
|
|
@@ -102,24 +106,74 @@ module Everywhere
|
|
|
102
106
|
UI.die!("build submission failed (HTTP #{res.status}) #{res.body["error"]}")
|
|
103
107
|
end
|
|
104
108
|
|
|
109
|
+
# How often we ask the Platform for new logs. A build spends most of its
|
|
110
|
+
# wall clock before the first log byte (GitHub queue + toolchain), so poll
|
|
111
|
+
# lazily until something is actually building, then tighten up so the log
|
|
112
|
+
# tails smoothly.
|
|
113
|
+
IDLE_POLL = 5
|
|
114
|
+
ACTIVE_POLL = 2
|
|
115
|
+
SPINNER_TICK = 0.2
|
|
116
|
+
|
|
105
117
|
def poll_and_tail(client, build_id)
|
|
106
118
|
printed = Hash.new("")
|
|
107
119
|
last = {}
|
|
120
|
+
started = now
|
|
121
|
+
status = UI::Status.new
|
|
122
|
+
|
|
108
123
|
loop do
|
|
109
124
|
res = client.get("/cli/builds/#{build_id}/logs")
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
break if artifacts.any? && artifacts.all? { |a| terminal?(a["status"]) }
|
|
125
|
+
artifacts = res.ok? ? (res.body["artifacts"] || []) : []
|
|
126
|
+
|
|
127
|
+
status.clear
|
|
128
|
+
artifacts.each do |artifact|
|
|
129
|
+
tail_log(artifact, printed)
|
|
130
|
+
announce_phase(artifact, last)
|
|
117
131
|
end
|
|
118
|
-
|
|
132
|
+
break if artifacts.any? && artifacts.all? { |a| terminal?(a["status"]) }
|
|
133
|
+
|
|
134
|
+
interval = artifacts.any? { |a| a["status"] == "running" } ? ACTIVE_POLL : IDLE_POLL
|
|
135
|
+
spin(status, artifacts, started, interval)
|
|
119
136
|
end
|
|
137
|
+
|
|
120
138
|
client.get("/cli/builds/#{build_id}").body
|
|
139
|
+
ensure
|
|
140
|
+
status&.clear
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Hold the status line alive for `interval` seconds. The spinner ticks far
|
|
144
|
+
# faster than we poll so the line reads as "alive" during the multi-minute
|
|
145
|
+
# pre-runner wait rather than freezing between requests.
|
|
146
|
+
def spin(status, artifacts, started, interval)
|
|
147
|
+
deadline = now + interval
|
|
148
|
+
loop do
|
|
149
|
+
status.update("#{waiting_summary(artifacts)} · #{UI.elapsed(now - started)} elapsed")
|
|
150
|
+
remaining = deadline - now
|
|
151
|
+
break if remaining <= 0
|
|
152
|
+
|
|
153
|
+
sleep([remaining, SPINNER_TICK].min)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# What to show on the status line. One target names its phase; several
|
|
158
|
+
# collapse to a count, since per-target detail already lands as permanent
|
|
159
|
+
# `announce_phase` lines.
|
|
160
|
+
def waiting_summary(artifacts)
|
|
161
|
+
pending = artifacts.reject { |a| terminal?(a["status"]) }
|
|
162
|
+
return "waiting for the build" if pending.empty?
|
|
163
|
+
return phase_text(pending.first) if pending.size == 1
|
|
164
|
+
|
|
165
|
+
"#{pending.size} of #{artifacts.size} targets in progress"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# `phase_label` is a newer field; fall back to the status so this still
|
|
169
|
+
# says something useful against an older Platform.
|
|
170
|
+
def phase_text(artifact)
|
|
171
|
+
label = artifact["phase_label"].to_s
|
|
172
|
+
label.empty? ? artifact["status"].to_s : label
|
|
121
173
|
end
|
|
122
174
|
|
|
175
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
176
|
+
|
|
123
177
|
def tail_log(artifact, printed)
|
|
124
178
|
full = artifact["log"].to_s
|
|
125
179
|
prev = printed[artifact["id"]]
|
|
@@ -129,11 +183,16 @@ module Everywhere
|
|
|
129
183
|
printed[artifact["id"]] = full
|
|
130
184
|
end
|
|
131
185
|
|
|
132
|
-
|
|
133
|
-
|
|
186
|
+
# A permanent line per boundary crossed. Keyed on phase, not status: the
|
|
187
|
+
# whole point is that `status` sits on `queued` through the dispatch, the
|
|
188
|
+
# GitHub queue wait, and toolchain setup — three stages the developer used
|
|
189
|
+
# to experience as one unexplained silence.
|
|
190
|
+
def announce_phase(artifact, last)
|
|
191
|
+
key = artifact["phase"] || artifact["status"]
|
|
192
|
+
return if last[artifact["id"]] == key
|
|
134
193
|
|
|
135
|
-
UI.step("#{label(artifact["target"])} → #{artifact
|
|
136
|
-
last[artifact["id"]] =
|
|
194
|
+
UI.step("#{label(artifact["target"])} → #{phase_text(artifact)}")
|
|
195
|
+
last[artifact["id"]] = key
|
|
137
196
|
end
|
|
138
197
|
|
|
139
198
|
def report(client, build, out_dir, download)
|
|
@@ -173,7 +232,7 @@ module Everywhere
|
|
|
173
232
|
end
|
|
174
233
|
|
|
175
234
|
def label(target)
|
|
176
|
-
target ? "#{target["os"]}-#{target["arch"]}" : "target"
|
|
235
|
+
target ? "#{UI.os_label(target["os"])}-#{target["arch"]}" : "target"
|
|
177
236
|
end
|
|
178
237
|
|
|
179
238
|
def terminal?(status) = %w[succeeded failed].include?(status)
|
|
@@ -43,7 +43,7 @@ module Everywhere
|
|
|
43
43
|
|
|
44
44
|
job = get!("/runner/artifacts/#{artifact}")
|
|
45
45
|
patch!("/runner/artifacts/#{artifact}", status: "running")
|
|
46
|
-
remote_log("picked up #{UI.cyan("#{job.dig("target", "os")}-#{job.dig("target", "arch")}")} for #{UI.bold(job.dig("app", "name"))}")
|
|
46
|
+
remote_log("picked up #{UI.cyan("#{UI.os_label(job.dig("target", "os"))}-#{job.dig("target", "arch")}")} for #{UI.bold(job.dig("app", "name"))}")
|
|
47
47
|
|
|
48
48
|
Dir.mktmpdir("every-runner") do |work|
|
|
49
49
|
src = fetch_snapshot(work, job)
|
|
@@ -149,6 +149,7 @@ module Everywhere
|
|
|
149
149
|
end
|
|
150
150
|
|
|
151
151
|
def complete_success(zip, release_json)
|
|
152
|
+
report_phase("uploading")
|
|
152
153
|
remote_log("uploading artifact #{UI.dim("(#{(File.size(zip) / 1024.0 / 1024.0).round(1)}MB)")}")
|
|
153
154
|
signed_id = upload_artifact(zip)
|
|
154
155
|
post!("/runner/artifacts/#{@artifact}/complete",
|
|
@@ -200,6 +201,14 @@ module Everywhere
|
|
|
200
201
|
nil # never let a log hiccup kill the build
|
|
201
202
|
end
|
|
202
203
|
|
|
204
|
+
# Progress ping for the phases that produce no log output — same contract
|
|
205
|
+
# as the workflow's curl steps, and just as non-fatal.
|
|
206
|
+
def report_phase(name)
|
|
207
|
+
@client.post("/runner/artifacts/#{@artifact}/phase", phase: name)
|
|
208
|
+
rescue Everywhere::Error
|
|
209
|
+
nil
|
|
210
|
+
end
|
|
211
|
+
|
|
203
212
|
# A runner-orchestration line: distinct from the `every release` steps it
|
|
204
213
|
# brackets (tagged "runner"), rendered once and forwarded verbatim — color
|
|
205
214
|
# and all — to the web log.
|
|
@@ -49,7 +49,7 @@ module Everywhere
|
|
|
49
49
|
# 1. Produce (or reuse) the signed-for-dev .app via the build path. The
|
|
50
50
|
# notarize step below re-signs it inside-out with the Developer ID.
|
|
51
51
|
unless skip_build
|
|
52
|
-
UI.phase("Building #{app_name} for #{target}")
|
|
52
|
+
UI.phase("Building #{app_name} for #{UI.target_label(target)}")
|
|
53
53
|
Build.new.call(root: root, ruby: ruby, entry: entry, app_name: app_name, target: target, shell_dir: shell_dir, **)
|
|
54
54
|
end
|
|
55
55
|
|