ruby_everywhere 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +339 -0
- data/bridge/README.md +70 -1
- data/bridge/everywhere/bridge.js +211 -3
- data/bridge/everywhere/native.css +61 -0
- data/lib/everywhere/android_resources.rb +217 -0
- data/lib/everywhere/android_sdk.rb +330 -0
- data/lib/everywhere/asset_catalog.rb +185 -0
- data/lib/everywhere/auth_handoff.rb +404 -0
- data/lib/everywhere/auth_token.rb +118 -0
- data/lib/everywhere/builders/android.rb +844 -0
- data/lib/everywhere/builders/desktop.rb +326 -0
- data/lib/everywhere/builders/ios.rb +40 -3
- data/lib/everywhere/child_processes.rb +74 -0
- data/lib/everywhere/commands/build.rb +95 -14
- data/lib/everywhere/commands/clean.rb +10 -5
- data/lib/everywhere/commands/dev.rb +462 -57
- data/lib/everywhere/commands/doctor.rb +89 -3
- data/lib/everywhere/commands/install.rb +21 -1
- data/lib/everywhere/commands/logs.rb +117 -8
- data/lib/everywhere/commands/release.rb +2 -2
- data/lib/everywhere/config.rb +703 -13
- data/lib/everywhere/console.rb +117 -0
- data/lib/everywhere/desktop_assets.rb +150 -0
- data/lib/everywhere/dock/footer.rb +150 -0
- data/lib/everywhere/dock/screen.rb +114 -0
- data/lib/everywhere/dock/state.rb +59 -0
- data/lib/everywhere/dock.rb +238 -0
- data/lib/everywhere/emulator.rb +405 -0
- data/lib/everywhere/engine.rb +12 -0
- data/lib/everywhere/fatal.rb +20 -0
- data/lib/everywhere/icon.rb +196 -0
- data/lib/everywhere/line_pump.rb +89 -0
- data/lib/everywhere/log_filter.rb +111 -0
- data/lib/everywhere/native_helper.rb +39 -9
- data/lib/everywhere/omniauth.rb +88 -0
- data/lib/everywhere/paths.rb +109 -11
- data/lib/everywhere/relay.rb +77 -0
- data/lib/everywhere/shellout.rb +90 -15
- data/lib/everywhere/task_pool.rb +123 -0
- data/lib/everywhere/ui.rb +54 -11
- data/lib/everywhere/version.rb +1 -1
- data/support/desktop/README.md +121 -0
- data/support/{shell → desktop}/src-tauri/Cargo.lock +23 -0
- data/support/{shell → desktop}/src-tauri/Cargo.toml +19 -1
- data/support/{shell → desktop}/src-tauri/capabilities/default.json +7 -0
- data/support/desktop/src-tauri/gen/schemas/capabilities.json +1 -0
- data/support/desktop/src-tauri/src/extension_host.rs +53 -0
- data/support/desktop/src-tauri/src/extensions/mod.rs +39 -0
- data/support/{shell → desktop}/src-tauri/src/main.rs +355 -10
- data/support/{shell → desktop}/src-tauri/tauri.conf.json +2 -2
- data/support/mobile/android/README.md +217 -0
- data/support/mobile/android/app/build.gradle.kts +156 -0
- data/support/mobile/android/app/everywhere.properties +8 -0
- data/support/mobile/android/app/native-packages.gradle.kts +16 -0
- data/support/mobile/android/app/proguard-rules.pro +21 -0
- data/support/mobile/android/app/src/debug/res/xml/network_security_config.xml +30 -0
- data/support/mobile/android/app/src/main/AndroidManifest.xml +42 -0
- data/support/mobile/android/app/src/main/assets/everywhere.json +9 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.codepoints +2235 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.ttf +0 -0
- data/support/mobile/android/app/src/main/assets/json/path-configuration.json +19 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/AuthFlow.kt +268 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereApplication.kt +178 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereConfig.kt +409 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereEvents.kt +68 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereHost.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/IconFont.kt +211 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainActivity.kt +975 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainTabs.kt +201 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebBottomSheetFragment.kt +13 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebFragment.kt +36 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/BiometricsComponent.kt +535 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/HapticsComponent.kt +141 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/MenuComponent.kt +361 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/NotificationComponent.kt +151 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/PermissionsComponent.kt +271 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/StorageComponent.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/WebControlChannel.kt +204 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/extensions/EverywhereExtensions.kt +51 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_active_indicator.xml +4 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_item.xml +11 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +26 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_tab_placeholder.xml +28 -0
- data/support/mobile/android/app/src/main/res/layout/activity_main.xml +116 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet.xml +83 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet.xml +48 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +12 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- data/support/mobile/android/app/src/main/res/values/colors.xml +21 -0
- data/support/mobile/android/app/src/main/res/values/ic_launcher_background.xml +5 -0
- data/support/mobile/android/app/src/main/res/values/strings.xml +18 -0
- data/support/mobile/android/app/src/main/res/values/styles.xml +32 -0
- data/support/mobile/android/app/src/main/res/values/themes.xml +53 -0
- data/support/mobile/android/app/src/main/res/values-night/colors.xml +11 -0
- data/support/mobile/android/app/src/main/res/values-night/themes.xml +33 -0
- data/support/mobile/android/app/src/main/res/xml/network_security_config.xml +12 -0
- data/support/mobile/android/app/src/stamped/AndroidManifest.xml +34 -0
- data/support/mobile/android/build.gradle.kts +8 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.properties +10 -0
- data/support/mobile/android/gradle.properties +14 -0
- data/support/mobile/android/gradlew +251 -0
- data/support/mobile/android/gradlew.bat +94 -0
- data/support/mobile/android/settings.gradle.kts +28 -0
- data/support/mobile/ios/App/AppDelegate.swift +5 -0
- data/support/mobile/ios/App/AuthFlow.swift +101 -0
- data/support/mobile/ios/App/EverywhereConfig.swift +84 -0
- data/support/mobile/ios/App/SceneDelegate.swift +103 -0
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +4 -0
- data/support/mobile/ios/README.md +4 -1
- data/support/{macos → release/macos}/notarize.sh +2 -2
- metadata +97 -17
- data/support/github/build.yml +0 -85
- data/support/shell/src-tauri/gen/schemas/capabilities.json +0 -1
- /data/support/{shell → desktop}/splash/index.html +0 -0
- /data/support/{shell → desktop}/src-tauri/build.rs +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/acl-manifests.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/desktop-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/macOS-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/icons/icon.png +0 -0
- /data/support/{shell → desktop}/src-tauri/src/updater.rs +0 -0
- /data/support/{macos → release/macos}/entitlements.plist +0 -0
|
@@ -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
|
|
@@ -5,6 +5,7 @@ require_relative "../paths"
|
|
|
5
5
|
require_relative "../shellout"
|
|
6
6
|
require_relative "../log_filter"
|
|
7
7
|
require_relative "../icon"
|
|
8
|
+
require_relative "../asset_catalog"
|
|
8
9
|
require_relative "../ui"
|
|
9
10
|
|
|
10
11
|
module Everywhere
|
|
@@ -72,9 +73,9 @@ module Everywhere
|
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
# The whole per-app surface: one xcconfig (identity), two JSON resources
|
|
75
|
-
# (runtime config + path configuration with baked tabs),
|
|
76
|
-
# and the Extensions/ buildable folder (app-authored
|
|
77
|
-
# registry). The pbxproj is never touched.
|
|
76
|
+
# (runtime config + path configuration with baked tabs), the asset catalog
|
|
77
|
+
# (icon + app images), and the Extensions/ buildable folder (app-authored
|
|
78
|
+
# Swift + generated registry). The pbxproj is never touched.
|
|
78
79
|
def stamp!(work)
|
|
79
80
|
UI.step("stamping shell #{UI.dim("(#{bundle_id} → #{UI.short_path(work)})")}")
|
|
80
81
|
write_xcconfig(work)
|
|
@@ -86,8 +87,10 @@ module Everywhere
|
|
|
86
87
|
@config.path_configuration_json("ios"))
|
|
87
88
|
write_launch_background(work)
|
|
88
89
|
write_usage_strings(work)
|
|
90
|
+
write_url_scheme(work)
|
|
89
91
|
write_entitlements(work)
|
|
90
92
|
write_app_icon(work)
|
|
93
|
+
write_asset_catalog(work)
|
|
91
94
|
write_extensions(work)
|
|
92
95
|
write_native_packages(work)
|
|
93
96
|
end
|
|
@@ -291,6 +294,25 @@ module Everywhere
|
|
|
291
294
|
end
|
|
292
295
|
end
|
|
293
296
|
|
|
297
|
+
# The custom URL scheme third-party sign-in returns through
|
|
298
|
+
# (everywhere.yml auth:) → Info.plist. ASWebAuthenticationSession claims
|
|
299
|
+
# the callback for itself while it's running, so this registration isn't
|
|
300
|
+
# what makes the flow work — it's what stops another app from quietly
|
|
301
|
+
# owning the scheme, and what makes `xcrun simctl openurl` usable for
|
|
302
|
+
# testing the return leg by hand.
|
|
303
|
+
def write_url_scheme(work)
|
|
304
|
+
errors = @config.auth_errors
|
|
305
|
+
UI.die!("auth in config/everywhere.yml:\n#{errors.join("\n")}") unless errors.empty?
|
|
306
|
+
return unless @config.oauth?
|
|
307
|
+
|
|
308
|
+
require "json"
|
|
309
|
+
scheme = @config.auth_scheme
|
|
310
|
+
UI.step("stamping sign-in callback scheme #{UI.dim("(#{scheme}://)")}")
|
|
311
|
+
Shellout.run!({}, "plutil", "-replace", "CFBundleURLTypes", "-json",
|
|
312
|
+
JSON.generate([{ "CFBundleURLName" => bundle_id, "CFBundleURLSchemes" => [scheme] }]),
|
|
313
|
+
File.join(work, "App", "Info.plist"))
|
|
314
|
+
end
|
|
315
|
+
|
|
294
316
|
# The launch-screen (and splash overlay) background — the storyboard's
|
|
295
317
|
# LaunchBackground named color, stamped from appearance.background_color
|
|
296
318
|
# so the launch frame matches the app instead of the template default.
|
|
@@ -357,6 +379,21 @@ module Everywhere
|
|
|
357
379
|
UI.warn "couldn't read #{File.basename(source)} — shipping the placeholder icon"
|
|
358
380
|
end
|
|
359
381
|
|
|
382
|
+
# App-supplied images: native/ios/assets/** → imagesets in the template's
|
|
383
|
+
# Assets.xcassets, so native/ios Swift code reaches them by filename with
|
|
384
|
+
# Image("logo") / UIImage(named: "logo").
|
|
385
|
+
def write_asset_catalog(work)
|
|
386
|
+
source = File.join(@root, "native", "ios", "assets")
|
|
387
|
+
return unless File.directory?(source)
|
|
388
|
+
|
|
389
|
+
names, errors = AssetCatalog.write!(source, File.join(work, "App", "Assets.xcassets"))
|
|
390
|
+
UI.die!("native/ios/assets:\n#{errors.join("\n")}") unless errors.empty?
|
|
391
|
+
return if names.empty?
|
|
392
|
+
|
|
393
|
+
listed = names.length > 6 ? "#{names.take(6).join(", ")}, …" : names.join(", ")
|
|
394
|
+
UI.step("bundling image assets #{UI.dim("(#{names.length} from native/ios/assets: #{listed})")}")
|
|
395
|
+
end
|
|
396
|
+
|
|
360
397
|
def xcodebuild!(work, configuration, dist_dir)
|
|
361
398
|
UI.step("building iOS shell #{UI.dim("(xcodebuild, #{configuration}, first run compiles HotwireNative)")}")
|
|
362
399
|
FileUtils.mkdir_p(dist_dir)
|
|
@@ -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,7 +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"
|
|
14
|
+
require_relative "../builders/android"
|
|
15
|
+
require_relative "../builders/desktop"
|
|
13
16
|
|
|
14
17
|
module Everywhere
|
|
15
18
|
module Commands
|
|
@@ -24,34 +27,57 @@ module Everywhere
|
|
|
24
27
|
class Build < Dry::CLI::Command
|
|
25
28
|
SHIM = File.expand_path("~/.tebako-shims/clang++")
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
DEFAULT_TARGET = "macos-arm64"
|
|
31
|
+
IOS_TARGET = "ios-arm64"
|
|
32
|
+
# The APK/AAB is universal, so the arch half is nominal — it exists only
|
|
33
|
+
# so every target reads as os-arch.
|
|
34
|
+
ANDROID_TARGET = "android-arm64"
|
|
35
|
+
|
|
36
|
+
# The --ios/--android shorthands and the os they stand for, so adding a
|
|
37
|
+
# mobile platform is one entry rather than a third copy of resolve_target.
|
|
38
|
+
SHORTHAND_TARGETS = { ios: IOS_TARGET, android: ANDROID_TARGET }.freeze
|
|
39
|
+
|
|
40
|
+
desc "Package the app natively — desktop (tebako press + shell + .app), iOS (--ios) or Android (--android)"
|
|
28
41
|
|
|
29
42
|
option :root, default: ".", desc: "App root directory"
|
|
30
43
|
option :output, default: nil, desc: "Output binary path (default: dist/<app>)"
|
|
31
44
|
option :ruby, default: "4.0.6", desc: "Ruby version to package"
|
|
32
45
|
option :entry, default: "native_boot.rb", desc: "Entry point script relative to root"
|
|
33
46
|
option :app_name, default: nil, desc: "Bundle name (default: capitalized app dir)"
|
|
34
|
-
option :target, default:
|
|
47
|
+
option :target, default: nil, desc: "Build target (os-arch) — selects any platforms: overrides, default: \"#{DEFAULT_TARGET}\""
|
|
35
48
|
option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
|
|
49
|
+
option :ios, type: :boolean, default: false, desc: "Build the iOS shell (shorthand for --target #{IOS_TARGET})"
|
|
50
|
+
option :android, type: :boolean, default: false, desc: "Build the Android shell (shorthand for --target #{ANDROID_TARGET})"
|
|
36
51
|
option :skip_press, type: :boolean, default: false, desc: "Reuse the existing pressed binary, only rebuild the .app"
|
|
37
52
|
|
|
38
53
|
def call(root: ".", output: nil, ruby: "4.0.6", entry: "native_boot.rb",
|
|
39
|
-
app_name: nil, target:
|
|
54
|
+
app_name: nil, target: nil, shell_dir: nil, ios: false, android: false, skip_press: false, **)
|
|
55
|
+
target = resolve_target(target, ios: ios, android: android)
|
|
40
56
|
root_path = File.expand_path(root)
|
|
41
57
|
config = Everywhere::Config.load(root_path)
|
|
42
58
|
|
|
43
59
|
# Builder dispatch on the target's os (build-engine.md §5): macOS is
|
|
44
|
-
# the tebako+tauri path below;
|
|
45
|
-
# its own builder. Anything else has no builder yet.
|
|
60
|
+
# the tebako+tauri path below; the mobile targets are Hotwire Native
|
|
61
|
+
# shells, each built by its own builder. Anything else has no builder yet.
|
|
46
62
|
case Everywhere::Config.os_of(target)
|
|
47
|
-
when "ios"
|
|
48
|
-
app =
|
|
49
|
-
|
|
63
|
+
when "ios", "android"
|
|
64
|
+
app = mobile_builder(target).new(config: config, root: root_path, target: target)
|
|
65
|
+
.build!(dist_dir: File.expand_path("dist", root_path))
|
|
50
66
|
UI.success("built #{rel(app, root_path)}")
|
|
51
67
|
return
|
|
52
68
|
when "macos" # the rest of this method
|
|
53
69
|
else
|
|
54
|
-
UI.die!("no builder for target #{target} — macos-* and
|
|
70
|
+
UI.die!("no builder for target #{target} — macos-*, ios-* and android-* are supported")
|
|
71
|
+
end
|
|
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")}")
|
|
55
81
|
end
|
|
56
82
|
|
|
57
83
|
# The os of `target` selects any per-platform overrides (bundle id,
|
|
@@ -95,6 +121,34 @@ module Everywhere
|
|
|
95
121
|
|
|
96
122
|
private
|
|
97
123
|
|
|
124
|
+
# `--ios` is the shorthand `every dev` already has; --target stays the
|
|
125
|
+
# general escape hatch. Both given is only an error when they disagree —
|
|
126
|
+
# `--ios --target ios-arm64` is redundant, not wrong.
|
|
127
|
+
def mobile_builder(target)
|
|
128
|
+
Everywhere::Config.os_of(target) == "android" ? Builders::Android : Builders::Ios
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# --ios/--android are shorthand for their target, and disagreeing with an
|
|
132
|
+
# explicit --target is a mistake worth naming rather than silently
|
|
133
|
+
# resolving one way: both spellings were deliberate, so we can't know
|
|
134
|
+
# which one the user meant.
|
|
135
|
+
def resolve_target(target, ios:, android:)
|
|
136
|
+
asked = { ios: ios, android: android }.select { |_flag, given| given }.keys
|
|
137
|
+
if asked.length > 1
|
|
138
|
+
UI.die!("#{asked.map { |f| "--#{f}" }.join(" and ")} are separate builds — run one at a time")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
flag = asked.first or return target || DEFAULT_TARGET
|
|
142
|
+
shorthand = SHORTHAND_TARGETS.fetch(flag)
|
|
143
|
+
return shorthand if target.nil?
|
|
144
|
+
|
|
145
|
+
unless Everywhere::Config.os_of(target) == flag.to_s
|
|
146
|
+
UI.die!("--#{flag} and --target #{target} disagree — drop one " \
|
|
147
|
+
"(--#{flag} means --target #{shorthand})")
|
|
148
|
+
end
|
|
149
|
+
target
|
|
150
|
+
end
|
|
151
|
+
|
|
98
152
|
def rel(path, root_path) = UI.short_path(path.sub("#{root_path}/", "").sub("#{Dir.pwd}/", ""))
|
|
99
153
|
|
|
100
154
|
# The precompile step runs against the HOST Ruby — unlike tebako press,
|
|
@@ -111,7 +165,7 @@ module Everywhere
|
|
|
111
165
|
# EVERY_BUNDLE_PATH (set by `every platform runner`) redirects the app's
|
|
112
166
|
# gems to a stable per-user dir so CI can cache them across builds. It
|
|
113
167
|
# must be merged EXPLICITLY into each host-side bundler command:
|
|
114
|
-
# Shellout.
|
|
168
|
+
# Shellout.child_env strips inherited BUNDLE_* vars, so an inherited
|
|
115
169
|
# BUNDLE_PATH would never survive to the child process.
|
|
116
170
|
def bundle_env
|
|
117
171
|
path = ENV["EVERY_BUNDLE_PATH"]
|
|
@@ -162,7 +216,12 @@ module Everywhere
|
|
|
162
216
|
# Rails binary (as "app-server") + Info.plist + icon. An .app bundle is
|
|
163
217
|
# just a directory layout, so we build it by hand — no tauri-cli needed.
|
|
164
218
|
def bundle_app(server_binary, app_name, shell_dir, config, target: "macos-arm64", dist_dir: nil)
|
|
165
|
-
|
|
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
|
|
166
225
|
dist_dir ||= File.dirname(server_binary)
|
|
167
226
|
|
|
168
227
|
icon_source = config.icon
|
|
@@ -193,6 +252,12 @@ module Everywhere
|
|
|
193
252
|
# shell must be COMPILED with the app's icon. Swap in the shaped master
|
|
194
253
|
# for the build, restore the placeholder after; touch build.rs so
|
|
195
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.
|
|
196
261
|
shell_icon = File.join(shell_dir, "icons", "icon.png")
|
|
197
262
|
placeholder_backup = "#{shell_icon}.placeholder"
|
|
198
263
|
if icon_source
|
|
@@ -212,12 +277,12 @@ module Everywhere
|
|
|
212
277
|
FileUtils.touch(File.join(shell_dir, "build.rs"))
|
|
213
278
|
end
|
|
214
279
|
|
|
215
|
-
UI.step("building shell #{UI.dim("(cargo release)")}")
|
|
216
|
-
target_dir =
|
|
280
|
+
UI.step("building shell #{UI.dim(prepared.stamped? ? "(cargo release, per-app)" : "(cargo release)")}")
|
|
281
|
+
target_dir = prepared.target_dir
|
|
217
282
|
begin
|
|
218
283
|
Shellout.run!({ "CARGO_TARGET_DIR" => target_dir }, "cargo", "build", "--release", chdir: shell_dir)
|
|
219
284
|
ensure
|
|
220
|
-
if icon_source && File.exist?(placeholder_backup)
|
|
285
|
+
if !prepared.stamped? && icon_source && File.exist?(placeholder_backup)
|
|
221
286
|
FileUtils.mv(placeholder_backup, shell_icon)
|
|
222
287
|
FileUtils.touch(File.join(shell_dir, "build.rs"))
|
|
223
288
|
end
|
|
@@ -238,6 +303,7 @@ module Everywhere
|
|
|
238
303
|
FileUtils.cp(server_binary, File.join(macos, "app-server")) if server_binary
|
|
239
304
|
|
|
240
305
|
File.write(File.join(resources, "everywhere.json"), config.to_shell_json(target: target))
|
|
306
|
+
write_desktop_assets(config, resources)
|
|
241
307
|
if (splash = config.splash)
|
|
242
308
|
if File.exist?(splash)
|
|
243
309
|
FileUtils.cp(splash, File.join(resources, "splash.html"))
|
|
@@ -264,6 +330,21 @@ module Everywhere
|
|
|
264
330
|
FileUtils.remove_entry(work) if work && File.directory?(work)
|
|
265
331
|
end
|
|
266
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
|
+
|
|
267
348
|
# Write Resources/AppIcon.icns from a (shaped) PNG. Pure Ruby — no sips or
|
|
268
349
|
# iconutil — so it produces the same icns on any build host.
|
|
269
350
|
def make_icns(png, resources_dir)
|