ruby_everywhere 0.1.15 → 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 +3 -3
- data/lib/everywhere/commands/platform/runner.rb +1 -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 +18 -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
- 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,7 +49,7 @@ 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
|
|
@@ -98,7 +98,7 @@ module Everywhere
|
|
|
98
98
|
UI.bad("can't build yet — missing signing credentials:")
|
|
99
99
|
Array(res.body["targets"]).each do |t|
|
|
100
100
|
tgt = t["target"] || {}
|
|
101
|
-
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(", ")}")
|
|
102
102
|
end
|
|
103
103
|
UI.step("set them up → #{UI.cyan(res.body["manage_url"])}")
|
|
104
104
|
exit 1
|
|
@@ -232,7 +232,7 @@ module Everywhere
|
|
|
232
232
|
end
|
|
233
233
|
|
|
234
234
|
def label(target)
|
|
235
|
-
target ? "#{target["os"]}-#{target["arch"]}" : "target"
|
|
235
|
+
target ? "#{UI.os_label(target["os"])}-#{target["arch"]}" : "target"
|
|
236
236
|
end
|
|
237
237
|
|
|
238
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)
|
|
@@ -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
|
|
|
@@ -4,14 +4,21 @@ require "dry/cli"
|
|
|
4
4
|
|
|
5
5
|
module Everywhere
|
|
6
6
|
module Commands
|
|
7
|
-
# Print the absolute path of the gem-bundled
|
|
7
|
+
# Print the absolute path of the gem-bundled shell for a target's os.
|
|
8
8
|
# Output is the bare path so external tooling can capture it — e.g. the
|
|
9
9
|
# build-runner workflows resolve the shell with `$(every shell-dir)`.
|
|
10
|
+
# Default stays the desktop Tauri shell (src-tauri) so existing callers
|
|
11
|
+
# keep working; `--target ios` prints the Hotwire Native template instead.
|
|
10
12
|
class ShellDir < Dry::CLI::Command
|
|
11
|
-
desc "Print the path of the bundled
|
|
13
|
+
desc "Print the path of the bundled shell (Tauri src-tauri, or the iOS template)"
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
option :target, default: "macos-arm64", desc: "Build target (os or os-arch) selecting which shell"
|
|
16
|
+
|
|
17
|
+
def call(target: "macos-arm64", **)
|
|
18
|
+
case Everywhere::Config.os_of(target)
|
|
19
|
+
when "ios" then puts Everywhere::Paths.ios_dir!
|
|
20
|
+
else puts Everywhere::Paths.shell_dir!
|
|
21
|
+
end
|
|
15
22
|
end
|
|
16
23
|
end
|
|
17
24
|
end
|