ruby_everywhere 0.6.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 -2
- data/exe/rbe +2 -2
- data/lib/everywhere/agents_guide.rb +3 -1
- data/lib/everywhere/blake2b.rb +17 -1
- data/lib/everywhere/boot.rb +30 -6
- data/lib/everywhere/builders/android.rb +152 -64
- data/lib/everywhere/builders/base.rb +53 -0
- data/lib/everywhere/builders/desktop.rb +32 -40
- data/lib/everywhere/builders/ios.rb +268 -36
- 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/cli.rb +2 -0
- data/lib/everywhere/clock.rb +12 -0
- data/lib/everywhere/commands/build.rb +132 -66
- data/lib/everywhere/commands/clean.rb +8 -3
- data/lib/everywhere/commands/dev.rb +91 -242
- data/lib/everywhere/commands/doctor.rb +138 -21
- data/lib/everywhere/commands/install.rb +49 -8
- data/lib/everywhere/commands/platform/auth_status.rb +1 -0
- data/lib/everywhere/commands/platform/build.rb +141 -33
- data/lib/everywhere/commands/platform/login.rb +20 -6
- data/lib/everywhere/commands/platform/logout.rb +1 -0
- data/lib/everywhere/commands/platform/runner.rb +170 -25
- data/lib/everywhere/commands/preview.rb +286 -0
- data/lib/everywhere/commands/publish.rb +22 -2
- data/lib/everywhere/commands/release.rb +155 -35
- data/lib/everywhere/commands/shell_dir.rb +4 -2
- 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 +59 -1367
- data/lib/everywhere/console.rb +2 -1
- data/lib/everywhere/desktop_dev_app.rb +138 -0
- data/lib/everywhere/dock/state.rb +3 -1
- data/lib/everywhere/engine.rb +24 -0
- data/lib/everywhere/entrypoint.rb +24 -0
- data/lib/everywhere/error.rb +8 -0
- data/lib/everywhere/framework.rb +23 -6
- data/lib/everywhere/host.rb +11 -0
- data/lib/everywhere/ignore.rb +13 -5
- data/lib/everywhere/jump.rb +121 -0
- data/lib/everywhere/line_pump.rb +3 -1
- data/lib/everywhere/minisign.rb +1 -0
- data/lib/everywhere/mobile_config_endpoint.rb +47 -0
- data/lib/everywhere/mobile_configs_controller.rb +46 -0
- data/lib/everywhere/native_platform.rb +44 -0
- data/lib/everywhere/paths.rb +36 -13
- data/lib/everywhere/platform/client.rb +46 -7
- data/lib/everywhere/platform/credentials.rb +18 -8
- data/lib/everywhere/platform/snapshot.rb +12 -0
- 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/receipt.rb +55 -10
- data/lib/everywhere/s3.rb +1 -0
- data/lib/everywhere/shell_pages.rb +109 -0
- data/lib/everywhere/shellout.rb +19 -0
- data/lib/everywhere/simulator.rb +12 -1
- 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/android/app/build.gradle.kts +29 -1
- data/support/mobile/ios/App/EverywhereConfig.swift +17 -5
- data/support/mobile/ios/App/SceneDelegate.swift +75 -8
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +2 -4
- data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -1
- data/support/mobile/ios/README.md +13 -6
- data/support/release/macos/notarize.sh +3 -0
- metadata +40 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1644d6c9be14fdb8ef738eaee6f33225e5916ea401945f7c6c494e8963a2c16
|
|
4
|
+
data.tar.gz: 03621b7846f16b5621a352e7d4fc42ac38c8859095fc1be1098eef48b081fb0f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4de42927fa8a53c136e0f9a3015ee1f930199642a26504d198673d0fe3e8cc24e32e9db219d34a170023809db8a9fb20715a6577e13cc6bb030fc9e3a8600b45
|
|
7
|
+
data.tar.gz: 58da2af357a422bd9dd87badf31175ba71767a5c3364d52f875690fd217a0be15cf1b12296bb195332ebec015ede65b7f486e3c1bf65ff66d5d0cb6831a20e3f
|
data/exe/every
CHANGED
data/exe/rbe
CHANGED
|
@@ -51,7 +51,9 @@ module Everywhere
|
|
|
51
51
|
path = File.join(template_dir, *parts)
|
|
52
52
|
File.exist?(path) or UI.die!("couldn't find the bundled AGENTS.md template at #{path}; " \
|
|
53
53
|
"reinstall ruby_everywhere")
|
|
54
|
-
|
|
54
|
+
# The templates are UTF-8 prose; under LANG=C the gsub below would raise
|
|
55
|
+
# on them rather than substitute.
|
|
56
|
+
File.read(path, encoding: "UTF-8").scrub
|
|
55
57
|
end
|
|
56
58
|
end
|
|
57
59
|
end
|
data/lib/everywhere/blake2b.rb
CHANGED
|
@@ -68,7 +68,12 @@ module Everywhere
|
|
|
68
68
|
rescue StandardError
|
|
69
69
|
nil
|
|
70
70
|
end
|
|
71
|
-
|
|
71
|
+
# `digest` is one-shot (no incremental update), so the fallback has to hold
|
|
72
|
+
# the whole artifact in memory — worth warning about on a 300MB .zip.
|
|
73
|
+
unless md
|
|
74
|
+
warn_pure_ruby_fallback(path)
|
|
75
|
+
return digest(File.binread(path), 64)
|
|
76
|
+
end
|
|
72
77
|
|
|
73
78
|
File.open(path, "rb") do |io|
|
|
74
79
|
while (chunk = io.read(1 << 20))
|
|
@@ -78,6 +83,17 @@ module Everywhere
|
|
|
78
83
|
md.digest
|
|
79
84
|
end
|
|
80
85
|
|
|
86
|
+
# Once per process — signing prehashes the same artifact more than once.
|
|
87
|
+
def warn_pure_ruby_fallback(path)
|
|
88
|
+
return if @warned_fallback
|
|
89
|
+
|
|
90
|
+
@warned_fallback = true
|
|
91
|
+
require_relative "ui"
|
|
92
|
+
mb = (File.size(path) / 1024.0 / 1024.0).round(1)
|
|
93
|
+
Everywhere::UI.warn("this Ruby's OpenSSL has no BLAKE2b512 — hashing #{mb}MB in pure Ruby " \
|
|
94
|
+
"(slow, and the file is read into memory)")
|
|
95
|
+
end
|
|
96
|
+
|
|
81
97
|
def compress(h, block, t, last)
|
|
82
98
|
m = block.unpack("Q<16")
|
|
83
99
|
v = h + IV
|
data/lib/everywhere/boot.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "securerandom"
|
|
5
|
+
require_relative "error"
|
|
5
6
|
|
|
6
7
|
module Everywhere
|
|
7
8
|
# Runtime boot for the packaged desktop build. Called from the app's
|
|
@@ -64,15 +65,32 @@ module Everywhere
|
|
|
64
65
|
def default_app_data
|
|
65
66
|
case RUBY_PLATFORM
|
|
66
67
|
when /darwin/
|
|
67
|
-
File.join(
|
|
68
|
+
File.join(home, "Library", "Application Support", config.bundle_id)
|
|
68
69
|
when /mingw|mswin/
|
|
69
|
-
File.join(
|
|
70
|
+
File.join(app_data_env("APPDATA"), config.bundle_id)
|
|
70
71
|
else
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
# `||`, not ENV.fetch's default: the fallback resolves a home that a
|
|
73
|
+
# machine setting XDG_DATA_HOME needn't have.
|
|
74
|
+
File.join(ENV["XDG_DATA_HOME"] || File.join(home, ".local", "share"), config.bundle_id)
|
|
73
75
|
end
|
|
74
76
|
end
|
|
75
77
|
|
|
78
|
+
# Only reached in a standalone run — the shell always passes NATIVE_APPDATA.
|
|
79
|
+
# Dir.home raises when HOME is unset and there's no passwd entry (a
|
|
80
|
+
# container run with --user), which is a bare ArgumentError otherwise.
|
|
81
|
+
def home
|
|
82
|
+
Dir.home
|
|
83
|
+
rescue ArgumentError
|
|
84
|
+
raise Error, app_data_hint("no home directory")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def app_data_env(key)
|
|
88
|
+
ENV[key] or raise Error, app_data_hint("#{key} is unset")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def app_data_hint(reason) = "can't work out where to keep app data (#{reason}) — " \
|
|
92
|
+
"set NATIVE_APPDATA (or HOME / APPDATA)"
|
|
93
|
+
|
|
76
94
|
def prepare_app_data
|
|
77
95
|
storage = File.join(app_data, "storage")
|
|
78
96
|
FileUtils.mkdir_p(storage)
|
|
@@ -124,8 +142,14 @@ module Everywhere
|
|
|
124
142
|
def ensure_secret_key_base
|
|
125
143
|
secret_file = File.join(app_data, "secret_key_base")
|
|
126
144
|
unless File.exist?(secret_file)
|
|
127
|
-
|
|
128
|
-
|
|
145
|
+
# Created 0600 rather than written then chmod'ed: this signs the app's
|
|
146
|
+
# cookies, and write-then-tighten leaves it world-readable in between.
|
|
147
|
+
# EXCL so a racing boot doesn't clobber the one already in use.
|
|
148
|
+
begin
|
|
149
|
+
File.open(secret_file, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |f| f.write(SecureRandom.hex(64)) }
|
|
150
|
+
rescue Errno::EEXIST
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
129
153
|
end
|
|
130
154
|
ENV["SECRET_KEY_BASE"] ||= File.read(secret_file).strip
|
|
131
155
|
end
|
|
@@ -10,6 +10,8 @@ require_relative "../android_resources"
|
|
|
10
10
|
require_relative "../android_sdk"
|
|
11
11
|
require_relative "../version"
|
|
12
12
|
require_relative "../ui"
|
|
13
|
+
require_relative "base"
|
|
14
|
+
require_relative "native_sources"
|
|
13
15
|
|
|
14
16
|
module Everywhere
|
|
15
17
|
module Builders
|
|
@@ -32,13 +34,34 @@ module Everywhere
|
|
|
32
34
|
# app/src/stamped/res/drawable*/ native/android/assets/** as drawables
|
|
33
35
|
# app/src/main/java/com/rubyeverywhere/shell/extensions/ native/android/*.kt + registry
|
|
34
36
|
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
class Android
|
|
37
|
+
# Signing and the format Play accepts are driven from the environment, not
|
|
38
|
+
# from everywhere.yml — see SIGNING_ENV.
|
|
39
|
+
class Android < Base
|
|
38
40
|
# Fixed by the template contract: one Gradle module, always :app, so
|
|
39
41
|
# build paths and task names never depend on per-app values.
|
|
40
42
|
GRADLE_MODULE = ":app"
|
|
41
43
|
|
|
44
|
+
# What each package format is called in the three places it has a name:
|
|
45
|
+
# the Gradle verb, the outputs/ subdirectory and the file extension.
|
|
46
|
+
FORMATS = {
|
|
47
|
+
apk: { task: "assemble", dir: "apk", extension: "apk" },
|
|
48
|
+
aab: { task: "bundle", dir: "bundle", extension: "aab" }
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# Release signing is read from the environment and handed to Gradle as
|
|
52
|
+
# environment, never stamped into everywhere.properties: that file lives
|
|
53
|
+
# in the work dir for the life of the app, and a keystore password written
|
|
54
|
+
# there outlives the build that needed it. The keystore itself stays where
|
|
55
|
+
# the machine put it — nothing is copied into the project.
|
|
56
|
+
SIGNING_ENV = %w[EVERY_ANDROID_KEYSTORE EVERY_ANDROID_KEYSTORE_PASSWORD
|
|
57
|
+
EVERY_ANDROID_KEY_ALIAS EVERY_ANDROID_KEY_PASSWORD].freeze
|
|
58
|
+
|
|
59
|
+
# The two the keystore can't be used without. EVERY_ANDROID_KEY_PASSWORD
|
|
60
|
+
# is not among them: a key generated without -keypass shares the store's.
|
|
61
|
+
REQUIRED_SIGNING_ENV = %w[EVERY_ANDROID_KEYSTORE_PASSWORD EVERY_ANDROID_KEY_ALIAS].freeze
|
|
62
|
+
|
|
63
|
+
VERSION_CODE_ENV = "EVERY_ANDROID_VERSION_CODE"
|
|
64
|
+
|
|
42
65
|
# The Kotlin package the shell's sources live in. NOT the applicationId:
|
|
43
66
|
# the app id is stamped per app, the code's package is frozen, and mixing
|
|
44
67
|
# them up is the classic Android manifest mistake — so anything we
|
|
@@ -86,47 +109,36 @@ module Everywhere
|
|
|
86
109
|
# keeps its fonts out of assets/ still resolves.
|
|
87
110
|
BUNDLED_FONT_DIRS = [%w[app src main assets fonts], %w[fonts]].freeze
|
|
88
111
|
|
|
89
|
-
# Marks which template a work dir was staged from. A gem upgrade can add,
|
|
90
|
-
# rename or delete template files, and staging merges rather than wipes
|
|
91
|
-
# (see #stage), so a stale Kotlin file from an older template would keep
|
|
92
|
-
# compiling in. Version mismatch = wipe once, then merge as usual.
|
|
93
|
-
STAGE_MARKER = ".everywhere-template"
|
|
94
|
-
|
|
95
112
|
# What AGP accepts as an applicationId: two or more segments, each a Java
|
|
96
113
|
# identifier starting with a letter. No hyphens, unlike a bundle id.
|
|
97
114
|
APPLICATION_ID = /\A[a-zA-Z]\w*(\.[a-zA-Z]\w*)+\z/
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
@config = config
|
|
101
|
-
@root = root
|
|
102
|
-
@target = target
|
|
103
|
-
@template_dir = template_dir
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Build the APK and copy it into dist_dir. `dev` builds the debug variant
|
|
116
|
+
# Build the app and copy it into dist_dir. `dev` builds the debug variant
|
|
107
117
|
# and relaxes the remote-mode requirement (the dev URL is stamped into a
|
|
108
118
|
# debug asset each run — Android has no SIMCTL_CHILD_* equivalent).
|
|
109
|
-
#
|
|
110
|
-
|
|
119
|
+
# `format` is :apk (installable) or :aab (what Play accepts). Returns the
|
|
120
|
+
# path of the collected artifact.
|
|
121
|
+
def build!(dist_dir:, dev: false, dev_url: nil, format: :apk)
|
|
111
122
|
# The template is resolved before preflight, not after it like iOS:
|
|
112
123
|
# preflight has to settle the icon font, and the bundled font lives in
|
|
113
124
|
# the template. Everything that can fail slowly (a font download) or
|
|
114
125
|
# cheaply (a typo'd icon name) is therefore decided before Gradle — the
|
|
115
126
|
# difference between a 5-second error and one that lands 90 seconds in.
|
|
116
127
|
template = @template_dir ? File.expand_path(@template_dir) : Paths.android_dir!
|
|
117
|
-
preflight!(dev: dev, template: template)
|
|
128
|
+
preflight!(dev: dev, format: format, template: template)
|
|
118
129
|
work = stage(template)
|
|
119
130
|
stamp!(work)
|
|
120
131
|
stamp_dev_url!(work, dev_url)
|
|
121
132
|
variant = dev ? "debug" : "release"
|
|
122
|
-
gradle!(work, variant, dist_dir)
|
|
123
|
-
collect(work, variant, dist_dir)
|
|
133
|
+
gradle!(work, variant, format, dist_dir)
|
|
134
|
+
collect(work, variant, format, dist_dir)
|
|
124
135
|
end
|
|
125
136
|
|
|
126
137
|
private
|
|
127
138
|
|
|
128
|
-
|
|
129
|
-
|
|
139
|
+
# AGP name for the bundle id, kept because everything Android-side (the
|
|
140
|
+
# properties file, the work dir, the validation) says applicationId.
|
|
141
|
+
def application_id = bundle_id
|
|
130
142
|
|
|
131
143
|
# The dev-server URL, for the debug source set only (EverywhereConfig's
|
|
132
144
|
# devUrl reads it, and release builds never look). This is the whole of
|
|
@@ -149,13 +161,17 @@ module Everywhere
|
|
|
149
161
|
UI.step("pointing the shell at #{UI.cyan(dev_url)}")
|
|
150
162
|
end
|
|
151
163
|
|
|
152
|
-
def preflight!(dev:, template:)
|
|
164
|
+
def preflight!(dev:, format:, template:)
|
|
153
165
|
# Config shape first, machine state second: a typo in everywhere.yml is
|
|
154
166
|
# the app author's to fix wherever they are, while "no Android SDK"
|
|
155
167
|
# depends on the machine they happen to be sitting at.
|
|
156
168
|
errors = @config.native_android_errors
|
|
157
169
|
UI.die!("native.android in config/everywhere.yml:\n#{errors.join("\n")}") unless errors.empty?
|
|
158
170
|
|
|
171
|
+
check_format!(dev: dev, format: format)
|
|
172
|
+
@signing = dev ? nil : signing_env!
|
|
173
|
+
@version_code = version_code
|
|
174
|
+
|
|
159
175
|
unless dev || @config.remote?
|
|
160
176
|
UI.die!("Android apps are remote mode only for now — set remote.url in config/everywhere.yml " \
|
|
161
177
|
"(the shell wraps your deployed app; nothing is packaged locally)")
|
|
@@ -179,7 +195,10 @@ module Everywhere
|
|
|
179
195
|
# fix — the analogue of the iOS xcode-select message.
|
|
180
196
|
@sdk_root = AndroidSdk.sdk_root!
|
|
181
197
|
@java_home = AndroidSdk.java_home!
|
|
182
|
-
|
|
198
|
+
# adb only matters when something is going to be installed on a device,
|
|
199
|
+
# so it is a `dev` requirement and not a build one — a headless runner
|
|
200
|
+
# that builds a release AAB has no reason to carry platform-tools.
|
|
201
|
+
AndroidSdk.adb! if dev
|
|
183
202
|
# A missing compile SDK surfaces from Gradle as "Failed to find target
|
|
184
203
|
# with hash string android-35", minutes in and after the AGP download.
|
|
185
204
|
AndroidSdk.platform!
|
|
@@ -187,6 +206,48 @@ module Everywhere
|
|
|
187
206
|
resolve_icon_font!(template)
|
|
188
207
|
end
|
|
189
208
|
|
|
209
|
+
def check_format!(dev:, format:)
|
|
210
|
+
unless FORMATS.key?(format)
|
|
211
|
+
UI.die!("#{format.inspect} isn't an Android package format — :apk (installable) or :aab " \
|
|
212
|
+
"(what Google Play accepts)")
|
|
213
|
+
end
|
|
214
|
+
return unless format == :aab && dev
|
|
215
|
+
|
|
216
|
+
UI.die!("an .aab can't be installed on a device or an emulator — it's an upload format Play " \
|
|
217
|
+
"re-signs into per-device APKs. `every dev` builds an APK; ask for the bundle when " \
|
|
218
|
+
"you're publishing")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# --- signing ---------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
# The signing environment Gradle is handed, or nil for the unsigned
|
|
224
|
+
# release the default build still produces. Resolved here rather than left
|
|
225
|
+
# to the build script so a half-configured keystore fails in seconds with
|
|
226
|
+
# the missing variable named, instead of as an AGP error two minutes in —
|
|
227
|
+
# or, worse, as an unsigned APK nobody notices until Play rejects it.
|
|
228
|
+
def signing_env!
|
|
229
|
+
keystore = env_value("EVERY_ANDROID_KEYSTORE") or return nil
|
|
230
|
+
|
|
231
|
+
missing = REQUIRED_SIGNING_ENV.reject { |name| env_value(name) }
|
|
232
|
+
unless missing.empty?
|
|
233
|
+
UI.die!("EVERY_ANDROID_KEYSTORE is set but #{missing.join(" and ")} " \
|
|
234
|
+
"#{missing.one? ? "is" : "are"} not — signing needs the keystore, its password " \
|
|
235
|
+
"and the alias of the key inside it (EVERY_ANDROID_KEY_PASSWORD falls back to " \
|
|
236
|
+
"the keystore password)")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
path = File.expand_path(keystore)
|
|
240
|
+
UI.die!("EVERY_ANDROID_KEYSTORE points at #{path}, which isn't a file") unless File.file?(path)
|
|
241
|
+
|
|
242
|
+
UI.step("signing the release #{UI.dim("(#{env_value("EVERY_ANDROID_KEY_ALIAS")} in " \
|
|
243
|
+
"#{UI.short_path(path)})")}")
|
|
244
|
+
{ "EVERY_ANDROID_KEYSTORE" => path,
|
|
245
|
+
"EVERY_ANDROID_KEYSTORE_PASSWORD" => env_value("EVERY_ANDROID_KEYSTORE_PASSWORD"),
|
|
246
|
+
"EVERY_ANDROID_KEY_ALIAS" => env_value("EVERY_ANDROID_KEY_ALIAS"),
|
|
247
|
+
"EVERY_ANDROID_KEY_PASSWORD" => env_value("EVERY_ANDROID_KEY_PASSWORD") ||
|
|
248
|
+
env_value("EVERY_ANDROID_KEYSTORE_PASSWORD") }
|
|
249
|
+
end
|
|
250
|
+
|
|
190
251
|
# Copy the template into the app's persistent work dir. Merged over
|
|
191
252
|
# whatever is already there, NOT wiped like the iOS stage: Gradle's
|
|
192
253
|
# incremental state (app/build/, .gradle/) lives *inside* this directory,
|
|
@@ -202,15 +263,10 @@ module Everywhere
|
|
|
202
263
|
# non-executable gradlew fails with a bare "Permission denied".
|
|
203
264
|
wrapper = File.join(work, "gradlew")
|
|
204
265
|
FileUtils.chmod("u+x", wrapper) if File.file?(wrapper)
|
|
205
|
-
|
|
266
|
+
mark_staged!(work)
|
|
206
267
|
work
|
|
207
268
|
end
|
|
208
269
|
|
|
209
|
-
def staged_version(work)
|
|
210
|
-
marker = File.join(work, STAGE_MARKER)
|
|
211
|
-
File.read(marker).strip if File.file?(marker)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
270
|
# The whole per-app surface: identity properties, two JSON assets, colors,
|
|
215
271
|
# launcher icons, the icon font, the merged-in manifest, the Extensions
|
|
216
272
|
# sources + generated registry, and the Maven coordinate script. No build
|
|
@@ -248,7 +304,7 @@ module Everywhere
|
|
|
248
304
|
# belong in config/everywhere.yml.
|
|
249
305
|
everywhere.applicationId=#{properties_value(application_id)}
|
|
250
306
|
everywhere.versionName=#{properties_value(@config.version(target: @target))}
|
|
251
|
-
everywhere.versionCode=#{version_code}
|
|
307
|
+
everywhere.versionCode=#{@version_code}
|
|
252
308
|
everywhere.label=#{properties_value(name)}
|
|
253
309
|
PROPERTIES
|
|
254
310
|
end
|
|
@@ -257,7 +313,20 @@ module Everywhere
|
|
|
257
313
|
# deriving it from the version the app already maintains beats asking for
|
|
258
314
|
# a second number nobody remembers to bump. 1.2.3 → 10203, which stays
|
|
259
315
|
# monotonic as long as minor/patch stay under 100.
|
|
316
|
+
#
|
|
317
|
+
# EVERY_ANDROID_VERSION_CODE overrides it outright, for the pipeline that
|
|
318
|
+
# numbers builds rather than versions them (a run number, a commit count).
|
|
260
319
|
def version_code
|
|
320
|
+
override = env_value(VERSION_CODE_ENV) or return derived_version_code
|
|
321
|
+
|
|
322
|
+
unless override.match?(/\A[1-9]\d*\z/)
|
|
323
|
+
UI.die!("#{VERSION_CODE_ENV}=#{override.inspect} — a versionCode is a positive integer, " \
|
|
324
|
+
"and Play only accepts an upload whose code is higher than the last one's")
|
|
325
|
+
end
|
|
326
|
+
override.to_i
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def derived_version_code
|
|
261
330
|
major, minor, patch = @config.version(target: @target).to_s.split(".").map(&:to_i)
|
|
262
331
|
code = ((major || 0) * 10_000) + ((minor || 0) * 100) + (patch || 0)
|
|
263
332
|
[code, 1].max
|
|
@@ -473,7 +542,13 @@ module Everywhere
|
|
|
473
542
|
when Net::HTTPRedirection
|
|
474
543
|
raise "too many redirects for #{url}" if redirects <= 0
|
|
475
544
|
|
|
476
|
-
|
|
545
|
+
# The font is checksummed, so a swapped body is caught anyway —
|
|
546
|
+
# but the base is https and a redirect has no business dropping
|
|
547
|
+
# to http on the way to a cached artifact.
|
|
548
|
+
target = URI.join(uri, response["location"].to_s)
|
|
549
|
+
raise "refusing a non-https redirect to #{target.host}" unless target.scheme == "https"
|
|
550
|
+
|
|
551
|
+
return fetch!(target.to_s, dest, sha256: sha256, redirects: redirects - 1)
|
|
477
552
|
when Net::HTTPSuccess
|
|
478
553
|
File.open(part, "wb") { |f| response.read_body { |chunk| f.write(chunk) } }
|
|
479
554
|
else
|
|
@@ -672,18 +747,9 @@ module Everywhere
|
|
|
672
747
|
FileUtils.rm_rf(dest)
|
|
673
748
|
FileUtils.mkdir_p(dest)
|
|
674
749
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
sources.each do |file|
|
|
678
|
-
relative = file.delete_prefix("#{source}/")
|
|
679
|
-
if File.basename(file) == "EverywhereExtensions.kt"
|
|
680
|
-
UI.die!("native/android/#{relative}: EverywhereExtensions.kt is generated by " \
|
|
681
|
-
"`every build` — declare your types under native.android in config/everywhere.yml instead")
|
|
682
|
-
end
|
|
750
|
+
sources = NativeSources.copy!(from: File.join(@root, "native", "android"), into: dest, ext: "kt",
|
|
751
|
+
reserved: "EverywhereExtensions.kt", label: "native/android") do |file, relative|
|
|
683
752
|
check_package!(file, relative)
|
|
684
|
-
target_file = File.join(dest, relative)
|
|
685
|
-
FileUtils.mkdir_p(File.dirname(target_file))
|
|
686
|
-
FileUtils.cp(file, target_file)
|
|
687
753
|
end
|
|
688
754
|
|
|
689
755
|
if @config.native_android?
|
|
@@ -796,45 +862,67 @@ module Everywhere
|
|
|
796
862
|
|
|
797
863
|
# --- gradle ---------------------------------------------------------------
|
|
798
864
|
|
|
799
|
-
def gradle!(work, variant, dist_dir)
|
|
800
|
-
UI.step("building Android shell #{UI.dim("(Gradle, #{variant}, first
|
|
801
|
-
"Gradle and the Android Gradle Plugin)")}")
|
|
865
|
+
def gradle!(work, variant, format, dist_dir)
|
|
866
|
+
UI.step("building Android shell #{UI.dim("(Gradle, #{variant} #{format.to_s.upcase}, first " \
|
|
867
|
+
"run downloads Gradle and the Android Gradle Plugin)")}")
|
|
802
868
|
FileUtils.mkdir_p(dist_dir)
|
|
803
869
|
log = File.join(dist_dir, "android-build.log")
|
|
804
|
-
task = "#{GRADLE_MODULE}:
|
|
870
|
+
task = "#{GRADLE_MODULE}:#{FORMATS.fetch(format)[:task]}#{variant.capitalize}"
|
|
805
871
|
# -p instead of a chdir: the wrapper finds its own jar from $0, so an
|
|
806
872
|
# absolute gradlew with an explicit project dir works from anywhere
|
|
807
873
|
# (Shellout.run_logged! has no chdir). --console=plain keeps the stream
|
|
808
874
|
# line-oriented for LogFilter whether or not a TTY is attached.
|
|
809
875
|
Shellout.run_logged!(gradle_env,
|
|
810
|
-
[
|
|
876
|
+
[gradle_wrapper(work), "-p", work, "--console=plain", task],
|
|
811
877
|
log: log, filter: LogFilter.for(:gradle))
|
|
812
878
|
end
|
|
813
879
|
|
|
880
|
+
# The wrapper Windows can actually execute — the POSIX gradlew is a shell
|
|
881
|
+
# script there, and cmd.exe won't run it.
|
|
882
|
+
def gradle_wrapper(work)
|
|
883
|
+
File.join(work, AndroidSdk.windows? ? "gradlew.bat" : "gradlew")
|
|
884
|
+
end
|
|
885
|
+
|
|
814
886
|
# The whole toolchain is handed to Gradle explicitly. ANDROID_HOME is what
|
|
815
887
|
# AGP reads when there's no local.properties — and we never write one, so
|
|
816
888
|
# the stamped project stays clean and Android Studio can resolve its own
|
|
817
889
|
# SDK. GRADLE_USER_HOME redirects the 1.4 GB of Gradle distribution, AGP
|
|
818
890
|
# and Hotwire artifacts out of the gem and into a cache shared by every app.
|
|
891
|
+
#
|
|
892
|
+
# The signing variables travel the same way rather than through a file the
|
|
893
|
+
# build script reads: env is the one channel that reaches the Gradle child
|
|
894
|
+
# without leaving a password on disk. The resolved values (absolute
|
|
895
|
+
# keystore path, key password defaulted to the store's) are what's passed,
|
|
896
|
+
# so the build script never has to repeat the fallback.
|
|
819
897
|
def gradle_env
|
|
820
898
|
{ "JAVA_HOME" => @java_home,
|
|
821
899
|
"ANDROID_HOME" => @sdk_root,
|
|
822
900
|
"ANDROID_SDK_ROOT" => @sdk_root,
|
|
823
|
-
"GRADLE_USER_HOME" => Paths.android_gradle_home
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
#
|
|
828
|
-
#
|
|
829
|
-
#
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
901
|
+
"GRADLE_USER_HOME" => Paths.android_gradle_home,
|
|
902
|
+
VERSION_CODE_ENV => @version_code.to_s }.merge(@signing || {})
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
# Copy the built artifact out of the module's build dir. An unsigned
|
|
906
|
+
# release APK is spelled app-release-unsigned.apk by AGP, so both names
|
|
907
|
+
# are accepted — but only when no keystore was configured: with one set,
|
|
908
|
+
# the unsigned name means the signingConfig didn't take, and shipping that
|
|
909
|
+
# silently is how an unsignable APK reaches Play. Bundles have no such
|
|
910
|
+
# spelling; bundleRelease writes app-release.aab either way.
|
|
911
|
+
def collect(work, variant, format, dist_dir)
|
|
912
|
+
spec = FORMATS.fetch(format)
|
|
913
|
+
kind = spec[:extension].upcase
|
|
914
|
+
dir = File.join(work, "app", "build", "outputs", spec[:dir], variant)
|
|
915
|
+
signed = File.join(dir, "app-#{variant}.#{spec[:extension]}")
|
|
916
|
+
unsigned = File.join(dir, "app-#{variant}-unsigned.#{spec[:extension]}")
|
|
917
|
+
built = [signed, unsigned].find { |path| File.file?(path) }
|
|
918
|
+
UI.die!("Gradle reported success but no #{kind} in #{UI.short_path(dir)}") unless built
|
|
919
|
+
|
|
920
|
+
if @signing && built == unsigned
|
|
921
|
+
UI.die!("Gradle wrote #{File.basename(built)} — EVERY_ANDROID_KEYSTORE is set, so the " \
|
|
922
|
+
"#{kind} should have been signed. Check the keystore's alias and passwords")
|
|
923
|
+
end
|
|
836
924
|
|
|
837
|
-
dest = File.join(dist_dir, "#{name}
|
|
925
|
+
dest = File.join(dist_dir, "#{name}.#{spec[:extension]}")
|
|
838
926
|
FileUtils.rm_f(dest)
|
|
839
927
|
FileUtils.cp(built, dest)
|
|
840
928
|
dest
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../version"
|
|
4
|
+
|
|
5
|
+
module Everywhere
|
|
6
|
+
module Builders
|
|
7
|
+
# What the three platform builders (Android, Ios, Desktop) genuinely share.
|
|
8
|
+
# Deliberately thin: `stage`, `preflight!`, `stamp!` and `collect` are
|
|
9
|
+
# structurally parallel across the three but mean different things on each
|
|
10
|
+
# platform, so they stay where they are. Only identity (name, bundle id),
|
|
11
|
+
# the environment reader and the stage marker live here.
|
|
12
|
+
class Base
|
|
13
|
+
# Marks which template a work dir was staged from. A gem upgrade can add,
|
|
14
|
+
# rename or delete template files, and staging merges rather than wipes
|
|
15
|
+
# (see #stage), so a stale Kotlin file from an older template would keep
|
|
16
|
+
# compiling in. Version mismatch = wipe once, then merge as usual.
|
|
17
|
+
STAGE_MARKER = ".everywhere-template"
|
|
18
|
+
|
|
19
|
+
# `target` is a build target like "ios-arm64" or "android-arm64"; the
|
|
20
|
+
# desktop builder has none, and everything below reads it as nil.
|
|
21
|
+
def initialize(config:, root:, target: nil, template_dir: nil)
|
|
22
|
+
@config = config
|
|
23
|
+
@root = root
|
|
24
|
+
@target = target
|
|
25
|
+
@template_dir = template_dir
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def name = @config.name(target: @target)
|
|
31
|
+
def bundle_id = @config.bundle_id(target: @target)
|
|
32
|
+
|
|
33
|
+
# An env var that was set to nothing is the same as unset here: CI writes
|
|
34
|
+
# `EVERY_ANDROID_KEYSTORE: ${{ secrets.KEYSTORE }}` whether or not the
|
|
35
|
+
# secret exists, and an empty keystore path must not look configured.
|
|
36
|
+
def env_value(name)
|
|
37
|
+
value = ENV[name].to_s.strip
|
|
38
|
+
value.empty? ? nil : value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def staged_version(work)
|
|
42
|
+
marker = File.join(work, STAGE_MARKER)
|
|
43
|
+
File.read(marker).strip if File.file?(marker)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Records which gem version staged this work dir, for the next run to
|
|
47
|
+
# read back.
|
|
48
|
+
def mark_staged!(work)
|
|
49
|
+
File.write(File.join(work, STAGE_MARKER), "#{Everywhere::VERSION}\n")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|