ruby_everywhere 0.1.3 → 0.1.5
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/lib/everywhere/commands/build.rb +14 -10
- data/lib/everywhere/commands/install.rb +47 -2
- data/lib/everywhere/commands/platform/build.rb +3 -0
- data/lib/everywhere/commands/platform/runner.rb +22 -9
- data/lib/everywhere/commands/release.rb +15 -15
- data/lib/everywhere/config.rb +39 -14
- data/lib/everywhere/log_filter.rb +148 -0
- data/lib/everywhere/receipt.rb +4 -3
- data/lib/everywhere/shellout.rb +36 -5
- data/lib/everywhere/ui.rb +70 -11
- data/lib/everywhere/version.rb +1 -1
- data/support/shell/src-tauri/Cargo.lock +26 -0
- data/support/shell/src-tauri/Cargo.toml +9 -0
- data/support/shell/src-tauri/src/main.rs +74 -16
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8f07eb6ae976f35ea7ccda33b87bfefd7c2d0ffd39c1b7aa1f7001bedbd482b
|
|
4
|
+
data.tar.gz: b0b9afc49cdc99e731c5bc7a742f8a90c4a5c94d496749131fa37b403567f91c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f13c6059ba2dae4dbe87c21a376265ae7cef7f84a890492897694dde101e9b94bc4990df82b6efc64f278e1ed35d3d921e8bfd7d5c131763cc79589d096d4211
|
|
7
|
+
data.tar.gz: 741d54c87c703df48024662485d03edcc15546c20f3fbe6f6d52b2b3c6056658c8aff7f2e73eb558d7f31961aa1c65c686a656bfc5e9bf9c2d900a3c633057aa
|
|
@@ -5,6 +5,7 @@ require "fileutils"
|
|
|
5
5
|
require "tmpdir"
|
|
6
6
|
require "shellwords"
|
|
7
7
|
require_relative "../shellout"
|
|
8
|
+
require_relative "../log_filter"
|
|
8
9
|
require_relative "../png"
|
|
9
10
|
require_relative "../icon"
|
|
10
11
|
require_relative "../paths"
|
|
@@ -29,20 +30,23 @@ module Everywhere
|
|
|
29
30
|
option :ruby, default: "4.0.6", desc: "Ruby version to package"
|
|
30
31
|
option :entry, default: "native_boot.rb", desc: "Entry point script relative to root"
|
|
31
32
|
option :app_name, default: nil, desc: "Bundle name (default: capitalized app dir)"
|
|
33
|
+
option :target, default: "macos-arm64", desc: "Build target (os-arch) — selects any platforms: overrides"
|
|
32
34
|
option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
|
|
33
35
|
option :skip_press, type: :boolean, default: false, desc: "Reuse the existing pressed binary, only rebuild the .app"
|
|
34
36
|
|
|
35
37
|
def call(root: ".", output: nil, ruby: "4.0.6", entry: "native_boot.rb",
|
|
36
|
-
app_name: nil, shell_dir: nil, skip_press: false, **)
|
|
38
|
+
app_name: nil, target: "macos-arm64", shell_dir: nil, skip_press: false, **)
|
|
37
39
|
root_path = File.expand_path(root)
|
|
38
40
|
config = Everywhere::Config.load(root_path)
|
|
39
|
-
|
|
41
|
+
# The os of `target` selects any per-platform overrides (bundle id,
|
|
42
|
+
# version, name) — local builds always target the host, macOS today.
|
|
43
|
+
app_name ||= config.name(target: target)
|
|
40
44
|
|
|
41
45
|
# Remote mode: thin shell around an already-deployed app. No Rails app
|
|
42
46
|
# required locally, nothing to press — just shell + config in a bundle.
|
|
43
47
|
if config.remote?
|
|
44
48
|
UI.step("#{UI.bold("remote")} mode → #{UI.cyan(config.remote_url)} #{UI.dim("(no local server packaged)")}")
|
|
45
|
-
app_dir = bundle_app(nil, app_name, shell_dir, config, dist_dir: File.expand_path("dist", root_path))
|
|
49
|
+
app_dir = bundle_app(nil, app_name, shell_dir, config, target: target, dist_dir: File.expand_path("dist", root_path))
|
|
46
50
|
return
|
|
47
51
|
end
|
|
48
52
|
|
|
@@ -64,7 +68,7 @@ module Everywhere
|
|
|
64
68
|
press!(framework, entry, ruby, output_path)
|
|
65
69
|
end
|
|
66
70
|
|
|
67
|
-
bundle_app(output_path, app_name, shell_dir, config) if RUBY_PLATFORM.include?("darwin")
|
|
71
|
+
bundle_app(output_path, app_name, shell_dir, config, target: target) if RUBY_PLATFORM.include?("darwin")
|
|
68
72
|
|
|
69
73
|
UI.success("built #{output} (#{(File.size(output_path) / 1024.0 / 1024.0).round}MB)")
|
|
70
74
|
end
|
|
@@ -86,11 +90,11 @@ module Everywhere
|
|
|
86
90
|
staging = File.join(Dir.mktmpdir("everywhere-press"), File.basename(output_path))
|
|
87
91
|
log = "#{output_path}.build.log"
|
|
88
92
|
|
|
89
|
-
UI.step("rbe-tebako press (Ruby #{ruby}
|
|
90
|
-
UI.
|
|
93
|
+
UI.step("rbe-tebako press #{UI.dim("(Ruby #{ruby} — grab a coffee on first run)")}")
|
|
94
|
+
UI.detail("full build log: #{UI.short_path(log)}")
|
|
91
95
|
Shellout.run_logged!(press_env, ["rbe-tebako", "press",
|
|
92
96
|
"--root=#{framework.root}", "--entry-point=#{entry}",
|
|
93
|
-
"--Ruby=#{ruby}", "--output=#{staging}"], log: log)
|
|
97
|
+
"--Ruby=#{ruby}", "--output=#{staging}"], log: log, filter: LogFilter.for(:tebako))
|
|
94
98
|
|
|
95
99
|
# Tebako's final strip step only warns (and still exits 0) if it can't
|
|
96
100
|
# write the output, so verify the artifact instead of trusting exit codes.
|
|
@@ -112,7 +116,7 @@ module Everywhere
|
|
|
112
116
|
# Assemble dist/<App Name>.app: the release shell binary + the pressed
|
|
113
117
|
# Rails binary (as "app-server") + Info.plist + icon. An .app bundle is
|
|
114
118
|
# just a directory layout, so we build it by hand — no tauri-cli needed.
|
|
115
|
-
def bundle_app(server_binary, app_name, shell_dir, config, dist_dir: nil)
|
|
119
|
+
def bundle_app(server_binary, app_name, shell_dir, config, target: "macos-arm64", dist_dir: nil)
|
|
116
120
|
shell_dir ||= Everywhere::Paths.shell_dir!
|
|
117
121
|
dist_dir ||= File.dirname(server_binary)
|
|
118
122
|
|
|
@@ -188,7 +192,7 @@ module Everywhere
|
|
|
188
192
|
FileUtils.cp(shell_bin, File.join(macos, exe_name))
|
|
189
193
|
FileUtils.cp(server_binary, File.join(macos, "app-server")) if server_binary
|
|
190
194
|
|
|
191
|
-
File.write(File.join(resources, "everywhere.json"), config.to_shell_json)
|
|
195
|
+
File.write(File.join(resources, "everywhere.json"), config.to_shell_json(target: target))
|
|
192
196
|
if (splash = config.splash)
|
|
193
197
|
if File.exist?(splash)
|
|
194
198
|
FileUtils.cp(splash, File.join(resources, "splash.html"))
|
|
@@ -199,7 +203,7 @@ module Everywhere
|
|
|
199
203
|
end
|
|
200
204
|
icns = make_icns(macos_icon || File.join(shell_dir, "icons", "icon.png"), resources)
|
|
201
205
|
File.write(File.join(app_dir, "Contents", "Info.plist"),
|
|
202
|
-
info_plist(app_name, exe_name, icns, config.bundle_id, config.version))
|
|
206
|
+
info_plist(app_name, exe_name, icns, config.bundle_id(target: target), config.version(target: target)))
|
|
203
207
|
|
|
204
208
|
UI.step("signing #{File.basename(app_dir)}")
|
|
205
209
|
Shellout.run!({}, "codesign", "-s", "-", "-f", File.join(macos, "app-server")) if server_binary
|
|
@@ -102,17 +102,26 @@ module Everywhere
|
|
|
102
102
|
return change("create config/everywhere.yml", false) if File.exist?(config)
|
|
103
103
|
|
|
104
104
|
app_name = File.basename(@root).split(/[-_]/).map(&:capitalize).join(" ")
|
|
105
|
+
slug = app_name.downcase.gsub(/[^a-z0-9]+/, "-")
|
|
105
106
|
File.write(config, <<~YAML)
|
|
106
|
-
# RubyEverywhere app configuration
|
|
107
|
+
# RubyEverywhere app configuration.
|
|
108
|
+
# Full reference: https://rubyeverywhere.com/docs/shared/everywhere-yml
|
|
107
109
|
app:
|
|
108
110
|
name: #{app_name}
|
|
109
111
|
# Reverse-DNS app identifier: names the app-data directory on every
|
|
110
112
|
# platform (and the macOS bundle id). Set once, never change it.
|
|
111
|
-
bundle_id: com.example.#{
|
|
113
|
+
bundle_id: com.example.#{slug}
|
|
114
|
+
version: "0.1.0" # marketing version (CFBundleShortVersionString)
|
|
115
|
+
mode: local # "local" (compile & ship the app) or "remote" (native shell around a deployed site)
|
|
112
116
|
entry_path: / # initial url on app launch
|
|
113
117
|
# icon: icon.png # PNG used to generate app icons (defaults to icon.png at the app root if present)
|
|
114
118
|
# splash: splash.html # custom boot splash (HTML); window.__EVERYWHERE_CONFIG__ carries name/colors
|
|
115
119
|
|
|
120
|
+
# Remote mode only — the deployed URL the native shell opens.
|
|
121
|
+
# Set mode: remote above, then uncomment:
|
|
122
|
+
# remote:
|
|
123
|
+
# url: https://example.com
|
|
124
|
+
|
|
116
125
|
appearance:
|
|
117
126
|
# Colors take a hex string, or split by system theme:
|
|
118
127
|
# tint_color:
|
|
@@ -122,6 +131,42 @@ module Everywhere
|
|
|
122
131
|
background_color:
|
|
123
132
|
light: "#FFFFFF"
|
|
124
133
|
dark: "#1C1B1A"
|
|
134
|
+
|
|
135
|
+
# Native menu-bar items — each one visits a route in your app (no native code).
|
|
136
|
+
# menu:
|
|
137
|
+
# - label: Settings…
|
|
138
|
+
# path: /settings
|
|
139
|
+
# accelerator: CmdOrCtrl+,
|
|
140
|
+
# - separator
|
|
141
|
+
# - label: Documentation
|
|
142
|
+
# path: /help
|
|
143
|
+
|
|
144
|
+
# System tray / menu-bar-extra items — same shape as menu, plus action: show | quit.
|
|
145
|
+
# tray:
|
|
146
|
+
# - label: Open #{app_name}
|
|
147
|
+
# action: show
|
|
148
|
+
# - label: New…
|
|
149
|
+
# path: /new
|
|
150
|
+
# - separator
|
|
151
|
+
# - label: Quit
|
|
152
|
+
# action: quit
|
|
153
|
+
|
|
154
|
+
# Durable build knobs (CLI flags still override per-run).
|
|
155
|
+
# build:
|
|
156
|
+
# ruby: "4.0.6"
|
|
157
|
+
# targets: # the platforms you ship — the CI matrix, expressed once
|
|
158
|
+
# - macos-arm64
|
|
159
|
+
# # - ios-arm64 # future — mobile is remote mode only
|
|
160
|
+
# # - android-arm64 # future — mobile is remote mode only
|
|
161
|
+
# permissions: [notifications, filesystem:read, clipboard]
|
|
162
|
+
|
|
163
|
+
# Per-platform overrides. Keys are platform names (macos / ios / android /
|
|
164
|
+
# windows / linux); anything here overrides the app: defaults above for that
|
|
165
|
+
# platform — handy when a store needs a distinct bundle id or version.
|
|
166
|
+
# platforms:
|
|
167
|
+
# ios:
|
|
168
|
+
# bundle_id: com.example.#{slug}.ios
|
|
169
|
+
# version: "0.1.1"
|
|
125
170
|
YAML
|
|
126
171
|
change("create config/everywhere.yml", true)
|
|
127
172
|
end
|
|
@@ -50,6 +50,9 @@ module Everywhere
|
|
|
50
50
|
|
|
51
51
|
build = submit(client, targets, channel, signed_id, manifest)
|
|
52
52
|
UI.success("submitted #{UI.bold(build["id"])} for #{targets.join(", ")}")
|
|
53
|
+
if (build_url = build["url"] || "#{client.base_url}/builds/#{build["id"]}")
|
|
54
|
+
UI.substep("watch it live → #{UI.cyan(build_url)}")
|
|
55
|
+
end
|
|
53
56
|
|
|
54
57
|
return unless wait
|
|
55
58
|
|
|
@@ -31,13 +31,18 @@ module Everywhere
|
|
|
31
31
|
option :shell_dir, default: nil, desc: "Path to the generic Tauri shell (src-tauri)"
|
|
32
32
|
|
|
33
33
|
def call(artifact:, token:, url: nil, shell_dir: nil, **)
|
|
34
|
+
# We run headless on a build runner (a pipe, not a TTY) and stream into
|
|
35
|
+
# the Platform web log, which renders ANSI. Force color on so the hosted
|
|
36
|
+
# log reads like a local run.
|
|
37
|
+
ENV["CLICOLOR_FORCE"] ||= "1" unless ENV["NO_COLOR"]
|
|
38
|
+
|
|
34
39
|
base = Everywhere::Platform::Client.base_url(url)
|
|
35
40
|
@client = Everywhere::Platform::Client.new(base, token: token)
|
|
36
41
|
@artifact = artifact
|
|
37
42
|
|
|
38
43
|
job = get!("/runner/artifacts/#{artifact}")
|
|
39
44
|
patch!("/runner/artifacts/#{artifact}", status: "running")
|
|
40
|
-
remote_log("
|
|
45
|
+
remote_log("picked up #{UI.cyan("#{job.dig("target", "os")}-#{job.dig("target", "arch")}")} for #{UI.bold(job.dig("app", "name"))}")
|
|
41
46
|
|
|
42
47
|
Dir.mktmpdir("every-runner") do |work|
|
|
43
48
|
src = fetch_snapshot(work, job)
|
|
@@ -63,7 +68,7 @@ module Everywhere
|
|
|
63
68
|
# ---- pipeline steps ------------------------------------------------------
|
|
64
69
|
|
|
65
70
|
def fetch_snapshot(work, job)
|
|
66
|
-
remote_log("
|
|
71
|
+
remote_log("downloading source snapshot")
|
|
67
72
|
tarball = File.join(work, "snapshot.tar.gz")
|
|
68
73
|
@client.download(job["snapshot_url"], tarball)
|
|
69
74
|
src = File.join(work, "src")
|
|
@@ -73,7 +78,7 @@ module Everywhere
|
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
def pull_credentials(work)
|
|
76
|
-
remote_log("
|
|
81
|
+
remote_log("pulling signing credentials")
|
|
77
82
|
creds = get!("/runner/artifacts/#{@artifact}/credentials")["credentials"] || []
|
|
78
83
|
env = {}
|
|
79
84
|
creds.each do |cred|
|
|
@@ -113,19 +118,23 @@ module Everywhere
|
|
|
113
118
|
listing = `security find-identity -p codesigning #{@keychain.shellescape} 2>/dev/null`
|
|
114
119
|
sha = listing[/\b([0-9A-F]{40})\b/, 1]
|
|
115
120
|
UI.die!("no codesigning identity in the delivered .p12") unless sha
|
|
116
|
-
remote_log("
|
|
121
|
+
remote_log("signing identity #{listing[/"(.+)"/, 1]} #{UI.dim("(#{sha[0, 10]}…)")}")
|
|
117
122
|
sha
|
|
118
123
|
end
|
|
119
124
|
|
|
120
125
|
def run_release(src, job, signing_env, shell_dir)
|
|
121
|
-
remote_log("
|
|
126
|
+
remote_log("every release — press → sign → notarize → staple")
|
|
122
127
|
cmd = [RbConfig.ruby, "-I", lib_dir, every_exe, "release",
|
|
123
128
|
"--root", src,
|
|
124
129
|
"--target", "#{job.dig("target", "os")}-#{job.dig("target", "arch")}",
|
|
125
130
|
"--channel", job.dig("target", "distribution_channel").to_s]
|
|
126
131
|
cmd += ["--shell-dir", shell_dir] if shell_dir
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
# The web UI renders ANSI, but `every release` runs here as a pipe (not a
|
|
134
|
+
# TTY) so it would otherwise strip all color. Force it on — for our own
|
|
135
|
+
# output and for cargo — so the hosted log looks like the local one.
|
|
136
|
+
forced = { "CLICOLOR_FORCE" => "1", "CARGO_TERM_COLOR" => "always" }
|
|
137
|
+
success = stream(forced.merge(signing_env), cmd)
|
|
129
138
|
|
|
130
139
|
dist = File.join(src, "dist")
|
|
131
140
|
release_json = JSON.parse(File.read(File.join(dist, "release.json"))) rescue nil
|
|
@@ -134,7 +143,7 @@ module Everywhere
|
|
|
134
143
|
end
|
|
135
144
|
|
|
136
145
|
def complete_success(zip, release_json)
|
|
137
|
-
remote_log("
|
|
146
|
+
remote_log("uploading artifact #{UI.dim("(#{(File.size(zip) / 1024.0 / 1024.0).round(1)}MB)")}")
|
|
138
147
|
signed_id = upload_artifact(zip)
|
|
139
148
|
post!("/runner/artifacts/#{@artifact}/complete",
|
|
140
149
|
status: "succeeded", file_signed_id: signed_id, release_json: release_json)
|
|
@@ -185,9 +194,13 @@ module Everywhere
|
|
|
185
194
|
nil # never let a log hiccup kill the build
|
|
186
195
|
end
|
|
187
196
|
|
|
197
|
+
# A runner-orchestration line: distinct from the `every release` steps it
|
|
198
|
+
# brackets (tagged "runner"), rendered once and forwarded verbatim — color
|
|
199
|
+
# and all — to the web log.
|
|
188
200
|
def remote_log(message)
|
|
189
|
-
UI.
|
|
190
|
-
|
|
201
|
+
line = "#{UI.gray("runner")} #{UI.cyan("→")} #{message}"
|
|
202
|
+
puts line
|
|
203
|
+
flush_log("#{line}\n")
|
|
191
204
|
end
|
|
192
205
|
|
|
193
206
|
# ---- helpers -------------------------------------------------------------
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require_relative "../shellout"
|
|
5
|
+
require_relative "../log_filter"
|
|
5
6
|
require_relative "../receipt"
|
|
6
7
|
require_relative "../paths"
|
|
7
8
|
require_relative "build"
|
|
@@ -37,7 +38,7 @@ module Everywhere
|
|
|
37
38
|
app_name: nil, shell_dir: nil, env_file: nil, skip_build: false, **)
|
|
38
39
|
root_path = File.expand_path(root)
|
|
39
40
|
config = Everywhere::Config.load(root_path)
|
|
40
|
-
app_name ||= config.name
|
|
41
|
+
app_name ||= config.name(target: target)
|
|
41
42
|
ruby ||= config.build_ruby || "4.0.6"
|
|
42
43
|
|
|
43
44
|
preflight!(target)
|
|
@@ -48,8 +49,8 @@ module Everywhere
|
|
|
48
49
|
# 1. Produce (or reuse) the signed-for-dev .app via the build path. The
|
|
49
50
|
# notarize step below re-signs it inside-out with the Developer ID.
|
|
50
51
|
unless skip_build
|
|
51
|
-
UI.
|
|
52
|
-
Build.new.call(root: root, ruby: ruby, entry: entry, app_name: app_name, shell_dir: shell_dir, **)
|
|
52
|
+
UI.phase("Building #{app_name} for #{target}")
|
|
53
|
+
Build.new.call(root: root, ruby: ruby, entry: entry, app_name: app_name, target: target, shell_dir: shell_dir, **)
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
app_dir = Dir[File.join(dist_dir, "*.app")].max_by { |d| File.mtime(d) }
|
|
@@ -58,10 +59,11 @@ module Everywhere
|
|
|
58
59
|
# 2. Sign (Developer ID + hardened runtime) → notarize → staple → verify.
|
|
59
60
|
env_file ||= discover_env_file(root_path)
|
|
60
61
|
env = env_file ? { "ENV_FILE" => env_file } : {}
|
|
61
|
-
UI.
|
|
62
|
+
UI.phase("Signing & notarizing #{File.basename(app_dir)}")
|
|
63
|
+
UI.step("preparing #{File.basename(app_dir)}#{env_file ? " #{UI.dim("(env: #{rel(env_file, root_path)})")}" : ""}")
|
|
62
64
|
UI.warn("no signing env found — set APPLE_* or pass --env-file (notarize.sh will fail without it)") unless env_file || ENV["APPLE_SIGNING_IDENTITY"]
|
|
63
65
|
log = "#{app_dir}.notarize.log"
|
|
64
|
-
Shellout.run_logged!(env, ["/bin/bash", NOTARIZE, app_dir], log: log)
|
|
66
|
+
Shellout.run_logged!(env, ["/bin/bash", NOTARIZE, app_dir], log: log, filter: LogFilter.for(:notarize))
|
|
65
67
|
|
|
66
68
|
# 3. Gather signing facts from the bundle itself, then emit the receipt.
|
|
67
69
|
artifact = "#{app_dir.sub(/\.app\z/, "")}.zip" # notarize.sh re-zips the stapled app
|
|
@@ -125,18 +127,16 @@ module Everywhere
|
|
|
125
127
|
end
|
|
126
128
|
|
|
127
129
|
def summarize(out, artifact, signing, root_path)
|
|
128
|
-
UI.
|
|
129
|
-
UI.
|
|
130
|
-
UI.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
UI.
|
|
130
|
+
UI.phase("Release ready 🎉")
|
|
131
|
+
UI.success("#{rel(artifact, root_path)} #{UI.dim("(#{(File.size(artifact) / 1024.0 / 1024.0).round}MB — send this to your friends)")}")
|
|
132
|
+
UI.substep("signed by #{UI.bold(signing["signed_by"] || "?")}")
|
|
133
|
+
UI.substep("notarized #{UI.yn(signing.dig("platform", "notarized"))} · " \
|
|
134
|
+
"stapled #{UI.yn(signing.dig("platform", "stapled"))} · " \
|
|
135
|
+
"gatekeeper #{UI.yn(signing["verified"])}")
|
|
136
|
+
UI.substep("receipt #{UI.dim(rel(out, root_path))}")
|
|
135
137
|
end
|
|
136
138
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
def rel(path, root_path) = path.sub("#{root_path}/", "").sub("#{Dir.pwd}/", "")
|
|
139
|
+
def rel(path, root_path) = UI.short_path(path.sub("#{root_path}/", "").sub("#{Dir.pwd}/", ""))
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
end
|
data/lib/everywhere/config.rb
CHANGED
|
@@ -31,15 +31,23 @@ module Everywhere
|
|
|
31
31
|
@root = root
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
# Every target (os-arch string) resolves to a single platform (its os), and
|
|
35
|
+
# a platform may override the app-level identity/version keys — see
|
|
36
|
+
# `platforms:` below. Pass `target:` to any of these accessors to apply the
|
|
37
|
+
# matching override; omit it for the shared app-level value.
|
|
38
|
+
def self.os_of(target) = target.to_s.split("-", 2).first
|
|
39
|
+
|
|
40
|
+
def name(target: nil)
|
|
41
|
+
resolved(target)["name"] || File.basename(File.expand_path(root)).split(/[-_]/).map(&:capitalize).join(" ")
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
# Reverse-DNS identifier: macOS bundle id, and the name of the per-user
|
|
39
45
|
# app-data directory on every platform. Set it explicitly and never change
|
|
40
|
-
# it — renaming moves users' data.
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
# it — renaming moves users' data. A platform may override it (e.g. the App
|
|
47
|
+
# Store often wants `com.example.app.ios`).
|
|
48
|
+
def bundle_id(target: nil)
|
|
49
|
+
resolved(target)["bundle_id"] ||
|
|
50
|
+
"com.rubyeverywhere.#{name(target: target).downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")}"
|
|
43
51
|
end
|
|
44
52
|
|
|
45
53
|
# App icon source: a PNG (ideally square, 1024px+). Explicit `app.icon`
|
|
@@ -67,9 +75,11 @@ module Everywhere
|
|
|
67
75
|
end
|
|
68
76
|
|
|
69
77
|
# Marketing/display version of the app (CFBundleShortVersionString, and the
|
|
70
|
-
# `version` recorded in the build receipt).
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
# `version` recorded in the build receipt). One shared `app.version` is the
|
|
79
|
+
# default for every target; a platform may override it when a store forces a
|
|
80
|
+
# different number. Defaults conservatively.
|
|
81
|
+
def version(target: nil)
|
|
82
|
+
resolved(target)["version"] || "0.1.0"
|
|
73
83
|
end
|
|
74
84
|
|
|
75
85
|
# The `build:` section — durable build knobs that were once `every build`
|
|
@@ -149,11 +159,13 @@ module Everywhere
|
|
|
149
159
|
end
|
|
150
160
|
|
|
151
161
|
# The subset the shell needs, shipped as JSON (env var in dev,
|
|
152
|
-
# Resources/everywhere.json inside a bundled .app).
|
|
153
|
-
|
|
162
|
+
# Resources/everywhere.json inside a bundled .app). Pass `target:` so the
|
|
163
|
+
# packaged config reflects the platform being built (its bundle id / name).
|
|
164
|
+
def to_shell_hash(target: nil)
|
|
154
165
|
{
|
|
155
|
-
"name" => name,
|
|
156
|
-
"bundle_id" => bundle_id,
|
|
166
|
+
"name" => name(target: target),
|
|
167
|
+
"bundle_id" => bundle_id(target: target),
|
|
168
|
+
"version" => version(target: target),
|
|
157
169
|
"mode" => mode,
|
|
158
170
|
"remote_url" => remote_url,
|
|
159
171
|
"entry_path" => entry_path,
|
|
@@ -164,9 +176,9 @@ module Everywhere
|
|
|
164
176
|
}.compact
|
|
165
177
|
end
|
|
166
178
|
|
|
167
|
-
def to_shell_json
|
|
179
|
+
def to_shell_json(target: nil)
|
|
168
180
|
require "json"
|
|
169
|
-
JSON.generate(to_shell_hash)
|
|
181
|
+
JSON.generate(to_shell_hash(target: target))
|
|
170
182
|
end
|
|
171
183
|
|
|
172
184
|
private
|
|
@@ -174,6 +186,19 @@ module Everywhere
|
|
|
174
186
|
def app = @data.fetch("app", nil) || {}
|
|
175
187
|
def appearance = @data.fetch("appearance", nil) || {}
|
|
176
188
|
|
|
189
|
+
# Per-platform overrides, keyed by os (macos / ios / android / windows /
|
|
190
|
+
# linux). Each value overrides the matching `app:` keys for that platform.
|
|
191
|
+
def platforms = @data.fetch("platforms", nil) || {}
|
|
192
|
+
|
|
193
|
+
# The app-level keys with the target's platform override merged on top.
|
|
194
|
+
# Blank override values are dropped (`.compact`) so they don't erase a
|
|
195
|
+
# default. Without a target — or without an override for that os — this is
|
|
196
|
+
# just the plain `app:` hash.
|
|
197
|
+
def resolved(target)
|
|
198
|
+
override = target && platforms[self.class.os_of(target)]
|
|
199
|
+
override.is_a?(Hash) ? app.merge(override.compact) : app
|
|
200
|
+
end
|
|
201
|
+
|
|
177
202
|
def normalize_color(value)
|
|
178
203
|
case value
|
|
179
204
|
when String
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ui"
|
|
4
|
+
|
|
5
|
+
module Everywhere
|
|
6
|
+
# Turns the raw, chatty output of the external build tools (tebako, the
|
|
7
|
+
# notarize script and the Apple tools it drives) into clean, indented, colored
|
|
8
|
+
# lines for the console and the Platform web log.
|
|
9
|
+
#
|
|
10
|
+
# A filter is a small state machine fed one raw line at a time. It returns:
|
|
11
|
+
# * a String -> print this (already colored/indented) instead of the raw line
|
|
12
|
+
# * :drop -> swallow the line (pure noise: repeated status, temp paths…)
|
|
13
|
+
# * nil -> not recognized; caller shows the raw line dimmed as a detail
|
|
14
|
+
#
|
|
15
|
+
# The FULL raw stream is always still written to the on-disk log, so nothing is
|
|
16
|
+
# lost — this only governs what a human sees scroll by.
|
|
17
|
+
class LogFilter
|
|
18
|
+
def self.for(profile) = new(profile)
|
|
19
|
+
|
|
20
|
+
def initialize(profile)
|
|
21
|
+
@profile = profile
|
|
22
|
+
@seen = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(raw)
|
|
26
|
+
line = raw.to_s.rstrip
|
|
27
|
+
return :drop if line.empty?
|
|
28
|
+
|
|
29
|
+
case @profile
|
|
30
|
+
when :notarize then notarize(line)
|
|
31
|
+
when :tebako then tebako(line)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# --- `every release` signing pipeline (notarize.sh + Apple tools) ---------
|
|
38
|
+
|
|
39
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
40
|
+
def notarize(line)
|
|
41
|
+
case line
|
|
42
|
+
# notarize.sh's own "==> phase" markers become substeps.
|
|
43
|
+
when /\A==> signing with:\s*(.+)/
|
|
44
|
+
UI.substep_line("signing bundle #{UI.dim("(#{short_identity(Regexp.last_match(1))})")}")
|
|
45
|
+
when /\A==> verifying signature/
|
|
46
|
+
UI.substep_line("verifying signature")
|
|
47
|
+
when /\A==> zipping for notarization:\s*(.+)/
|
|
48
|
+
UI.substep_line("zipping for notarization #{UI.dim("→ #{File.basename(Regexp.last_match(1))}")}")
|
|
49
|
+
when /\A==> submitting to Apple notary service/
|
|
50
|
+
UI.substep_line("submitting to Apple notary service #{UI.dim("(this can take a few minutes)")}")
|
|
51
|
+
when /\A==> stapling ticket/
|
|
52
|
+
UI.substep_line("stapling notarization ticket")
|
|
53
|
+
when /\A==> Gatekeeper assessment/
|
|
54
|
+
UI.substep_line("Gatekeeper assessment")
|
|
55
|
+
when /\A==> re-zipping/
|
|
56
|
+
UI.substep_line("re-zipping stapled app for distribution")
|
|
57
|
+
|
|
58
|
+
# Per-binary signing chatter -> one dim line each (basename only).
|
|
59
|
+
when /\A\s+sign (.+)/
|
|
60
|
+
UI.note_line("signed #{File.basename(Regexp.last_match(1))}")
|
|
61
|
+
when /: replacing existing signature/
|
|
62
|
+
:drop
|
|
63
|
+
|
|
64
|
+
# codesign --verify / stapler / spctl outcomes.
|
|
65
|
+
when /: valid on disk/
|
|
66
|
+
UI.note_line("valid on disk", marker: "✓", color: :green)
|
|
67
|
+
when /: satisfies its Designated Requirement/
|
|
68
|
+
:drop
|
|
69
|
+
when /The staple and validate action worked!/
|
|
70
|
+
UI.note_line("stapled and validated", marker: "✓", color: :green)
|
|
71
|
+
when /The validate action worked!/
|
|
72
|
+
:drop
|
|
73
|
+
when /\A(.+): accepted/
|
|
74
|
+
UI.note_line("Gatekeeper accepted", marker: "✓", color: :green)
|
|
75
|
+
when /\Asource=/, /\Aorigin=/
|
|
76
|
+
UI.note_line(line)
|
|
77
|
+
|
|
78
|
+
# notarytool --wait progress. Collapse the repeated dotted "In Progress"
|
|
79
|
+
# lines into a single heartbeat, and celebrate the terminal states.
|
|
80
|
+
when /Current status:\s*Accepted/
|
|
81
|
+
UI.note_line("notarization accepted", marker: "✓", color: :green)
|
|
82
|
+
when /Current status:\s*(Invalid|Rejected)/
|
|
83
|
+
UI.note_line("notarization #{Regexp.last_match(1).downcase}", marker: "✗", color: :red)
|
|
84
|
+
when /Current status:\s*In Progress/
|
|
85
|
+
once(:progress) { UI.note_line("waiting on Apple…", marker: "⋯") }
|
|
86
|
+
when /\A\s*id:\s*([0-9a-fA-F-]{36})/
|
|
87
|
+
once(:submission) { UI.note_line("submission #{Regexp.last_match(1)[0, 8]}…") }
|
|
88
|
+
|
|
89
|
+
# Pure noise from notarytool's own prose / the final banner (release.rb
|
|
90
|
+
# prints its own summary, so drop notarize.sh's).
|
|
91
|
+
when /\AConducting pre-submission checks/,
|
|
92
|
+
/\ASubmission ID received/,
|
|
93
|
+
/\ASuccessfully uploaded file/,
|
|
94
|
+
/\A\s*path:/,
|
|
95
|
+
/\A\s*status:/,
|
|
96
|
+
/\AWaiting for processing/,
|
|
97
|
+
/\AProcessing:/,
|
|
98
|
+
/Processing complete/,
|
|
99
|
+
/\A✅ Done\./,
|
|
100
|
+
/Send this to your friend/,
|
|
101
|
+
/Stapled, so it works even offline/,
|
|
102
|
+
/zipping preserves the signature/
|
|
103
|
+
:drop
|
|
104
|
+
|
|
105
|
+
else
|
|
106
|
+
nil # unrecognized -> caller shows it dimmed
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
110
|
+
|
|
111
|
+
# --- tebako press ---------------------------------------------------------
|
|
112
|
+
#
|
|
113
|
+
# Thousands of lines of C++/Ruby build output. We can't restructure it, but
|
|
114
|
+
# we can keep errors/warnings legible and dim the rest into the background so
|
|
115
|
+
# the important lines (and our own steps around it) stand out.
|
|
116
|
+
def tebako(line)
|
|
117
|
+
short = UI.short_path(line)
|
|
118
|
+
case line
|
|
119
|
+
when /\b(error|fatal|undefined reference|cannot find)\b/i
|
|
120
|
+
" #{UI.red(short)}"
|
|
121
|
+
when /\b(warning|deprecated)\b/i
|
|
122
|
+
" #{UI.yellow(short)}"
|
|
123
|
+
when /\A-- /, /\bBuilding\b/, /\bConfiguring\b/, /\bInstalling\b/
|
|
124
|
+
UI.note_line(short.sub(/\A-- /, ""))
|
|
125
|
+
else
|
|
126
|
+
" #{UI.gray(short)}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Show a line only the first time its key is seen; :drop the repeats. Keeps a
|
|
131
|
+
# long notarization from scrolling a wall of identical "waiting" lines (and
|
|
132
|
+
# notarytool's thrice-echoed submission id from showing three times).
|
|
133
|
+
def once(key)
|
|
134
|
+
return :drop if @seen[key]
|
|
135
|
+
|
|
136
|
+
@seen[key] = true
|
|
137
|
+
yield
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# "Developer ID Application: Andrea Fomera (TU4485Z3KR)" or a bare SHA-1.
|
|
141
|
+
def short_identity(id)
|
|
142
|
+
id = id.strip
|
|
143
|
+
return "#{id[0, 10]}…" if id.match?(/\A[0-9A-F]{40}\z/i)
|
|
144
|
+
|
|
145
|
+
id[/\(([^)]+)\)\s*\z/, 1] || id
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
data/lib/everywhere/receipt.rb
CHANGED
|
@@ -27,6 +27,7 @@ module Everywhere
|
|
|
27
27
|
@config = config
|
|
28
28
|
@framework = framework
|
|
29
29
|
@ruby = ruby
|
|
30
|
+
@target_str = target # raw "os-arch" — selects platforms: overrides
|
|
30
31
|
@target = parse_target(target, channel)
|
|
31
32
|
@shell_dir = shell_dir
|
|
32
33
|
end
|
|
@@ -35,9 +36,9 @@ module Everywhere
|
|
|
35
36
|
def manifest
|
|
36
37
|
{
|
|
37
38
|
"app" => {
|
|
38
|
-
"name" => @config.name,
|
|
39
|
-
"bundle_id" => @config.bundle_id,
|
|
40
|
-
"version" => @config.version
|
|
39
|
+
"name" => @config.name(target: @target_str),
|
|
40
|
+
"bundle_id" => @config.bundle_id(target: @target_str),
|
|
41
|
+
"version" => @config.version(target: @target_str)
|
|
41
42
|
},
|
|
42
43
|
"framework" => framework_info,
|
|
43
44
|
"ruby" => @ruby,
|
data/lib/everywhere/shellout.rb
CHANGED
|
@@ -31,14 +31,45 @@ module Everywhere
|
|
|
31
31
|
["", nil]
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# Run a command
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
# Run a command, capturing its full combined output to a log file while
|
|
35
|
+
# showing a (optionally filtered) view on the console.
|
|
36
|
+
#
|
|
37
|
+
# Pass filter: a LogFilter (or any #call(line) -> String | :drop | nil) to
|
|
38
|
+
# prettify what a human sees; the on-disk log always gets the raw stream, so
|
|
39
|
+
# nothing is lost. Unrecognized lines (filter returns nil) are shown dimmed
|
|
40
|
+
# and indented so they read as background detail under the current step.
|
|
41
|
+
def run_logged!(env, cmd, log:, filter: nil)
|
|
42
|
+
require "open3"
|
|
43
|
+
success = false
|
|
44
|
+
unbundled do
|
|
45
|
+
File.open(log, "w") do |logf|
|
|
46
|
+
Open3.popen2e(env, *cmd) do |stdin, out, wait|
|
|
47
|
+
stdin.close
|
|
48
|
+
out.each_line do |raw|
|
|
49
|
+
logf.write(raw)
|
|
50
|
+
emit(raw, filter)
|
|
51
|
+
end
|
|
52
|
+
success = wait.value.success?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
39
56
|
UI.die!("command failed: #{cmd.join(" ")} (full log: #{log})") unless success
|
|
40
57
|
end
|
|
41
58
|
|
|
59
|
+
# Route one raw line to the console: verbatim when there's no filter, else
|
|
60
|
+
# the filter's rendition (:drop swallows it, nil falls back to dim detail).
|
|
61
|
+
def emit(raw, filter)
|
|
62
|
+
return $stdout.print(raw) unless filter
|
|
63
|
+
|
|
64
|
+
shown = filter.call(raw)
|
|
65
|
+
case shown
|
|
66
|
+
when :drop, false then nil
|
|
67
|
+
when nil then $stdout.puts(" #{UI.gray(UI.short_path(raw.rstrip))}")
|
|
68
|
+
else $stdout.puts(shown)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
private_class_method :emit
|
|
72
|
+
|
|
42
73
|
def spawn(env, cmd, chdir:)
|
|
43
74
|
unbundled { Process.spawn(env, cmd, chdir: chdir) }
|
|
44
75
|
end
|
data/lib/everywhere/ui.rb
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
module Everywhere
|
|
4
4
|
# Terminal output helpers. Colors follow the informal standard:
|
|
5
5
|
# on when stdout is a TTY or CLICOLOR_FORCE=1, always off when NO_COLOR is set.
|
|
6
|
+
#
|
|
7
|
+
# The hosted runner streams `every release` through a pipe (not a TTY) into the
|
|
8
|
+
# Platform web log, so it exports CLICOLOR_FORCE=1 to keep our color — the web
|
|
9
|
+
# UI renders ANSI. That's why we force rather than sniff the TTY there.
|
|
6
10
|
module UI
|
|
7
11
|
module_function
|
|
8
12
|
|
|
@@ -17,21 +21,76 @@ module Everywhere
|
|
|
17
21
|
color? ? "\e[#{codes.join(";")}m#{text}\e[0m" : text.to_s
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
def bold(t)
|
|
21
|
-
def dim(t)
|
|
22
|
-
def red(t)
|
|
23
|
-
def green(t)
|
|
24
|
-
def yellow(t)
|
|
25
|
-
def
|
|
24
|
+
def bold(t) = paint(t, 1)
|
|
25
|
+
def dim(t) = paint(t, 2)
|
|
26
|
+
def red(t) = paint(t, 31)
|
|
27
|
+
def green(t) = paint(t, 32)
|
|
28
|
+
def yellow(t) = paint(t, 33)
|
|
29
|
+
def blue(t) = paint(t, 34)
|
|
30
|
+
def magenta(t) = paint(t, 35)
|
|
31
|
+
def cyan(t) = paint(t, 36)
|
|
32
|
+
def gray(t) = paint(t, 90) # bright black — dimmer than dim() on most themes
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
# --- output levels --------------------------------------------------------
|
|
35
|
+
#
|
|
36
|
+
# phase ● a major phase boundary (build / sign / notarize)
|
|
37
|
+
# step → a top-level action inside a phase
|
|
38
|
+
# substep ▸ a sub-action under a step (indented)
|
|
39
|
+
# detail · raw-ish detail under a substep (indented + dim)
|
|
40
|
+
#
|
|
41
|
+
# Every level has a *_line string builder (used by LogFilter, which returns
|
|
42
|
+
# lines for someone else to print) and a printer that puts it. ok / success /
|
|
43
|
+
# warn / bad / die! carry outcomes at any level.
|
|
44
|
+
|
|
45
|
+
def phase_line(msg) = "\n#{bold(cyan("●"))} #{bold(msg)}"
|
|
46
|
+
def step_line(msg) = "#{cyan("→")} #{msg}"
|
|
47
|
+
def substep_line(msg) = " #{cyan("▸")} #{msg}"
|
|
48
|
+
def detail_line(msg) = " #{gray("·")} #{gray(msg)}"
|
|
49
|
+
def ok_line(msg) = "#{green("✓")} #{msg}"
|
|
50
|
+
def bad_line(msg) = "#{red("✗")} #{msg}"
|
|
51
|
+
def warn_line(msg) = "#{yellow("!")} #{msg}"
|
|
52
|
+
def success_line(msg) = "#{green("✓")} #{bold(msg)}"
|
|
53
|
+
|
|
54
|
+
def phase(msg) = puts(phase_line(msg))
|
|
55
|
+
def step(msg) = puts(step_line(msg))
|
|
56
|
+
def substep(msg) = puts(substep_line(msg))
|
|
57
|
+
def detail(msg) = puts(detail_line(msg))
|
|
58
|
+
def ok(msg) = puts(ok_line(msg))
|
|
59
|
+
def bad(msg) = puts(bad_line(msg))
|
|
60
|
+
def warn(msg) = puts(warn_line(msg))
|
|
61
|
+
def success(msg) = puts(success_line(msg))
|
|
62
|
+
|
|
63
|
+
# An indented, dim outcome under a substep — e.g. "accepted", "valid on disk".
|
|
64
|
+
def note_line(msg, marker: "·", color: :gray)
|
|
65
|
+
tint = respond_to?(color) ? method(color) : method(:gray)
|
|
66
|
+
" #{tint.call(marker)} #{tint.call(msg)}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def note(msg, marker: "·", color: :gray) = puts(note_line(msg, marker: marker, color: color))
|
|
32
70
|
|
|
33
71
|
def die!(msg)
|
|
34
72
|
abort "#{red("✗")} #{red(msg)}"
|
|
35
73
|
end
|
|
74
|
+
|
|
75
|
+
# A yes/no fact, colored by truthiness (for signing receipts).
|
|
76
|
+
def yn(bool) = bool ? green("yes") : red("no")
|
|
77
|
+
|
|
78
|
+
# --- path shortening ------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
# Collapse the noisy machine paths that external tools echo (codesign,
|
|
81
|
+
# notarytool, stapler) down to something a human can scan. In particular the
|
|
82
|
+
# runner works out of a deep `/var/folders/…/every-runner…/src/` sandbox — we
|
|
83
|
+
# strip that prefix so only the meaningful `dist/App.app` tail remains. Then
|
|
84
|
+
# $HOME → ~ and the cwd → a leading (dropped) prefix.
|
|
85
|
+
def short_path(str)
|
|
86
|
+
s = str.to_s.dup
|
|
87
|
+
# Runner/local temp sandboxes: keep only the part after the extracted src/.
|
|
88
|
+
s = s.gsub(%r{/(?:private/)?(?:var|tmp)/[^\s"']*?/every-[a-z]+[0-9A-Za-z_-]+/src/}, "")
|
|
89
|
+
# Any other everywhere-* scratch dir: keep the basename tail.
|
|
90
|
+
s = s.gsub(%r{/(?:private/)?(?:var|tmp)/[^\s"']*?/everywhere-[a-z]+[0-9A-Za-z_.-]*/}, "")
|
|
91
|
+
home = ENV["HOME"]
|
|
92
|
+
s = s.gsub("#{home}/", "~/") if home && !home.empty?
|
|
93
|
+
s
|
|
94
|
+
end
|
|
36
95
|
end
|
|
37
96
|
end
|
data/lib/everywhere/version.rb
CHANGED
|
@@ -993,6 +993,9 @@ dependencies = [
|
|
|
993
993
|
name = "example-shell"
|
|
994
994
|
version = "0.1.0"
|
|
995
995
|
dependencies = [
|
|
996
|
+
"objc2",
|
|
997
|
+
"objc2-foundation",
|
|
998
|
+
"objc2-web-kit",
|
|
996
999
|
"serde_json",
|
|
997
1000
|
"tauri",
|
|
998
1001
|
"tauri-build",
|
|
@@ -2366,6 +2369,16 @@ dependencies = [
|
|
|
2366
2369
|
"objc2-core-foundation",
|
|
2367
2370
|
]
|
|
2368
2371
|
|
|
2372
|
+
[[package]]
|
|
2373
|
+
name = "objc2-javascript-core"
|
|
2374
|
+
version = "0.3.2"
|
|
2375
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2376
|
+
checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
|
|
2377
|
+
dependencies = [
|
|
2378
|
+
"objc2",
|
|
2379
|
+
"objc2-core-foundation",
|
|
2380
|
+
]
|
|
2381
|
+
|
|
2369
2382
|
[[package]]
|
|
2370
2383
|
name = "objc2-quartz-core"
|
|
2371
2384
|
version = "0.3.2"
|
|
@@ -2378,6 +2391,17 @@ dependencies = [
|
|
|
2378
2391
|
"objc2-foundation",
|
|
2379
2392
|
]
|
|
2380
2393
|
|
|
2394
|
+
[[package]]
|
|
2395
|
+
name = "objc2-security"
|
|
2396
|
+
version = "0.3.2"
|
|
2397
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2398
|
+
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
|
|
2399
|
+
dependencies = [
|
|
2400
|
+
"bitflags 2.13.0",
|
|
2401
|
+
"objc2",
|
|
2402
|
+
"objc2-core-foundation",
|
|
2403
|
+
]
|
|
2404
|
+
|
|
2381
2405
|
[[package]]
|
|
2382
2406
|
name = "objc2-ui-kit"
|
|
2383
2407
|
version = "0.3.2"
|
|
@@ -2421,6 +2445,8 @@ dependencies = [
|
|
|
2421
2445
|
"objc2-app-kit",
|
|
2422
2446
|
"objc2-core-foundation",
|
|
2423
2447
|
"objc2-foundation",
|
|
2448
|
+
"objc2-javascript-core",
|
|
2449
|
+
"objc2-security",
|
|
2424
2450
|
]
|
|
2425
2451
|
|
|
2426
2452
|
[[package]]
|
|
@@ -14,6 +14,15 @@ tauri-plugin-notification = "2"
|
|
|
14
14
|
tauri-plugin-dialog = "2"
|
|
15
15
|
tauri-plugin-clipboard-manager = "2"
|
|
16
16
|
|
|
17
|
+
# macOS: set the WebView's `applicationNameForUserAgent`, which WKWebView
|
|
18
|
+
# natively APPENDS to its real default UA — so we get the true system UA plus
|
|
19
|
+
# our marker without ever reading or hardcoding the base string. Versions are
|
|
20
|
+
# pinned to match what wry 0.55 already resolves, so no duplicate objc2 trees.
|
|
21
|
+
[target.'cfg(target_os = "macos")'.dependencies]
|
|
22
|
+
objc2 = "0.6"
|
|
23
|
+
objc2-foundation = { version = "0.3", features = ["NSString", "NSUUID", "NSProcessInfo"] }
|
|
24
|
+
objc2-web-kit = { version = "0.3", features = ["WKWebViewConfiguration", "WKWebsiteDataStore"] }
|
|
25
|
+
|
|
17
26
|
[profile.release]
|
|
18
27
|
strip = true
|
|
19
28
|
lto = true
|
|
@@ -32,6 +32,7 @@ struct AppConfig {
|
|
|
32
32
|
raw_json: String,
|
|
33
33
|
name: String,
|
|
34
34
|
bundle_id: Option<String>,
|
|
35
|
+
version: Option<String>,
|
|
35
36
|
entry_path: String,
|
|
36
37
|
remote_url: Option<String>,
|
|
37
38
|
background_light: Option<(u8, u8, u8)>,
|
|
@@ -71,6 +72,7 @@ fn load_config() -> AppConfig {
|
|
|
71
72
|
raw_json: if value.is_null() { "{}".to_string() } else { value.to_string() },
|
|
72
73
|
name: value["name"].as_str().unwrap_or("RubyEverywhere App").to_string(),
|
|
73
74
|
bundle_id: value["bundle_id"].as_str().map(str::to_string),
|
|
75
|
+
version: value["version"].as_str().map(str::to_string),
|
|
74
76
|
entry_path: value["entry_path"].as_str().unwrap_or("/").to_string(),
|
|
75
77
|
remote_url: (value["mode"].as_str() == Some("remote"))
|
|
76
78
|
.then(|| value["remote_url"].as_str().map(str::to_string))
|
|
@@ -549,12 +551,72 @@ const INIT_SCRIPT: &str = r#"
|
|
|
549
551
|
})();
|
|
550
552
|
"#;
|
|
551
553
|
|
|
554
|
+
// The marker we append to the WebView's own user agent. We never build or
|
|
555
|
+
// hardcode the browser part of the UA — the WebView keeps producing its real
|
|
556
|
+
// default and this is added on the end, so anything the Ruby side sniffs (the
|
|
557
|
+
// `browser` gem, `request.variant`) still sees a genuine desktop browser. The
|
|
558
|
+
// contract with the app: `request.user_agent =~ %r{RubyEverywhere/([\d.]+)}`
|
|
559
|
+
// tells it the request came from the native shell and which app version (from
|
|
560
|
+
// everywhere.yml → the submitted build) made it. Version is absent only in bare
|
|
561
|
+
// `cargo run` dev where no config was passed.
|
|
562
|
+
fn ua_marker(config: &AppConfig) -> String {
|
|
563
|
+
let os = std::env::consts::OS;
|
|
564
|
+
match &config.version {
|
|
565
|
+
Some(v) => format!("RubyEverywhere/{v} ({os})"),
|
|
566
|
+
None => format!("RubyEverywhere ({os})"),
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// macOS: build the WKWebViewConfiguration ourselves so we can set
|
|
571
|
+
// `applicationNameForUserAgent` — WebKit appends it to the real default UA (the
|
|
572
|
+
// one clean, no-read way to *append* rather than replace). Because wry uses the
|
|
573
|
+
// config's own `websiteDataStore` when we supply a configuration (and ignores
|
|
574
|
+
// Tauri's `data_store_identifier`), we must reproduce the per-app persistent
|
|
575
|
+
// store here too, or logins would leak into the shared default store. Mirrors
|
|
576
|
+
// wry's own logic: identified store on macOS 14+, default otherwise / in dev.
|
|
577
|
+
#[cfg(target_os = "macos")]
|
|
578
|
+
fn macos_webview_configuration(
|
|
579
|
+
config: &AppConfig,
|
|
580
|
+
dev_mode: bool,
|
|
581
|
+
) -> objc2::rc::Retained<objc2_web_kit::WKWebViewConfiguration> {
|
|
582
|
+
use objc2::MainThreadMarker;
|
|
583
|
+
use objc2_foundation::{NSProcessInfo, NSString, NSUUID, NSOperatingSystemVersion};
|
|
584
|
+
use objc2_web_kit::{WKWebViewConfiguration, WKWebsiteDataStore};
|
|
585
|
+
|
|
586
|
+
let mtm = MainThreadMarker::new().expect("webview configuration must be built on the main thread");
|
|
587
|
+
let cfg = unsafe { WKWebViewConfiguration::new(mtm) };
|
|
588
|
+
|
|
589
|
+
unsafe {
|
|
590
|
+
cfg.setApplicationNameForUserAgent(Some(&NSString::from_str(&ua_marker(config))));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Cookie / session persistence, keyed by bundle_id so each app is isolated.
|
|
594
|
+
// Dev mode keeps the default store (matches the previous ephemeral-ish dev
|
|
595
|
+
// behaviour). dataStoreForIdentifier needs macOS 14+.
|
|
596
|
+
if !dev_mode {
|
|
597
|
+
let os14 = NSProcessInfo::processInfo().isOperatingSystemAtLeastVersion(
|
|
598
|
+
NSOperatingSystemVersion { majorVersion: 14, minorVersion: 0, patchVersion: 0 },
|
|
599
|
+
);
|
|
600
|
+
if os14 {
|
|
601
|
+
let id = config.bundle_id.as_deref().unwrap_or("com.rubyeverywhere.app");
|
|
602
|
+
let store = unsafe {
|
|
603
|
+
WKWebsiteDataStore::dataStoreForIdentifier(&NSUUID::from_bytes(data_store_id(id)), mtm)
|
|
604
|
+
};
|
|
605
|
+
unsafe { cfg.setWebsiteDataStore(&store) };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
cfg
|
|
610
|
+
}
|
|
611
|
+
|
|
552
612
|
fn build_window(
|
|
553
613
|
app: &tauri::App,
|
|
554
614
|
config: &AppConfig,
|
|
555
615
|
title: &str,
|
|
556
616
|
url: WebviewUrl,
|
|
557
617
|
) -> tauri::Result<tauri::WebviewWindow> {
|
|
618
|
+
let dev_mode = std::env::var("NATIVE_DEV_URL").is_ok();
|
|
619
|
+
|
|
558
620
|
let mut builder = WebviewWindowBuilder::new(app, "main", url)
|
|
559
621
|
.title(title)
|
|
560
622
|
.inner_size(1100.0, 750.0)
|
|
@@ -566,23 +628,19 @@ fn build_window(
|
|
|
566
628
|
}
|
|
567
629
|
|
|
568
630
|
// Cookie / session persistence: give the webview a stable data store so
|
|
569
|
-
// logins survive relaunches. macOS
|
|
570
|
-
//
|
|
571
|
-
// ephemeral — a persistent cache would keep serving stale
|
|
572
|
-
|
|
631
|
+
// logins survive relaunches. macOS carries it (and the UA marker) on a
|
|
632
|
+
// custom WKWebViewConfiguration; Windows/Linux want an explicit directory.
|
|
633
|
+
// Dev mode stays ephemeral — a persistent cache would keep serving stale
|
|
634
|
+
// dev assets.
|
|
635
|
+
#[cfg(target_os = "macos")]
|
|
636
|
+
{
|
|
637
|
+
builder = builder.with_webview_configuration(macos_webview_configuration(config, dev_mode));
|
|
638
|
+
}
|
|
639
|
+
#[cfg(any(windows, target_os = "linux"))]
|
|
573
640
|
if !dev_mode {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
builder = builder.
|
|
577
|
-
config.bundle_id.as_deref().unwrap_or("com.rubyeverywhere.app"),
|
|
578
|
-
));
|
|
579
|
-
}
|
|
580
|
-
#[cfg(any(windows, target_os = "linux"))]
|
|
581
|
-
{
|
|
582
|
-
if let Ok(dir) = app.path().data_dir() {
|
|
583
|
-
let id = config.bundle_id.clone().unwrap_or("com.rubyeverywhere.app".into());
|
|
584
|
-
builder = builder.data_directory(dir.join(id).join("webview"));
|
|
585
|
-
}
|
|
641
|
+
if let Ok(dir) = app.path().data_dir() {
|
|
642
|
+
let id = config.bundle_id.clone().unwrap_or("com.rubyeverywhere.app".into());
|
|
643
|
+
builder = builder.data_directory(dir.join(id).join("webview"));
|
|
586
644
|
}
|
|
587
645
|
}
|
|
588
646
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_everywhere
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrea Fomera
|
|
@@ -69,6 +69,7 @@ files:
|
|
|
69
69
|
- lib/everywhere/icon.rb
|
|
70
70
|
- lib/everywhere/ignore.rb
|
|
71
71
|
- lib/everywhere/javascript/bridge.js
|
|
72
|
+
- lib/everywhere/log_filter.rb
|
|
72
73
|
- lib/everywhere/paths.rb
|
|
73
74
|
- lib/everywhere/platform/client.rb
|
|
74
75
|
- lib/everywhere/platform/credentials.rb
|