ruby_everywhere 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +86 -2
- data/bridge/README.md +70 -1
- data/bridge/everywhere/bridge.js +151 -0
- data/bridge/everywhere/native.css +61 -0
- data/lib/everywhere/auth_handoff.rb +8 -2
- data/lib/everywhere/builders/desktop.rb +326 -0
- data/lib/everywhere/child_processes.rb +74 -0
- data/lib/everywhere/commands/build.rb +44 -5
- data/lib/everywhere/commands/dev.rb +426 -59
- data/lib/everywhere/commands/install.rb +20 -0
- data/lib/everywhere/commands/release.rb +2 -2
- data/lib/everywhere/config.rb +298 -4
- data/lib/everywhere/console.rb +117 -0
- data/lib/everywhere/desktop_assets.rb +150 -0
- data/lib/everywhere/dock/footer.rb +150 -0
- data/lib/everywhere/dock/screen.rb +114 -0
- data/lib/everywhere/dock/state.rb +59 -0
- data/lib/everywhere/dock.rb +238 -0
- data/lib/everywhere/emulator.rb +2 -2
- data/lib/everywhere/fatal.rb +20 -0
- data/lib/everywhere/line_pump.rb +89 -0
- data/lib/everywhere/log_filter.rb +37 -0
- data/lib/everywhere/native_helper.rb +38 -5
- data/lib/everywhere/paths.rb +62 -11
- data/lib/everywhere/relay.rb +77 -0
- data/lib/everywhere/shellout.rb +90 -15
- data/lib/everywhere/task_pool.rb +123 -0
- data/lib/everywhere/ui.rb +54 -11
- data/lib/everywhere/version.rb +1 -1
- data/support/desktop/README.md +121 -0
- data/support/{shell → desktop}/src-tauri/Cargo.lock +23 -0
- data/support/{shell → desktop}/src-tauri/Cargo.toml +19 -1
- data/support/{shell → desktop}/src-tauri/capabilities/default.json +7 -0
- data/support/desktop/src-tauri/gen/schemas/capabilities.json +1 -0
- data/support/desktop/src-tauri/src/extension_host.rs +53 -0
- data/support/desktop/src-tauri/src/extensions/mod.rs +39 -0
- data/support/{shell → desktop}/src-tauri/src/main.rs +355 -10
- data/support/{shell → desktop}/src-tauri/tauri.conf.json +2 -2
- data/support/{macos → release/macos}/notarize.sh +2 -2
- metadata +31 -17
- data/support/github/build.yml +0 -85
- data/support/shell/src-tauri/gen/schemas/capabilities.json +0 -1
- /data/support/{shell → desktop}/splash/index.html +0 -0
- /data/support/{shell → desktop}/src-tauri/build.rs +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/acl-manifests.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/desktop-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/macOS-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/icons/icon.png +0 -0
- /data/support/{shell → desktop}/src-tauri/src/updater.rs +0 -0
- /data/support/{macos → release/macos}/entitlements.plist +0 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "../paths"
|
|
5
|
+
require_relative "../ui"
|
|
6
|
+
require_relative "../version"
|
|
7
|
+
|
|
8
|
+
module Everywhere
|
|
9
|
+
module Builders
|
|
10
|
+
# Resolves where cargo should run for the desktop shell, and stamps a
|
|
11
|
+
# per-app copy of it when the app declares `native.desktop`.
|
|
12
|
+
#
|
|
13
|
+
# Two paths, deliberately:
|
|
14
|
+
#
|
|
15
|
+
# no native.desktop the gem's own support/desktop/src-tauri, compiled
|
|
16
|
+
# against the SHARED ~/.rubyeverywhere/shell-target.
|
|
17
|
+
# Identical source for every app, so one warm target
|
|
18
|
+
# dir serves all of them. This is what every app did
|
|
19
|
+
# before extensions existed, and it stays free.
|
|
20
|
+
#
|
|
21
|
+
# native.desktop a stamped copy under ~/.rubyeverywhere/desktop/<id>
|
|
22
|
+
# with its own target dir, exactly like the iOS and
|
|
23
|
+
# Android work dirs. Costs one cold Tauri compile
|
|
24
|
+
# (minutes) and a couple of GB, which is why it is not
|
|
25
|
+
# the default.
|
|
26
|
+
#
|
|
27
|
+
# The stamped list, and nothing else:
|
|
28
|
+
#
|
|
29
|
+
# Cargo.toml the template's, with native.desktop.crates
|
|
30
|
+
# substituted into the __EVERYWHERE_APP_CRATES__
|
|
31
|
+
# marker
|
|
32
|
+
# src/extensions/** native/desktop/**/*.rs, plus a generated
|
|
33
|
+
# mod.rs per directory
|
|
34
|
+
#
|
|
35
|
+
# Everything else in the work dir is the frozen template, copied as-is. In
|
|
36
|
+
# particular src/main.rs, src/extension_host.rs and capabilities/ are never
|
|
37
|
+
# rewritten — see support/desktop/README.md.
|
|
38
|
+
class Desktop
|
|
39
|
+
# The generated module file, at every level of src/extensions. Reserved:
|
|
40
|
+
# an app shipping its own would be silently overwritten, so we refuse.
|
|
41
|
+
MODULE_FILE = "mod.rs"
|
|
42
|
+
STAGE_MARKER = ".everywhere-template"
|
|
43
|
+
CRATES_MARKER = "# __EVERYWHERE_APP_CRATES__"
|
|
44
|
+
|
|
45
|
+
# Where the app keeps its Rust, relative to the app root.
|
|
46
|
+
SOURCE_DIR = File.join("native", "desktop")
|
|
47
|
+
|
|
48
|
+
Prepared = Struct.new(:dir, :target_dir, :stamped, keyword_init: true) do
|
|
49
|
+
def stamped? = stamped
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def initialize(config:, root:, template_dir: nil)
|
|
53
|
+
@config = config
|
|
54
|
+
@root = root
|
|
55
|
+
@template_dir = template_dir
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns where to run cargo and what to set CARGO_TARGET_DIR to. Callers
|
|
59
|
+
# (`every dev`, `every build`) don't need to care which path they got.
|
|
60
|
+
def prepare!
|
|
61
|
+
template = @template_dir || Paths.shell_dir!
|
|
62
|
+
unless @config.native_desktop?
|
|
63
|
+
return Prepared.new(dir: template, target_dir: Paths.cargo_target_dir, stamped: false)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
preflight!
|
|
67
|
+
src_tauri = stage(template)
|
|
68
|
+
stamp!(src_tauri)
|
|
69
|
+
Prepared.new(dir: src_tauri, target_dir: Paths.desktop_target_dir(bundle_id), stamped: true)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def bundle_id = @config.bundle_id
|
|
75
|
+
|
|
76
|
+
# Config errors first: they're free to check, and everything below either
|
|
77
|
+
# interpolates these values into generated Rust or spends minutes on cargo.
|
|
78
|
+
def preflight!
|
|
79
|
+
errors = @config.native_desktop_errors
|
|
80
|
+
return if errors.empty?
|
|
81
|
+
|
|
82
|
+
UI.die!("native.desktop in config/everywhere.yml:\n#{errors.join("\n")}")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Merge-copy the template over the work dir, NOT a wipe like the iOS
|
|
86
|
+
# stage: cargo's incremental state (target/ is elsewhere, but .fingerprint
|
|
87
|
+
# data and the lock file are not) plus a resolved Cargo.lock live here, and
|
|
88
|
+
# a cold Tauri build is minutes against a warm one's seconds. The one
|
|
89
|
+
# directory the stamp regenerates wholesale (src/extensions/) is cleared by
|
|
90
|
+
# its own writer, so deleted app files stop compiling.
|
|
91
|
+
#
|
|
92
|
+
# A gem upgrade wipes it: the template's own sources changed, and merging
|
|
93
|
+
# new files over old ones would leave whatever the new template deleted.
|
|
94
|
+
# `template` points at src-tauri (that's what --shell-dir means), but the
|
|
95
|
+
# whole shell root has to come along: tauri.conf.json's frontendDist is
|
|
96
|
+
# "../splash", a sibling of src-tauri, and generate_context! panics at
|
|
97
|
+
# compile time if it isn't there. So we copy the parent and hand back the
|
|
98
|
+
# src-tauri inside it.
|
|
99
|
+
def stage(template)
|
|
100
|
+
work = Paths.desktop_work_dir(bundle_id)
|
|
101
|
+
FileUtils.rm_rf(work) unless staged_version(work) == Everywhere::VERSION
|
|
102
|
+
FileUtils.mkdir_p(work)
|
|
103
|
+
FileUtils.cp_r(File.join(File.dirname(template), "."), work)
|
|
104
|
+
File.write(File.join(work, STAGE_MARKER), "#{Everywhere::VERSION}\n")
|
|
105
|
+
File.join(work, File.basename(template))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def staged_version(work)
|
|
109
|
+
marker = File.join(work, STAGE_MARKER)
|
|
110
|
+
File.read(marker).strip if File.file?(marker)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def stamp!(work)
|
|
114
|
+
write_cargo_toml(work)
|
|
115
|
+
write_extensions(work)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Cargo has no include mechanism, so the crate list can't be its own file
|
|
119
|
+
# the way Android's native-packages.gradle.kts is — the template carries a
|
|
120
|
+
# marker comment as the last line of [dependencies] and we replace it here.
|
|
121
|
+
# In the STAMPED copy: the gem's Cargo.toml is never touched.
|
|
122
|
+
def write_cargo_toml(work)
|
|
123
|
+
path = File.join(work, "Cargo.toml")
|
|
124
|
+
source = File.read(path)
|
|
125
|
+
unless source.include?(CRATES_MARKER)
|
|
126
|
+
UI.die!("the shell's Cargo.toml has no #{CRATES_MARKER} marker — " \
|
|
127
|
+
"reinstall ruby_everywhere or pass --shell-dir at your own risk")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
crates = @config.native_desktop_crates
|
|
131
|
+
UI.step("desktop crates: #{crates.keys.join(", ")}") unless crates.empty?
|
|
132
|
+
File.write(path, source.sub(CRATES_MARKER, render_crates(crates)))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def render_crates(crates)
|
|
136
|
+
return "# (no native.desktop.crates declared)" if crates.empty?
|
|
137
|
+
|
|
138
|
+
lines = ["# from native.desktop.crates in config/everywhere.yml"]
|
|
139
|
+
lines += crates.map { |name, spec| "#{name} = #{render_spec(spec)}" }
|
|
140
|
+
lines.join("\n")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# A lone version renders as cargo's short form; anything else as an inline
|
|
144
|
+
# table. Key order is fixed so a rebuild with unchanged config produces a
|
|
145
|
+
# byte-identical Cargo.toml — cargo re-resolves on any change to this file.
|
|
146
|
+
TABLE_KEYS = %w[version git branch tag rev path features default_features optional].freeze
|
|
147
|
+
|
|
148
|
+
def render_spec(spec)
|
|
149
|
+
return toml_string(spec["version"]) if spec.keys == ["version"]
|
|
150
|
+
|
|
151
|
+
pairs = TABLE_KEYS.filter_map do |key|
|
|
152
|
+
value = spec[key]
|
|
153
|
+
next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
154
|
+
|
|
155
|
+
# default_features is the only key whose YAML spelling differs from
|
|
156
|
+
# cargo's; everything else is verbatim.
|
|
157
|
+
"#{key.tr("_", "-")} = #{toml_value(value)}"
|
|
158
|
+
end
|
|
159
|
+
"{ #{pairs.join(", ")} }"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def toml_value(value)
|
|
163
|
+
case value
|
|
164
|
+
when true, false then value.to_s
|
|
165
|
+
when Array then "[#{value.map { |item| toml_string(item) }.join(", ")}]"
|
|
166
|
+
else toml_string(value)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Config validation already rejects quotes, backslashes and newlines in
|
|
171
|
+
# every string that reaches here; escaping anyway is belt-and-braces, so a
|
|
172
|
+
# future key added to TABLE_KEYS can't become an injection by omission.
|
|
173
|
+
def toml_string(value)
|
|
174
|
+
%("#{value.to_s.gsub("\\", "\\\\\\\\").gsub('"', '\"')}")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# native/desktop/**/*.rs → src/extensions/, preserving relative paths, plus
|
|
178
|
+
# a generated mod.rs per directory. Rust needs a module declaration for
|
|
179
|
+
# every file, so the directory tree IS the module tree.
|
|
180
|
+
def write_extensions(work)
|
|
181
|
+
dest = File.join(work, "src", "extensions")
|
|
182
|
+
# Regenerated wholesale: a file deleted from the app repo has to stop
|
|
183
|
+
# compiling, and a stale module declaration would fail the build in a
|
|
184
|
+
# way that points at the wrong place.
|
|
185
|
+
FileUtils.rm_rf(dest)
|
|
186
|
+
FileUtils.mkdir_p(dest)
|
|
187
|
+
|
|
188
|
+
source = File.join(@root, SOURCE_DIR)
|
|
189
|
+
sources = File.directory?(source) ? Dir.glob(File.join(source, "**", "*.rs")).sort : []
|
|
190
|
+
sources.each do |path|
|
|
191
|
+
relative = path.delete_prefix("#{source}/")
|
|
192
|
+
if File.basename(relative) == MODULE_FILE
|
|
193
|
+
UI.die!("#{File.join(SOURCE_DIR, relative)} is generated by `every build` — " \
|
|
194
|
+
"remove it and let the CLI write the module declarations")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
target = File.join(dest, relative)
|
|
198
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
199
|
+
FileUtils.cp(path, target)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
modules = module_tree(sources.map { |path| path.delete_prefix("#{source}/") })
|
|
203
|
+
write_module_files(dest, modules)
|
|
204
|
+
report(sources, modules)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Directory → the child modules declared in its mod.rs. "" is the root.
|
|
208
|
+
# A .rs file contributes its basename; a sub-directory contributes itself
|
|
209
|
+
# and gets a mod.rs of its own.
|
|
210
|
+
def module_tree(relatives)
|
|
211
|
+
tree = Hash.new { |hash, key| hash[key] = [] }
|
|
212
|
+
tree[""] # the root always exists, even with no sources
|
|
213
|
+
relatives.each do |relative|
|
|
214
|
+
parts = relative.delete_suffix(".rs").split(File::SEPARATOR)
|
|
215
|
+
parts.each_with_index do |part, index|
|
|
216
|
+
parent = parts[0...index].join(File::SEPARATOR)
|
|
217
|
+
tree[parent] << part unless tree[parent].include?(part)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
tree.transform_values(&:sort)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def write_module_files(dest, modules)
|
|
224
|
+
modules.each do |dir, children|
|
|
225
|
+
path = dir.empty? ? File.join(dest, MODULE_FILE) : File.join(dest, dir, MODULE_FILE)
|
|
226
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
227
|
+
File.write(path, dir.empty? ? root_module(modules) : child_module(children))
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def child_module(children)
|
|
232
|
+
<<~RUST
|
|
233
|
+
// GENERATED by `every build` — edits are overwritten.
|
|
234
|
+
// Declares the .rs files this directory holds in native/desktop/.
|
|
235
|
+
#{children.map { |name| "pub mod #{name};" }.join("\n")}
|
|
236
|
+
RUST
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Everything reachable from the root, as `self::a::b` paths, so the
|
|
240
|
+
# dispatcher can glob-import them all.
|
|
241
|
+
def module_paths(modules)
|
|
242
|
+
modules.flat_map do |dir, children|
|
|
243
|
+
prefix = dir.empty? ? [] : dir.split(File::SEPARATOR)
|
|
244
|
+
children.map { |name| (prefix + [name]).join("::") }
|
|
245
|
+
end.sort
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def root_module(modules)
|
|
249
|
+
commands = @config.native_desktop_commands
|
|
250
|
+
paths = module_paths(modules)
|
|
251
|
+
|
|
252
|
+
<<~RUST
|
|
253
|
+
// GENERATED by `every build` / `every dev` from `native.desktop` in
|
|
254
|
+
// config/everywhere.yml, alongside the app's own .rs files copied from
|
|
255
|
+
// native/desktop/. Edits are overwritten — see support/desktop/README.md.
|
|
256
|
+
|
|
257
|
+
use crate::commands;
|
|
258
|
+
|
|
259
|
+
#{modules.fetch("", []).map { |name| "pub mod #{name};" }.join("\n")}
|
|
260
|
+
|
|
261
|
+
// Command names are global, the same way iOS screen types and asset
|
|
262
|
+
// names are: you declare `scan_ports`, not `serial::scan_ports`. Two
|
|
263
|
+
// modules defining the same name is a compile error naming both, which
|
|
264
|
+
// is the right way to find out.
|
|
265
|
+
#{glob_imports(paths)}
|
|
266
|
+
|
|
267
|
+
// The shell's own commands. App extensions answer over events instead —
|
|
268
|
+
// see extension_host.rs for why invoke isn't available to them.
|
|
269
|
+
pub fn handler() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
|
270
|
+
tauri::generate_handler![commands::notify_command]
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
pub fn setup(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
|
274
|
+
crate::extension_host::install_command_bridge(app, dispatch);
|
|
275
|
+
#{setup_call} Ok(())
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn dispatch(
|
|
279
|
+
app: &tauri::AppHandle,
|
|
280
|
+
name: &str,
|
|
281
|
+
payload: serde_json::Value,
|
|
282
|
+
) -> Result<serde_json::Value, String> {
|
|
283
|
+
match name {
|
|
284
|
+
#{dispatch_arms(commands)} _ => Err(format!(
|
|
285
|
+
"no desktop command named {name:?} — declare it under native.desktop.commands"
|
|
286
|
+
)),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
RUST
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# One attribute per import, not one for the block: #[allow] applies to the
|
|
293
|
+
# item that follows it, so a single leading attribute would leave every
|
|
294
|
+
# import after the first warning. Most of these ARE unused — a module of
|
|
295
|
+
# helpers with no command in it is normal.
|
|
296
|
+
def glob_imports(paths)
|
|
297
|
+
paths.map { |path| "#[allow(unused_imports)]\nuse self::#{path}::*;" }.join("\n")
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# `setup: true` means the app defines setup() in native/desktop/setup.rs.
|
|
301
|
+
# One fixed location rather than a configurable one: it's called exactly
|
|
302
|
+
# once from exactly here, so a second way to spell it buys nothing.
|
|
303
|
+
def setup_call
|
|
304
|
+
return "" unless @config.native_desktop_setup?
|
|
305
|
+
|
|
306
|
+
" setup::setup(app)?;\n"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def dispatch_arms(commands)
|
|
310
|
+
commands.map { |name| " #{name.inspect} => #{name}(app, payload),\n" }.join
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def report(sources, modules)
|
|
314
|
+
commands = @config.native_desktop_commands
|
|
315
|
+
if sources.empty? && !commands.empty?
|
|
316
|
+
UI.warn("native.desktop declares #{commands.join(", ")} but #{SOURCE_DIR}/ has no .rs files")
|
|
317
|
+
return
|
|
318
|
+
end
|
|
319
|
+
return if sources.empty?
|
|
320
|
+
|
|
321
|
+
UI.step("desktop extensions: #{sources.length} file#{"s" unless sources.length == 1} " \
|
|
322
|
+
"(#{modules.fetch("", []).join(", ")})#{" → #{commands.join(", ")}" unless commands.empty?}")
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
# A thread-scoped registry of the long-running children a task spawned, so the
|
|
5
|
+
# task can be cancelled without hunting pids by hand.
|
|
6
|
+
#
|
|
7
|
+
# `every dev` runs each build in its own thread. When the session ends (or a
|
|
8
|
+
# build has to be abandoned) we need to kill *that* thread's xcodebuild or
|
|
9
|
+
# gradlew and nothing else — a global list would leak entries from threads
|
|
10
|
+
# that died unexpectedly and would make "which build does this pid belong to?"
|
|
11
|
+
# unanswerable. Registering against the spawning thread answers it for free.
|
|
12
|
+
#
|
|
13
|
+
# Only genuinely long-running children register (see Shellout.run_logged!).
|
|
14
|
+
# Short probes — simctl, adb, xcrun — deliberately stay in the CLI's own
|
|
15
|
+
# process group, where a Ctrl-C at the terminal kills them as it always has.
|
|
16
|
+
module ChildProcesses
|
|
17
|
+
LOCK = Mutex.new
|
|
18
|
+
PIDS = :everywhere_child_pids
|
|
19
|
+
ISOLATE = :everywhere_isolate_children
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Mark this thread's long-running children as needing their own process
|
|
24
|
+
# group — set by TaskPool for every worker. Outside a worker this is false
|
|
25
|
+
# and `every build` / `every release` keep today's behavior, where a Ctrl-C
|
|
26
|
+
# at the terminal reaches the compiler directly.
|
|
27
|
+
def isolate! = Thread.current.thread_variable_set(ISOLATE, true)
|
|
28
|
+
|
|
29
|
+
def isolated? = !!Thread.current.thread_variable_get(ISOLATE)
|
|
30
|
+
|
|
31
|
+
def track(pid) = LOCK.synchronize { list << pid }
|
|
32
|
+
def untrack(pid) = LOCK.synchronize { list.delete(pid) }
|
|
33
|
+
|
|
34
|
+
# TERM every child `thread` spawned, escalating to KILL for anything still
|
|
35
|
+
# alive after `grace`. Returns the pids that had to be killed.
|
|
36
|
+
def terminate(thread, grace: 5)
|
|
37
|
+
pids = LOCK.synchronize { (thread.thread_variable_get(PIDS) || []).dup }
|
|
38
|
+
return [] if pids.empty?
|
|
39
|
+
|
|
40
|
+
pids.each { |pid| signal(pid, "TERM") }
|
|
41
|
+
deadline = now + grace
|
|
42
|
+
sleep(0.1) while pids.any? { |pid| alive?(pid) } && now < deadline
|
|
43
|
+
pids.select { |pid| alive?(pid) }.each { |pid| signal(pid, "KILL") }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Signal a child's whole process group, falling back to the bare pid when it
|
|
47
|
+
# doesn't lead one. Never raises: callers are teardown paths.
|
|
48
|
+
def signal(pid, sig)
|
|
49
|
+
Process.kill(sig, -Process.getpgid(pid))
|
|
50
|
+
rescue StandardError
|
|
51
|
+
begin
|
|
52
|
+
Process.kill(sig, pid)
|
|
53
|
+
rescue StandardError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def alive?(pid)
|
|
59
|
+
Process.kill(0, pid)
|
|
60
|
+
true
|
|
61
|
+
rescue Errno::ESRCH
|
|
62
|
+
false
|
|
63
|
+
rescue Errno::EPERM
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
68
|
+
|
|
69
|
+
def list
|
|
70
|
+
Thread.current.thread_variable_get(PIDS) ||
|
|
71
|
+
Thread.current.thread_variable_set(PIDS, [])
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -9,8 +9,10 @@ require_relative "../log_filter"
|
|
|
9
9
|
require_relative "../png"
|
|
10
10
|
require_relative "../icon"
|
|
11
11
|
require_relative "../paths"
|
|
12
|
+
require_relative "../desktop_assets"
|
|
12
13
|
require_relative "../builders/ios"
|
|
13
14
|
require_relative "../builders/android"
|
|
15
|
+
require_relative "../builders/desktop"
|
|
14
16
|
|
|
15
17
|
module Everywhere
|
|
16
18
|
module Commands
|
|
@@ -68,6 +70,16 @@ module Everywhere
|
|
|
68
70
|
UI.die!("no builder for target #{target} — macos-*, ios-* and android-* are supported")
|
|
69
71
|
end
|
|
70
72
|
|
|
73
|
+
# Desktop chrome is validated here rather than in the shell: a typo in
|
|
74
|
+
# `window:` would otherwise fall through Rust's match arms as a silent
|
|
75
|
+
# default, and you'd learn about it by squinting at a title bar after a
|
|
76
|
+
# full press+compile. The mobile builders validate their own sections
|
|
77
|
+
# the same way (Builders::Ios#write_extensions).
|
|
78
|
+
window_errors = config.window_errors
|
|
79
|
+
unless window_errors.empty?
|
|
80
|
+
UI.die!("window: in config/everywhere.yml:\n#{window_errors.join("\n")}")
|
|
81
|
+
end
|
|
82
|
+
|
|
71
83
|
# The os of `target` selects any per-platform overrides (bundle id,
|
|
72
84
|
# version, name) — local builds always target the host, macOS today.
|
|
73
85
|
app_name ||= config.name(target: target)
|
|
@@ -153,7 +165,7 @@ module Everywhere
|
|
|
153
165
|
# EVERY_BUNDLE_PATH (set by `every platform runner`) redirects the app's
|
|
154
166
|
# gems to a stable per-user dir so CI can cache them across builds. It
|
|
155
167
|
# must be merged EXPLICITLY into each host-side bundler command:
|
|
156
|
-
# Shellout.
|
|
168
|
+
# Shellout.child_env strips inherited BUNDLE_* vars, so an inherited
|
|
157
169
|
# BUNDLE_PATH would never survive to the child process.
|
|
158
170
|
def bundle_env
|
|
159
171
|
path = ENV["EVERY_BUNDLE_PATH"]
|
|
@@ -204,7 +216,12 @@ module Everywhere
|
|
|
204
216
|
# Rails binary (as "app-server") + Info.plist + icon. An .app bundle is
|
|
205
217
|
# just a directory layout, so we build it by hand — no tauri-cli needed.
|
|
206
218
|
def bundle_app(server_binary, app_name, shell_dir, config, target: "macos-arm64", dist_dir: nil)
|
|
207
|
-
|
|
219
|
+
# An app with native.desktop gets a stamped per-app shell; everyone else
|
|
220
|
+
# compiles the gem's copy against the shared target dir, exactly as
|
|
221
|
+
# before. Builders::Desktop decides which, and the rest of this method
|
|
222
|
+
# doesn't need to know.
|
|
223
|
+
prepared = Builders::Desktop.new(config: config, root: config.root, template_dir: shell_dir).prepare!
|
|
224
|
+
shell_dir = prepared.dir
|
|
208
225
|
dist_dir ||= File.dirname(server_binary)
|
|
209
226
|
|
|
210
227
|
icon_source = config.icon
|
|
@@ -235,6 +252,12 @@ module Everywhere
|
|
|
235
252
|
# shell must be COMPILED with the app's icon. Swap in the shaped master
|
|
236
253
|
# for the build, restore the placeholder after; touch build.rs so
|
|
237
254
|
# tauri-build re-embeds.
|
|
255
|
+
#
|
|
256
|
+
# The restore only matters on the fast path, where shell_dir is the
|
|
257
|
+
# INSTALLED GEM and every app shares it — leaving one app's icon there
|
|
258
|
+
# would brand the next app's build. A stamped work dir belongs to this
|
|
259
|
+
# app alone, so its icon can stay: restoring would only force another
|
|
260
|
+
# re-embed on the next build.
|
|
238
261
|
shell_icon = File.join(shell_dir, "icons", "icon.png")
|
|
239
262
|
placeholder_backup = "#{shell_icon}.placeholder"
|
|
240
263
|
if icon_source
|
|
@@ -254,12 +277,12 @@ module Everywhere
|
|
|
254
277
|
FileUtils.touch(File.join(shell_dir, "build.rs"))
|
|
255
278
|
end
|
|
256
279
|
|
|
257
|
-
UI.step("building shell #{UI.dim("(cargo release)")}")
|
|
258
|
-
target_dir =
|
|
280
|
+
UI.step("building shell #{UI.dim(prepared.stamped? ? "(cargo release, per-app)" : "(cargo release)")}")
|
|
281
|
+
target_dir = prepared.target_dir
|
|
259
282
|
begin
|
|
260
283
|
Shellout.run!({ "CARGO_TARGET_DIR" => target_dir }, "cargo", "build", "--release", chdir: shell_dir)
|
|
261
284
|
ensure
|
|
262
|
-
if icon_source && File.exist?(placeholder_backup)
|
|
285
|
+
if !prepared.stamped? && icon_source && File.exist?(placeholder_backup)
|
|
263
286
|
FileUtils.mv(placeholder_backup, shell_icon)
|
|
264
287
|
FileUtils.touch(File.join(shell_dir, "build.rs"))
|
|
265
288
|
end
|
|
@@ -280,6 +303,7 @@ module Everywhere
|
|
|
280
303
|
FileUtils.cp(server_binary, File.join(macos, "app-server")) if server_binary
|
|
281
304
|
|
|
282
305
|
File.write(File.join(resources, "everywhere.json"), config.to_shell_json(target: target))
|
|
306
|
+
write_desktop_assets(config, resources)
|
|
283
307
|
if (splash = config.splash)
|
|
284
308
|
if File.exist?(splash)
|
|
285
309
|
FileUtils.cp(splash, File.join(resources, "splash.html"))
|
|
@@ -306,6 +330,21 @@ module Everywhere
|
|
|
306
330
|
FileUtils.remove_entry(work) if work && File.directory?(work)
|
|
307
331
|
end
|
|
308
332
|
|
|
333
|
+
# native/desktop/assets/** → Contents/Resources/assets/, where the shell
|
|
334
|
+
# finds the tray icon and extension Rust finds anything it draws. The
|
|
335
|
+
# mobile builders compile their assets into a platform resource format;
|
|
336
|
+
# desktop has none, so this is a validated copy (Everywhere::DesktopAssets).
|
|
337
|
+
def write_desktop_assets(config, resources)
|
|
338
|
+
source = File.join(config.root, "native", "desktop", "assets")
|
|
339
|
+
names, errors = Everywhere::DesktopAssets.write!(source, File.join(resources, "assets"))
|
|
340
|
+
unless errors.empty?
|
|
341
|
+
UI.die!("native/desktop/assets:\n#{errors.join("\n")}")
|
|
342
|
+
end
|
|
343
|
+
return if names.empty?
|
|
344
|
+
|
|
345
|
+
UI.step("desktop assets: #{names.join(", ")}")
|
|
346
|
+
end
|
|
347
|
+
|
|
309
348
|
# Write Resources/AppIcon.icns from a (shaped) PNG. Pure Ruby — no sips or
|
|
310
349
|
# iconutil — so it produces the same icns on any build host.
|
|
311
350
|
def make_icns(png, resources_dir)
|