ruby_everywhere 0.7.0 → 0.8.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/exe/every +2 -13
- data/exe/rbe +2 -13
- data/lib/everywhere/boot.rb +9 -2
- data/lib/everywhere/builders/android.rb +16 -42
- data/lib/everywhere/builders/base.rb +53 -0
- data/lib/everywhere/builders/desktop.rb +7 -23
- data/lib/everywhere/builders/ios.rb +49 -52
- data/lib/everywhere/builders/native_sources.rb +38 -0
- data/lib/everywhere/child_processes.rb +4 -4
- data/lib/everywhere/child_supervision.rb +172 -0
- data/lib/everywhere/clock.rb +12 -0
- data/lib/everywhere/commands/build.rb +20 -9
- data/lib/everywhere/commands/dev.rb +59 -284
- data/lib/everywhere/commands/doctor.rb +6 -6
- data/lib/everywhere/commands/install.rb +1 -0
- data/lib/everywhere/commands/platform/auth_status.rb +1 -0
- data/lib/everywhere/commands/platform/build.rb +5 -6
- data/lib/everywhere/commands/platform/login.rb +4 -4
- data/lib/everywhere/commands/platform/logout.rb +1 -0
- data/lib/everywhere/commands/platform/runner.rb +62 -7
- data/lib/everywhere/commands/preview.rb +24 -97
- data/lib/everywhere/commands/publish.rb +2 -0
- data/lib/everywhere/commands/release.rb +14 -11
- data/lib/everywhere/commands/shell_dir.rb +2 -0
- data/lib/everywhere/commands/updates_keygen.rb +25 -1
- data/lib/everywhere/config/app.rb +126 -0
- data/lib/everywhere/config/auth.rb +108 -0
- data/lib/everywhere/config/data.rb +50 -0
- data/lib/everywhere/config/deep_linking.rb +107 -0
- data/lib/everywhere/config/desktop_ui.rb +153 -0
- data/lib/everywhere/config/mobile.rb +211 -0
- data/lib/everywhere/config/native_desktop.rb +168 -0
- data/lib/everywhere/config/native_mobile.rb +337 -0
- data/lib/everywhere/config/shell.rb +57 -0
- data/lib/everywhere/config/updates.rb +63 -0
- data/lib/everywhere/config.rb +30 -1423
- data/lib/everywhere/desktop_dev_app.rb +138 -0
- data/lib/everywhere/dock/state.rb +3 -1
- data/lib/everywhere/entrypoint.rb +24 -0
- data/lib/everywhere/error.rb +8 -0
- data/lib/everywhere/framework.rb +2 -2
- data/lib/everywhere/host.rb +11 -0
- data/lib/everywhere/ignore.rb +11 -5
- data/lib/everywhere/line_pump.rb +3 -1
- data/lib/everywhere/minisign.rb +1 -0
- data/lib/everywhere/native_platform.rb +44 -0
- data/lib/everywhere/paths.rb +23 -7
- data/lib/everywhere/platform/client.rb +19 -1
- data/lib/everywhere/platform/snapshot.rb +6 -4
- data/lib/everywhere/plist.rb +17 -0
- data/lib/everywhere/png.rb +1 -0
- data/lib/everywhere/raw_tty.rb +51 -0
- data/lib/everywhere/s3.rb +1 -0
- data/lib/everywhere/shell_pages.rb +109 -0
- data/lib/everywhere/tab_filter.rb +36 -0
- data/lib/everywhere/task_pool.rb +3 -4
- data/lib/everywhere/ui.rb +6 -1
- data/lib/everywhere/version.rb +6 -4
- data/lib/everywhere.rb +1 -2
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +0 -2
- data/support/release/macos/notarize.sh +3 -0
- metadata +24 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "native_mobile"
|
|
4
|
+
|
|
5
|
+
module Everywhere
|
|
6
|
+
class Config
|
|
7
|
+
module NativeDesktop
|
|
8
|
+
# The desktop half of "supernative": Rust the app repo carries in
|
|
9
|
+
# native/desktop/ that `every build` compiles into the Tauri shell.
|
|
10
|
+
#
|
|
11
|
+
# The shape differs from ios/android because desktop differs: the whole UI
|
|
12
|
+
# is a webview, so there are no native screens or splash views to register.
|
|
13
|
+
# What an app actually wants down here is the machine — a serial port, a USB
|
|
14
|
+
# device, an FFI library, a Tauri plugin — reached from the page.
|
|
15
|
+
#
|
|
16
|
+
# native:
|
|
17
|
+
# desktop:
|
|
18
|
+
# commands: [scan_ports] # fns reachable from the page
|
|
19
|
+
# setup: true # run native/desktop/setup.rs at boot
|
|
20
|
+
# crates:
|
|
21
|
+
# serialport: "4.3"
|
|
22
|
+
# reqwest: { version: "0.12", features: [json] }
|
|
23
|
+
#
|
|
24
|
+
# A command is a plain function, NOT a #[tauri::command] — Tauri's typed
|
|
25
|
+
# commands only arrive through invoke, and invoke is unavailable to our
|
|
26
|
+
# pages (they're served over http; see extension_host.rs in the shell). The
|
|
27
|
+
# signature is uniform:
|
|
28
|
+
#
|
|
29
|
+
# pub fn scan_ports(app: &tauri::AppHandle, payload: serde_json::Value)
|
|
30
|
+
# -> Result<serde_json::Value, String>
|
|
31
|
+
#
|
|
32
|
+
# Declaring anything here has a real cost the mobile sections don't have:
|
|
33
|
+
# the shell stops being one prebuilt crate shared by every app and gets
|
|
34
|
+
# stamped and compiled per app (see Builders::Desktop). Apps that declare
|
|
35
|
+
# nothing keep the fast path.
|
|
36
|
+
def native_desktop
|
|
37
|
+
section = @data.dig("native", "desktop")
|
|
38
|
+
section.is_a?(Hash) ? section : {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def native_desktop_commands = Array(native_desktop["commands"]).map { |name| name.to_s.strip }
|
|
42
|
+
|
|
43
|
+
def native_desktop_setup? = native_desktop["setup"] == true
|
|
44
|
+
|
|
45
|
+
# Cargo dependencies, normalized to { "name" => {...table} } so the
|
|
46
|
+
# generator has one shape to write. The short form (a bare version string)
|
|
47
|
+
# becomes { "version" => "4.3" }.
|
|
48
|
+
def native_desktop_crates
|
|
49
|
+
entries = native_desktop["crates"]
|
|
50
|
+
return {} unless entries.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
entries.to_h do |name, spec|
|
|
53
|
+
table = spec.is_a?(Hash) ? spec : { "version" => spec.to_s }
|
|
54
|
+
[name.to_s.strip, table]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def native_desktop? = !(native_desktop_commands.empty? && native_desktop_crates.empty?) ||
|
|
59
|
+
native_desktop_setup?
|
|
60
|
+
|
|
61
|
+
# Everything here is interpolated into generated Rust and TOML, so names are
|
|
62
|
+
# validated hard — same reasoning as MAVEN_COORDINATE above. A command name
|
|
63
|
+
# becomes a path segment in a generate_handler! list; a crate name and its
|
|
64
|
+
# features become bare TOML keys and quoted strings.
|
|
65
|
+
RUST_FN_NAME = /\A[a-z_][a-z0-9_]*\z/
|
|
66
|
+
CRATE_NAME = /\A[a-zA-Z0-9_][a-zA-Z0-9_-]*\z/
|
|
67
|
+
# Cargo's requirement syntax, loosely: ^1.2, ~1.2, >=1.0, 1.*, 1.2.3-beta.1.
|
|
68
|
+
# Deliberately no spaces and no commas — a multi-requirement string would
|
|
69
|
+
# need quoting rules we don't want to own, and cargo reports anything
|
|
70
|
+
# subtler at build time.
|
|
71
|
+
CRATE_VERSION = /\A[\^~<>=]{0,2}\d+(\.\d+){0,2}(\.\*)?(-[0-9A-Za-z.-]+)?\z/
|
|
72
|
+
CRATE_FEATURE = /\A[A-Za-z0-9_][A-Za-z0-9_+-]*\z/
|
|
73
|
+
# Where a git dependency may point. Same rule the SPM packages use —
|
|
74
|
+
# named through NativeMobile because a sibling section module's constants
|
|
75
|
+
# aren't in scope here the way they were when both lived in Config.
|
|
76
|
+
CRATE_GIT_URL = NativeMobile::PACKAGE_URL
|
|
77
|
+
|
|
78
|
+
def native_desktop_errors
|
|
79
|
+
raw = @data.dig("native", "desktop")
|
|
80
|
+
return [] if raw.nil?
|
|
81
|
+
return ["native.desktop must be a mapping"] unless raw.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
errors = []
|
|
84
|
+
commands = raw["commands"]
|
|
85
|
+
if !commands.nil? && !commands.is_a?(Array)
|
|
86
|
+
errors << "native.desktop.commands must be a list of function names"
|
|
87
|
+
else
|
|
88
|
+
errors += native_desktop_commands.reject { |name| name.match?(RUST_FN_NAME) }.map do |name|
|
|
89
|
+
"native.desktop.commands: #{name.inspect} is not a Rust function name — " \
|
|
90
|
+
"snake_case (letters, digits, _), matching the fn in native/desktop/"
|
|
91
|
+
end
|
|
92
|
+
duplicates = native_desktop_commands.tally.select { |_name, count| count > 1 }.keys
|
|
93
|
+
errors += duplicates.map do |name|
|
|
94
|
+
"native.desktop.commands: #{name.inspect} is listed twice"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
setup = raw["setup"]
|
|
99
|
+
errors << "native.desktop.setup must be true or false" unless setup.nil? || boolean_or_nil(setup) == setup
|
|
100
|
+
|
|
101
|
+
errors + native_desktop_crate_errors
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def native_desktop_crate_errors
|
|
105
|
+
raw = native_desktop["crates"]
|
|
106
|
+
return [] if raw.nil?
|
|
107
|
+
return ["native.desktop.crates must be a mapping of crate name to version"] unless raw.is_a?(Hash)
|
|
108
|
+
|
|
109
|
+
raw.flat_map do |name, spec|
|
|
110
|
+
at = "native.desktop.crates.#{name}"
|
|
111
|
+
errors = []
|
|
112
|
+
unless name.to_s.strip.match?(CRATE_NAME)
|
|
113
|
+
errors << "#{at}: #{name.to_s.inspect} is not a crate name (letters, digits, _, -)"
|
|
114
|
+
end
|
|
115
|
+
errors + crate_spec_errors(at, spec)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# One crate's dependency spec: either a bare version string or a table.
|
|
122
|
+
def crate_spec_errors(at, spec)
|
|
123
|
+
return version_errors(at, spec) unless spec.is_a?(Hash)
|
|
124
|
+
|
|
125
|
+
errors = []
|
|
126
|
+
# A crate needs somewhere to come from. version: is the common case; git:
|
|
127
|
+
# and path: are the two escape hatches cargo itself offers.
|
|
128
|
+
unless %w[version git path].any? { |key| !spec[key].to_s.strip.empty? }
|
|
129
|
+
errors << "#{at} needs a version: (or git: / path:)"
|
|
130
|
+
end
|
|
131
|
+
errors += version_errors("#{at}.version", spec["version"]) if spec.key?("version")
|
|
132
|
+
|
|
133
|
+
git = spec["git"].to_s.strip
|
|
134
|
+
unless git.empty? || git.match?(CRATE_GIT_URL)
|
|
135
|
+
errors << "#{at}.git #{git.inspect} must be an https:// or git@ URL"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# branch/tag/rev/path have no useful shape to check, but they DO end up
|
|
139
|
+
# inside quoted TOML strings — so the one thing that matters is that they
|
|
140
|
+
# can't close the quote or start a new line.
|
|
141
|
+
errors += %w[branch tag rev path].filter_map do |key|
|
|
142
|
+
value = spec[key].to_s
|
|
143
|
+
next if value.empty? || !value.match?(/["\\\n\r]/)
|
|
144
|
+
|
|
145
|
+
"#{at}.#{key} can't contain quotes, backslashes or newlines"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
features = spec["features"]
|
|
149
|
+
if !features.nil? && !features.is_a?(Array)
|
|
150
|
+
errors << "#{at}.features must be a list"
|
|
151
|
+
else
|
|
152
|
+
errors += Array(features).reject { |f| f.to_s.match?(CRATE_FEATURE) }.map do |feature|
|
|
153
|
+
"#{at}.features: #{feature.to_s.inspect} is not a feature name (letters, digits, _, +, -)"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
errors
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def version_errors(at, value)
|
|
160
|
+
version = value.to_s.strip
|
|
161
|
+
return [] if version.empty?
|
|
162
|
+
return [] if version.match?(CRATE_VERSION)
|
|
163
|
+
|
|
164
|
+
["#{at} #{version.inspect} is not a cargo version requirement — like \"1.2\", \"^1.2.3\" or \"~0.4\""]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
class Config
|
|
5
|
+
module NativeMobile
|
|
6
|
+
# Native code extensions ("supernative"): Swift the app repo carries in
|
|
7
|
+
# native/ios/ that `every build` compiles into the shell, plus this
|
|
8
|
+
# declaration of what to hook up. Build-time only — stores forbid
|
|
9
|
+
# downloading native code, so extensions ship with the app binary.
|
|
10
|
+
#
|
|
11
|
+
# native:
|
|
12
|
+
# ios:
|
|
13
|
+
# components: [ChartComponent] # BridgeComponent subclasses to register
|
|
14
|
+
# screens: # path rules with view_controller: <id>
|
|
15
|
+
# map: MapScreen # SwiftUI View or UIViewController, init(url:)
|
|
16
|
+
# splash: LaunchSplash # SwiftUI View or UIViewController, init()
|
|
17
|
+
# splash_min_seconds: 1.0 # how long the custom splash stays up at minimum
|
|
18
|
+
# lazy_load_tabs: true # defer each tab's first visit until it's selected
|
|
19
|
+
def native_ios = native_section("ios")
|
|
20
|
+
|
|
21
|
+
def native_ios_components = native_components("ios")
|
|
22
|
+
|
|
23
|
+
def native_ios_screens = native_screens("ios")
|
|
24
|
+
|
|
25
|
+
def native_ios_splash = native_splash("ios")
|
|
26
|
+
|
|
27
|
+
# Minimum seconds a custom splash stays on screen (default 1.0 shell-side;
|
|
28
|
+
# a fast server would otherwise dismiss a branded splash in a ~50ms flash).
|
|
29
|
+
# Clamped to the shell's 8s give-up timeout.
|
|
30
|
+
def native_ios_splash_min_seconds = native_splash_seconds("ios")
|
|
31
|
+
|
|
32
|
+
# Whether the native tab bar defers each non-selected tab's initial visit
|
|
33
|
+
# until the tab is first selected (Hotwire Native `lazyLoadTabs`). Tri-state
|
|
34
|
+
# so apps can opt in or opt out explicitly: `true` lazy-loads, `false` loads
|
|
35
|
+
# every tab up front, and an absent key leaves the shell on its default
|
|
36
|
+
# (eager).
|
|
37
|
+
def native_ios_lazy_load_tabs = native_lazy_load("ios")
|
|
38
|
+
|
|
39
|
+
# Third-party Swift packages this app pins for its native/ios code. Each
|
|
40
|
+
# entry is one SPM dependency; `every build` writes them into the shell's
|
|
41
|
+
# local NativeExtensions package and re-exports every product, so native/ios
|
|
42
|
+
# code reaches them with a single `import NativeExtensions`.
|
|
43
|
+
#
|
|
44
|
+
# packages:
|
|
45
|
+
# - url: https://github.com/simibac/ConfettiSwiftUI
|
|
46
|
+
# from: "1.1.0" # or exact: / branch: / revision:
|
|
47
|
+
# products: [ConfettiSwiftUI] # optional; defaults to the repo name
|
|
48
|
+
#
|
|
49
|
+
# Returns normalized entries: { "url", "requirement" => { "kind", "value" },
|
|
50
|
+
# "products" => [...] }. Assumes the config validated clean (see
|
|
51
|
+
# native_ios_package_errors) — the requirement is always present here.
|
|
52
|
+
PACKAGE_REQUIREMENT_KEYS = %w[from exact branch revision].freeze
|
|
53
|
+
|
|
54
|
+
def native_ios_packages
|
|
55
|
+
Array(native_ios["packages"]).filter_map do |entry|
|
|
56
|
+
next unless entry.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
url = entry["url"].to_s.strip
|
|
59
|
+
kind = PACKAGE_REQUIREMENT_KEYS.find { |k| entry.key?(k) && !entry[k].to_s.strip.empty? }
|
|
60
|
+
products = Array(entry["products"]).map { |p| p.to_s.strip }.reject(&:empty?)
|
|
61
|
+
products = [package_identity(url)] if products.empty?
|
|
62
|
+
|
|
63
|
+
{ "url" => url,
|
|
64
|
+
"requirement" => (kind && { "kind" => kind, "value" => entry[kind].to_s.strip }),
|
|
65
|
+
"products" => products }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def native_ios_packages? = !native_ios_packages.empty?
|
|
70
|
+
|
|
71
|
+
# SPM's package identity: the URL's last path segment without a .git suffix
|
|
72
|
+
# (github.com/simibac/ConfettiSwiftUI → "ConfettiSwiftUI"). Used as the
|
|
73
|
+
# default product name and as the `package:` key in .product(name:package:).
|
|
74
|
+
def package_identity(url) = File.basename(url.to_s.sub(%r{/+\z}, "")).sub(/\.git\z/, "")
|
|
75
|
+
|
|
76
|
+
def native_ios? = native_declared?("ios")
|
|
77
|
+
|
|
78
|
+
# Everything declared here is interpolated into generated Swift, so names
|
|
79
|
+
# are validated hard: type names must be plain Swift identifiers, screen
|
|
80
|
+
# identifiers must be safe inside a Swift string literal.
|
|
81
|
+
SWIFT_TYPE_NAME = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
82
|
+
SCREEN_IDENTIFIER = /\A[A-Za-z0-9_-]+\z/
|
|
83
|
+
|
|
84
|
+
def native_ios_errors = native_type_errors("ios") + native_ios_package_errors
|
|
85
|
+
|
|
86
|
+
# A URL SPM accepts (https for the common case, git@ for SSH remotes) and a
|
|
87
|
+
# loose semver for from:/exact: — enough to catch typos without reimplementing
|
|
88
|
+
# SPM's resolver, which reports anything subtler at build time.
|
|
89
|
+
PACKAGE_URL = %r{\A(https?://|git@).+}
|
|
90
|
+
PACKAGE_VERSION = /\A\d+(\.\d+){0,2}(-[0-9A-Za-z.-]+)?\z/
|
|
91
|
+
|
|
92
|
+
def native_ios_package_errors
|
|
93
|
+
raw = native_ios["packages"]
|
|
94
|
+
return [] if raw.nil?
|
|
95
|
+
return ["native.ios.packages must be a list"] unless raw.is_a?(Array)
|
|
96
|
+
|
|
97
|
+
raw.each_with_index.flat_map do |entry, i|
|
|
98
|
+
at = "native.ios.packages[#{i}]"
|
|
99
|
+
next ["#{at} must be a mapping with a url:"] unless entry.is_a?(Hash)
|
|
100
|
+
|
|
101
|
+
errors = []
|
|
102
|
+
url = entry["url"].to_s.strip
|
|
103
|
+
if url.empty?
|
|
104
|
+
errors << "#{at}.url is required"
|
|
105
|
+
elsif !url.match?(PACKAGE_URL)
|
|
106
|
+
errors << "#{at}.url #{url.inspect} must be an https:// or git@ URL"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
present = PACKAGE_REQUIREMENT_KEYS.select { |k| entry.key?(k) && !entry[k].to_s.strip.empty? }
|
|
110
|
+
if present.empty?
|
|
111
|
+
errors << "#{at} needs one version requirement (#{PACKAGE_REQUIREMENT_KEYS.join(" / ")})"
|
|
112
|
+
elsif present.length > 1
|
|
113
|
+
errors << "#{at} sets conflicting requirements (#{present.join(", ")}) — pick one"
|
|
114
|
+
elsif %w[from exact].include?(present.first) && !entry[present.first].to_s.strip.match?(PACKAGE_VERSION)
|
|
115
|
+
errors << "#{at}.#{present.first} #{entry[present.first].to_s.inspect} must be a version like \"1.2.0\""
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Array(entry["products"]).each do |product|
|
|
119
|
+
next if product.to_s.match?(SWIFT_TYPE_NAME)
|
|
120
|
+
|
|
121
|
+
errors << "#{at}.products: #{product.to_s.inspect} is not a module name (letters, digits, _)"
|
|
122
|
+
end
|
|
123
|
+
errors
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The Android half of "supernative": Kotlin the app repo carries in
|
|
128
|
+
# native/android/ that `every build --android` compiles into the shell,
|
|
129
|
+
# declared the same way iOS declares Swift. Same build-time-only rule —
|
|
130
|
+
# Play forbids downloading executable code — and the same shape, so an app
|
|
131
|
+
# that already knows `native.ios:` knows this section too.
|
|
132
|
+
#
|
|
133
|
+
# native:
|
|
134
|
+
# android:
|
|
135
|
+
# components: [ChartComponent] # BridgeComponent subclasses to register
|
|
136
|
+
# screens: # path rules with view_controller: <id>
|
|
137
|
+
# map: MapFragment # Fragment with an (url) argument
|
|
138
|
+
# splash: LaunchSplash # Fragment/Activity shown while booting
|
|
139
|
+
# splash_min_seconds: 1.0 # how long that splash stays up at minimum
|
|
140
|
+
# lazy_load_tabs: true # defer each tab's first visit until selected
|
|
141
|
+
# icon_font: symbols # which Material icon font resolves icon names
|
|
142
|
+
# packages: # Maven coordinates, not SPM entries
|
|
143
|
+
# - "com.airbnb.android:lottie:6.4.0"
|
|
144
|
+
def native_android = native_section("android")
|
|
145
|
+
|
|
146
|
+
def native_android_components = native_components("android")
|
|
147
|
+
|
|
148
|
+
def native_android_screens = native_screens("android")
|
|
149
|
+
|
|
150
|
+
def native_android_splash = native_splash("android")
|
|
151
|
+
|
|
152
|
+
# Same contract as the iOS splash minimum, and the same clamp: the shell
|
|
153
|
+
# gives up waiting at 8s either way, so a larger number here would only
|
|
154
|
+
# promise something the shell won't honor.
|
|
155
|
+
def native_android_splash_min_seconds = native_splash_seconds("android")
|
|
156
|
+
|
|
157
|
+
# Tri-state like native_ios_lazy_load_tabs: true defers each non-selected
|
|
158
|
+
# tab's first visit, false loads them all up front, absent leaves the shell
|
|
159
|
+
# on its default. Android's bottom-nav hosts are created eagerly at
|
|
160
|
+
# onCreate, so this decides whether they *visit*, not whether they exist.
|
|
161
|
+
def native_android_lazy_load_tabs = native_lazy_load("android")
|
|
162
|
+
|
|
163
|
+
# Third-party dependencies for native/android code, as Gradle sees them:
|
|
164
|
+
# plain Maven coordinates. There is no SPM-style url + requirement split —
|
|
165
|
+
# a coordinate already carries group, artifact and version — so this
|
|
166
|
+
# accessor returns strings, one per `implementation(…)` line the builder
|
|
167
|
+
# writes into native-packages.gradle.kts. Assumes the config validated
|
|
168
|
+
# clean (see native_android_package_errors).
|
|
169
|
+
def native_android_packages
|
|
170
|
+
Array(native_android["packages"]).filter_map do |entry|
|
|
171
|
+
next unless entry.is_a?(String)
|
|
172
|
+
|
|
173
|
+
coordinate = entry.strip
|
|
174
|
+
coordinate unless coordinate.empty?
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def native_android_packages? = !native_android_packages.empty?
|
|
179
|
+
|
|
180
|
+
def native_android? = native_declared?("android")
|
|
181
|
+
|
|
182
|
+
# Kotlin's identifier rules match Swift's across everything we generate
|
|
183
|
+
# (no backticked names, no unicode escapes), so one pattern validates both.
|
|
184
|
+
# Aliased rather than reused by name so the Android messages can say
|
|
185
|
+
# "Kotlin" and the two paths can diverge later without touching iOS.
|
|
186
|
+
KOTLIN_TYPE_NAME = SWIFT_TYPE_NAME
|
|
187
|
+
|
|
188
|
+
def native_android_errors
|
|
189
|
+
native_type_errors("android") + native_android_icon_font_errors + native_android_package_errors
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# group:artifact:version — Gradle's shorthand form, and the only one we
|
|
193
|
+
# accept. The charset is narrower than Maven strictly allows on purpose:
|
|
194
|
+
# every coordinate is interpolated into a generated Kotlin DSL file inside
|
|
195
|
+
# a double-quoted literal, so a quote, a backslash, a `$` (Kotlin string
|
|
196
|
+
# templates), a newline or a space would rewrite the build script rather
|
|
197
|
+
# than name a dependency. Version ranges ("[1.0,2.0)") and BOM-style
|
|
198
|
+
# two-part coordinates are rejected for the same reason and because Gradle
|
|
199
|
+
# resolves anything subtler at build time anyway.
|
|
200
|
+
MAVEN_SEGMENT = /[A-Za-z0-9_][A-Za-z0-9_.-]*/
|
|
201
|
+
MAVEN_COORDINATE = /\A#{MAVEN_SEGMENT}:#{MAVEN_SEGMENT}:[A-Za-z0-9_][A-Za-z0-9_.+-]*\z/
|
|
202
|
+
|
|
203
|
+
def native_android_package_errors
|
|
204
|
+
raw = native_android["packages"]
|
|
205
|
+
return [] if raw.nil?
|
|
206
|
+
return ["native.android.packages must be a list"] unless raw.is_a?(Array)
|
|
207
|
+
|
|
208
|
+
raw.each_with_index.filter_map do |entry, i|
|
|
209
|
+
at = "native.android.packages[#{i}]"
|
|
210
|
+
next "#{at} must be a Maven coordinate string like \"com.airbnb.android:lottie:6.4.0\"" unless entry.is_a?(String)
|
|
211
|
+
|
|
212
|
+
coordinate = entry.strip
|
|
213
|
+
next if coordinate.match?(MAVEN_COORDINATE)
|
|
214
|
+
|
|
215
|
+
"#{at} #{coordinate.inspect} is not a Maven coordinate — " \
|
|
216
|
+
"\"group:artifact:version\" (letters, digits, . _ -)"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Which Material icon font `icons.android` names — and the per-platform
|
|
221
|
+
# icon names nav buttons and menu items carry in page HTML — resolve
|
|
222
|
+
# against. Android has no UIImage(systemName:), so the shell draws the
|
|
223
|
+
# icon's codepoint from a font; the choice is which font ships.
|
|
224
|
+
#
|
|
225
|
+
# `bundled` is the whole reason this is a map and not a list: only the
|
|
226
|
+
# classic set (357 KB) rides inside the gem, because `ruby_everywhere` is
|
|
227
|
+
# 229 KB today and Symbols is ~10.6 MB — a 48x install cost paid by every
|
|
228
|
+
# user, including the desktop-only ones. The Symbols variants are fetched
|
|
229
|
+
# once on first Android build and cached under ~/.rubyeverywhere, which is
|
|
230
|
+
# noise next to the Gradle and AGP downloads the same build already makes.
|
|
231
|
+
#
|
|
232
|
+
# `codepoints` is the name → codepoint map Ruby validates every declared
|
|
233
|
+
# icon name against at build time, so a typo fails the build naming the tab
|
|
234
|
+
# it came from instead of rendering a blank icon on device.
|
|
235
|
+
ANDROID_ICON_FONTS = {
|
|
236
|
+
"symbols" => { "file" => "MaterialSymbolsOutlined.ttf", "bundled" => false },
|
|
237
|
+
"symbols-rounded" => { "file" => "MaterialSymbolsRounded.ttf", "bundled" => false },
|
|
238
|
+
"symbols-sharp" => { "file" => "MaterialSymbolsSharp.ttf", "bundled" => false },
|
|
239
|
+
"classic" => { "file" => "MaterialIcons-Regular.ttf", "bundled" => true },
|
|
240
|
+
# No font at all: the app brings its own drawables in native/android/res/.
|
|
241
|
+
"none" => { "file" => nil, "bundled" => true }
|
|
242
|
+
}.freeze
|
|
243
|
+
|
|
244
|
+
DEFAULT_ANDROID_ICON_FONT = "symbols"
|
|
245
|
+
|
|
246
|
+
def native_android_icon_font
|
|
247
|
+
value = native_android["icon_font"].to_s.strip
|
|
248
|
+
value.empty? ? DEFAULT_ANDROID_ICON_FONT : value
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# The font's asset filename (assets/fonts/<file>), or nil for "none".
|
|
252
|
+
def native_android_icon_font_file = ANDROID_ICON_FONTS.dig(native_android_icon_font, "file")
|
|
253
|
+
|
|
254
|
+
# The sidecar map the builder validates names against, alongside the font.
|
|
255
|
+
def native_android_icon_font_codepoints
|
|
256
|
+
native_android_icon_font_file&.sub(/\.ttf\z/, ".codepoints")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# True when the selected font has to be downloaded before the first build
|
|
260
|
+
# can stamp it — the only case that can fail offline, so the builder warns
|
|
261
|
+
# about it (and points at `classic`) rather than dying mid-Gradle.
|
|
262
|
+
def native_android_icon_font_fetched?
|
|
263
|
+
entry = ANDROID_ICON_FONTS[native_android_icon_font]
|
|
264
|
+
!entry.nil? && entry["bundled"] == false
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def native_android_icons? = !native_android_icon_font_file.nil?
|
|
268
|
+
|
|
269
|
+
def native_android_icon_font_errors
|
|
270
|
+
return [] if ANDROID_ICON_FONTS.key?(native_android_icon_font)
|
|
271
|
+
|
|
272
|
+
["native.android.icon_font #{native_android_icon_font.inspect} is not a known font — " \
|
|
273
|
+
"one of: #{ANDROID_ICON_FONTS.keys.join(", ")}"]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# The language each shell's extensions are written in, named in the
|
|
277
|
+
# messages because that's what the app author is looking at, and the
|
|
278
|
+
# pattern their type names must match — one entry per platform so the two
|
|
279
|
+
# can still diverge without either one's half changing.
|
|
280
|
+
NATIVE_LANGUAGES = { "ios" => "Swift", "android" => "Kotlin" }.freeze
|
|
281
|
+
NATIVE_TYPE_NAMES = { "ios" => SWIFT_TYPE_NAME, "android" => KOTLIN_TYPE_NAME }.freeze
|
|
282
|
+
|
|
283
|
+
private
|
|
284
|
+
|
|
285
|
+
# The two `native:` halves are declared the same way and read the same
|
|
286
|
+
# way, so the readers above are one line each over these. What genuinely
|
|
287
|
+
# differs — SPM packages vs Maven coordinates, the Android icon font —
|
|
288
|
+
# stays spelled out per platform.
|
|
289
|
+
def native_section(os)
|
|
290
|
+
section = @data.dig("native", os)
|
|
291
|
+
section.is_a?(Hash) ? section : {}
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def native_components(os) = Array(native_section(os)["components"]).map(&:to_s)
|
|
295
|
+
|
|
296
|
+
def native_screens(os)
|
|
297
|
+
entries = native_section(os)["screens"]
|
|
298
|
+
return {} unless entries.is_a?(Hash)
|
|
299
|
+
|
|
300
|
+
entries.to_h { |id, type| [id.to_s, type.to_s] }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def native_splash(os) = native_section(os)["splash"]&.to_s
|
|
304
|
+
|
|
305
|
+
def native_splash_seconds(os)
|
|
306
|
+
value = native_section(os)["splash_min_seconds"]
|
|
307
|
+
value.to_f.clamp(0.0, 8.0) if value.is_a?(Numeric)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def native_lazy_load(os)
|
|
311
|
+
value = native_section(os)["lazy_load_tabs"]
|
|
312
|
+
value if value == true || value == false
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def native_declared?(os)
|
|
316
|
+
!(native_components(os).empty? && native_screens(os).empty? && native_splash(os).nil?)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# The type/identifier half of one platform's errors — everything both
|
|
320
|
+
# shells validate identically. Each platform's public *_errors appends
|
|
321
|
+
# what only it has (packages, and Android's icon font).
|
|
322
|
+
def native_type_errors(os)
|
|
323
|
+
types = native_components(os) + native_screens(os).values + [native_splash(os)].compact
|
|
324
|
+
errors = types.reject { |t| t.match?(NATIVE_TYPE_NAMES[os]) }.map do |t|
|
|
325
|
+
"native.#{os}: #{t.inspect} is not a #{NATIVE_LANGUAGES[os]} type name (letters, digits, _)"
|
|
326
|
+
end
|
|
327
|
+
errors += native_screens(os).keys.reject { |id| id.match?(SCREEN_IDENTIFIER) }.map do |id|
|
|
328
|
+
"native.#{os}.screens: identifier #{id.inspect} must match #{SCREEN_IDENTIFIER.inspect}"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
raw = native_section(os)["splash_min_seconds"]
|
|
332
|
+
errors << "native.#{os}.splash_min_seconds must be a number (seconds)" if raw && !raw.is_a?(Numeric)
|
|
333
|
+
errors
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
class Config
|
|
5
|
+
module Shell
|
|
6
|
+
# The two `native:` knobs that ship inside everywhere.json rather than
|
|
7
|
+
# being compiled in, resolved for the platform being stamped. They read the
|
|
8
|
+
# target's own section: everywhere.json is written once per target, so an
|
|
9
|
+
# Android build that inherited native.ios.lazy_load_tabs would silently
|
|
10
|
+
# apply a decision the app made about a different shell — and the two
|
|
11
|
+
# platforms load tabs differently enough that the answer legitimately
|
|
12
|
+
# differs. Without a target (the desktop dev loop, and every caller written
|
|
13
|
+
# before Android existed) the iOS answer stands, which is what those
|
|
14
|
+
# callers have always gotten.
|
|
15
|
+
def native_lazy_load_tabs(target: nil)
|
|
16
|
+
android_target?(target) ? native_android_lazy_load_tabs : native_ios_lazy_load_tabs
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def native_splash_min_seconds(target: nil)
|
|
20
|
+
android_target?(target) ? native_android_splash_min_seconds : native_ios_splash_min_seconds
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The subset the shell needs, shipped as JSON (env var in dev,
|
|
24
|
+
# Resources/everywhere.json inside a bundled .app). Pass `target:` so the
|
|
25
|
+
# packaged config reflects the platform being built (its bundle id / name).
|
|
26
|
+
def to_shell_hash(target: nil)
|
|
27
|
+
{
|
|
28
|
+
"name" => name(target: target),
|
|
29
|
+
"bundle_id" => bundle_id(target: target),
|
|
30
|
+
"version" => version(target: target),
|
|
31
|
+
"mode" => mode,
|
|
32
|
+
"remote_url" => remote_url,
|
|
33
|
+
"remote_instances" => (true if remote_instances?),
|
|
34
|
+
"entry_path" => entry_path,
|
|
35
|
+
"tint_color" => tint_color,
|
|
36
|
+
"background_color" => background_color,
|
|
37
|
+
"menu" => (menu unless menu.empty?),
|
|
38
|
+
"tray" => (tray unless tray.empty?),
|
|
39
|
+
"window" => window_shell_hash,
|
|
40
|
+
"lazy_load_tabs" => native_lazy_load_tabs(target: target),
|
|
41
|
+
"splash_min_seconds" => native_splash_min_seconds(target: target),
|
|
42
|
+
"updates" => updates_shell_hash,
|
|
43
|
+
"permissions" => (permissions.keys unless permissions.empty?),
|
|
44
|
+
"auth" => auth_shell_hash,
|
|
45
|
+
# Hosts the shell treats as its own for incoming universal links, so a
|
|
46
|
+
# tapped link to any associated domain opens in-app rather than Safari.
|
|
47
|
+
"universal_link_hosts" => (deep_linking_domains if deep_linking? && !deep_linking_domains.empty?)
|
|
48
|
+
}.compact
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_shell_json(target: nil)
|
|
52
|
+
require "json"
|
|
53
|
+
JSON.generate(to_shell_hash(target: target))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
class Config
|
|
5
|
+
module Updates
|
|
6
|
+
# The `updates:` section — self-hosted auto-updates. Two halves:
|
|
7
|
+
# a client half (url/channel/public_key/auto/interval) that ships to the
|
|
8
|
+
# shell, and a publish-side `s3:` half that NEVER leaves the dev machine.
|
|
9
|
+
# `channel` here is a *release channel* (stable/beta) — not the receipt's
|
|
10
|
+
# `distribution_channel` (direct vs app stores).
|
|
11
|
+
def updates = @data.fetch("updates", nil) || {}
|
|
12
|
+
|
|
13
|
+
def updates_s3 = updates.fetch("s3", nil) || {}
|
|
14
|
+
|
|
15
|
+
def updates_channel = updates["channel"] || "stable"
|
|
16
|
+
|
|
17
|
+
def updates_url = updates["url"]&.chomp("/")
|
|
18
|
+
|
|
19
|
+
def updates_public_key = updates["public_key"]
|
|
20
|
+
|
|
21
|
+
# off — never check; check — check + notify (default); download — also
|
|
22
|
+
# fetch/verify in the background; install — silent auto-update.
|
|
23
|
+
def updates_auto = updates["auto"] || "check"
|
|
24
|
+
|
|
25
|
+
def updates_interval = [(updates["interval"] || 21_600).to_i, 300].max
|
|
26
|
+
|
|
27
|
+
# A dev machine serving a feed to itself is the one case http is fine.
|
|
28
|
+
UPDATES_LOOPBACK = /\A(localhost|127(\.\d+){3}|::1)\z/
|
|
29
|
+
|
|
30
|
+
# An http updates.url is not a smaller version of a working updater: the
|
|
31
|
+
# shell refuses to fetch it and silently stops checking, so the app ships
|
|
32
|
+
# looking fine and never updates again. Say so at build/publish time.
|
|
33
|
+
def updates_errors
|
|
34
|
+
url = updates_url.to_s
|
|
35
|
+
return [] if url.empty?
|
|
36
|
+
|
|
37
|
+
require "uri"
|
|
38
|
+
uri = URI.parse(url)
|
|
39
|
+
return [] if uri.scheme == "https"
|
|
40
|
+
return [] if uri.scheme == "http" && uri.host.to_s.delete("[]").match?(UPDATES_LOOPBACK)
|
|
41
|
+
|
|
42
|
+
["updates.url #{url.inspect} must be https — the shell disables the updater otherwise"]
|
|
43
|
+
rescue URI::Error => e
|
|
44
|
+
["updates.url #{url.inspect} is not a URL (#{e.message})"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The client subset for the shell; nil (and thus absent from
|
|
48
|
+
# everywhere.json) until both url and public_key are configured — an
|
|
49
|
+
# unsigned update feed is not a thing.
|
|
50
|
+
def updates_shell_hash
|
|
51
|
+
return nil unless updates_url && updates_public_key
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
"url" => updates_url,
|
|
55
|
+
"channel" => updates_channel,
|
|
56
|
+
"public_key" => updates_public_key,
|
|
57
|
+
"auto" => updates_auto,
|
|
58
|
+
"interval" => updates_interval
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|