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
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
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "everywhere/
|
|
5
|
-
require "everywhere/ui"
|
|
4
|
+
require "everywhere/entrypoint"
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
Dry::CLI.new(Everywhere::CLI).call
|
|
9
|
-
rescue Interrupt
|
|
10
|
-
# ^C is an answer, not a crash: no backtrace, and the shell's own convention
|
|
11
|
-
# for "killed by SIGINT".
|
|
12
|
-
exit 130
|
|
13
|
-
rescue Everywhere::Error => e
|
|
14
|
-
# Everywhere::Fatal is a SystemExit, not an Error, so a die! that already
|
|
15
|
-
# printed its own line passes straight through here.
|
|
16
|
-
Everywhere::UI.die!(e.message)
|
|
17
|
-
end
|
|
6
|
+
Everywhere::Entrypoint.run
|
data/exe/rbe
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "everywhere/
|
|
5
|
-
require "everywhere/ui"
|
|
4
|
+
require "everywhere/entrypoint"
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
Dry::CLI.new(Everywhere::CLI).call
|
|
9
|
-
rescue Interrupt
|
|
10
|
-
# ^C is an answer, not a crash: no backtrace, and the shell's own convention
|
|
11
|
-
# for "killed by SIGINT".
|
|
12
|
-
exit 130
|
|
13
|
-
rescue Everywhere::Error => e
|
|
14
|
-
# Everywhere::Fatal is a SystemExit, not an Error, so a die! that already
|
|
15
|
-
# printed its own line passes straight through here.
|
|
16
|
-
Everywhere::UI.die!(e.message)
|
|
17
|
-
end
|
|
6
|
+
Everywhere::Entrypoint.run
|
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
|
|
@@ -141,8 +142,14 @@ module Everywhere
|
|
|
141
142
|
def ensure_secret_key_base
|
|
142
143
|
secret_file = File.join(app_data, "secret_key_base")
|
|
143
144
|
unless File.exist?(secret_file)
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
146
153
|
end
|
|
147
154
|
ENV["SECRET_KEY_BASE"] ||= File.read(secret_file).strip
|
|
148
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
|
|
@@ -34,7 +36,7 @@ module Everywhere
|
|
|
34
36
|
#
|
|
35
37
|
# Signing and the format Play accepts are driven from the environment, not
|
|
36
38
|
# from everywhere.yml — see SIGNING_ENV.
|
|
37
|
-
class Android
|
|
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"
|
|
@@ -107,23 +109,10 @@ module Everywhere
|
|
|
107
109
|
# keeps its fonts out of assets/ still resolves.
|
|
108
110
|
BUNDLED_FONT_DIRS = [%w[app src main assets fonts], %w[fonts]].freeze
|
|
109
111
|
|
|
110
|
-
# Marks which template a work dir was staged from. A gem upgrade can add,
|
|
111
|
-
# rename or delete template files, and staging merges rather than wipes
|
|
112
|
-
# (see #stage), so a stale Kotlin file from an older template would keep
|
|
113
|
-
# compiling in. Version mismatch = wipe once, then merge as usual.
|
|
114
|
-
STAGE_MARKER = ".everywhere-template"
|
|
115
|
-
|
|
116
112
|
# What AGP accepts as an applicationId: two or more segments, each a Java
|
|
117
113
|
# identifier starting with a letter. No hyphens, unlike a bundle id.
|
|
118
114
|
APPLICATION_ID = /\A[a-zA-Z]\w*(\.[a-zA-Z]\w*)+\z/
|
|
119
115
|
|
|
120
|
-
def initialize(config:, root:, target:, template_dir: nil)
|
|
121
|
-
@config = config
|
|
122
|
-
@root = root
|
|
123
|
-
@target = target
|
|
124
|
-
@template_dir = template_dir
|
|
125
|
-
end
|
|
126
|
-
|
|
127
116
|
# Build the app and copy it into dist_dir. `dev` builds the debug variant
|
|
128
117
|
# and relaxes the remote-mode requirement (the dev URL is stamped into a
|
|
129
118
|
# debug asset each run — Android has no SIMCTL_CHILD_* equivalent).
|
|
@@ -147,8 +136,9 @@ module Everywhere
|
|
|
147
136
|
|
|
148
137
|
private
|
|
149
138
|
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
152
142
|
|
|
153
143
|
# The dev-server URL, for the debug source set only (EverywhereConfig's
|
|
154
144
|
# devUrl reads it, and release builds never look). This is the whole of
|
|
@@ -258,14 +248,6 @@ module Everywhere
|
|
|
258
248
|
env_value("EVERY_ANDROID_KEYSTORE_PASSWORD") }
|
|
259
249
|
end
|
|
260
250
|
|
|
261
|
-
# An env var that was set to nothing is the same as unset here: CI writes
|
|
262
|
-
# `EVERY_ANDROID_KEYSTORE: ${{ secrets.KEYSTORE }}` whether or not the
|
|
263
|
-
# secret exists, and an empty keystore path must not look configured.
|
|
264
|
-
def env_value(name)
|
|
265
|
-
value = ENV[name].to_s.strip
|
|
266
|
-
value.empty? ? nil : value
|
|
267
|
-
end
|
|
268
|
-
|
|
269
251
|
# Copy the template into the app's persistent work dir. Merged over
|
|
270
252
|
# whatever is already there, NOT wiped like the iOS stage: Gradle's
|
|
271
253
|
# incremental state (app/build/, .gradle/) lives *inside* this directory,
|
|
@@ -281,15 +263,10 @@ module Everywhere
|
|
|
281
263
|
# non-executable gradlew fails with a bare "Permission denied".
|
|
282
264
|
wrapper = File.join(work, "gradlew")
|
|
283
265
|
FileUtils.chmod("u+x", wrapper) if File.file?(wrapper)
|
|
284
|
-
|
|
266
|
+
mark_staged!(work)
|
|
285
267
|
work
|
|
286
268
|
end
|
|
287
269
|
|
|
288
|
-
def staged_version(work)
|
|
289
|
-
marker = File.join(work, STAGE_MARKER)
|
|
290
|
-
File.read(marker).strip if File.file?(marker)
|
|
291
|
-
end
|
|
292
|
-
|
|
293
270
|
# The whole per-app surface: identity properties, two JSON assets, colors,
|
|
294
271
|
# launcher icons, the icon font, the merged-in manifest, the Extensions
|
|
295
272
|
# sources + generated registry, and the Maven coordinate script. No build
|
|
@@ -565,7 +542,13 @@ module Everywhere
|
|
|
565
542
|
when Net::HTTPRedirection
|
|
566
543
|
raise "too many redirects for #{url}" if redirects <= 0
|
|
567
544
|
|
|
568
|
-
|
|
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)
|
|
569
552
|
when Net::HTTPSuccess
|
|
570
553
|
File.open(part, "wb") { |f| response.read_body { |chunk| f.write(chunk) } }
|
|
571
554
|
else
|
|
@@ -764,18 +747,9 @@ module Everywhere
|
|
|
764
747
|
FileUtils.rm_rf(dest)
|
|
765
748
|
FileUtils.mkdir_p(dest)
|
|
766
749
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
sources.each do |file|
|
|
770
|
-
relative = file.delete_prefix("#{source}/")
|
|
771
|
-
if File.basename(file) == "EverywhereExtensions.kt"
|
|
772
|
-
UI.die!("native/android/#{relative}: EverywhereExtensions.kt is generated by " \
|
|
773
|
-
"`every build` — declare your types under native.android in config/everywhere.yml instead")
|
|
774
|
-
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|
|
|
775
752
|
check_package!(file, relative)
|
|
776
|
-
target_file = File.join(dest, relative)
|
|
777
|
-
FileUtils.mkdir_p(File.dirname(target_file))
|
|
778
|
-
FileUtils.cp(file, target_file)
|
|
779
753
|
end
|
|
780
754
|
|
|
781
755
|
if @config.native_android?
|
|
@@ -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
|
|
@@ -4,6 +4,8 @@ require "fileutils"
|
|
|
4
4
|
require_relative "../paths"
|
|
5
5
|
require_relative "../ui"
|
|
6
6
|
require_relative "../version"
|
|
7
|
+
require_relative "base"
|
|
8
|
+
require_relative "native_sources"
|
|
7
9
|
|
|
8
10
|
module Everywhere
|
|
9
11
|
module Builders
|
|
@@ -40,11 +42,10 @@ module Everywhere
|
|
|
40
42
|
# Everything else in the work dir is the frozen template, copied as-is. In
|
|
41
43
|
# particular src/main.rs, src/extension_host.rs and capabilities/ are never
|
|
42
44
|
# rewritten — see support/desktop/README.md.
|
|
43
|
-
class Desktop
|
|
45
|
+
class Desktop < Base
|
|
44
46
|
# The generated module file, at every level of src/extensions. Reserved:
|
|
45
47
|
# an app shipping its own would be silently overwritten, so we refuse.
|
|
46
48
|
MODULE_FILE = "mod.rs"
|
|
47
|
-
STAGE_MARKER = ".everywhere-template"
|
|
48
49
|
CRATES_MARKER = "# __EVERYWHERE_APP_CRATES__"
|
|
49
50
|
|
|
50
51
|
# Where the app keeps its Rust, relative to the app root.
|
|
@@ -54,12 +55,6 @@ module Everywhere
|
|
|
54
55
|
def stamped? = stamped
|
|
55
56
|
end
|
|
56
57
|
|
|
57
|
-
def initialize(config:, root:, template_dir: nil)
|
|
58
|
-
@config = config
|
|
59
|
-
@root = root
|
|
60
|
-
@template_dir = template_dir
|
|
61
|
-
end
|
|
62
|
-
|
|
63
58
|
# Returns where to run cargo and what to set CARGO_TARGET_DIR to. Callers
|
|
64
59
|
# (`every dev`, `every build`) don't need to care which path they got.
|
|
65
60
|
def prepare!
|
|
@@ -76,8 +71,6 @@ module Everywhere
|
|
|
76
71
|
|
|
77
72
|
private
|
|
78
73
|
|
|
79
|
-
def bundle_id = @config.bundle_id
|
|
80
|
-
|
|
81
74
|
# Config errors first: they're free to check, and everything below either
|
|
82
75
|
# interpolates these values into generated Rust or spends minutes on cargo.
|
|
83
76
|
def preflight!
|
|
@@ -109,15 +102,10 @@ module Everywhere
|
|
|
109
102
|
FileUtils.rm_rf(work) unless staged_version(work) == Everywhere::VERSION
|
|
110
103
|
FileUtils.mkdir_p(work)
|
|
111
104
|
FileUtils.cp_r(File.join(File.dirname(template), "."), work)
|
|
112
|
-
|
|
105
|
+
mark_staged!(work)
|
|
113
106
|
File.join(work, File.basename(template))
|
|
114
107
|
end
|
|
115
108
|
|
|
116
|
-
def staged_version(work)
|
|
117
|
-
marker = File.join(work, STAGE_MARKER)
|
|
118
|
-
File.read(marker).strip if File.file?(marker)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
109
|
def stamp!(work)
|
|
122
110
|
write_cargo_toml(work)
|
|
123
111
|
write_extensions(work)
|
|
@@ -194,17 +182,13 @@ module Everywhere
|
|
|
194
182
|
FileUtils.mkdir_p(dest)
|
|
195
183
|
|
|
196
184
|
source = File.join(@root, SOURCE_DIR)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
185
|
+
# mod.rs is reserved here rather than by NativeSources: the fix isn't to
|
|
186
|
+
# declare the file in everywhere.yml, it's to delete it.
|
|
187
|
+
sources = NativeSources.copy!(from: source, into: dest, ext: "rs") do |_path, relative|
|
|
200
188
|
if File.basename(relative) == MODULE_FILE
|
|
201
189
|
UI.die!("#{File.join(SOURCE_DIR, relative)} is generated by `every build` — " \
|
|
202
190
|
"remove it and let the CLI write the module declarations")
|
|
203
191
|
end
|
|
204
|
-
|
|
205
|
-
target = File.join(dest, relative)
|
|
206
|
-
FileUtils.mkdir_p(File.dirname(target))
|
|
207
|
-
FileUtils.cp(path, target)
|
|
208
192
|
end
|
|
209
193
|
|
|
210
194
|
modules = module_tree(sources.map { |path| path.delete_prefix("#{source}/") })
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require_relative "../host"
|
|
4
5
|
require_relative "../paths"
|
|
5
6
|
require_relative "../shellout"
|
|
6
7
|
require_relative "../log_filter"
|
|
7
8
|
require_relative "../icon"
|
|
8
9
|
require_relative "../asset_catalog"
|
|
10
|
+
require_relative "../plist"
|
|
9
11
|
require_relative "../ui"
|
|
12
|
+
require_relative "base"
|
|
13
|
+
require_relative "native_sources"
|
|
10
14
|
|
|
11
15
|
module Everywhere
|
|
12
16
|
module Builders
|
|
@@ -18,7 +22,7 @@ module Everywhere
|
|
|
18
22
|
# Two products come out of the same stamped project: the Simulator .app
|
|
19
23
|
# (ad-hoc signed, no certs) and, with device: true, a signed .ipa via
|
|
20
24
|
# xcodebuild archive + -exportArchive — see platform/docs/build-engine.md §5.
|
|
21
|
-
class Ios
|
|
25
|
+
class Ios < Base
|
|
22
26
|
# Fixed by the template contract: the target/product is always "App"
|
|
23
27
|
# (App.app), so build paths never depend on per-app values.
|
|
24
28
|
SCHEME = "App"
|
|
@@ -48,10 +52,7 @@ module Everywhere
|
|
|
48
52
|
}.freeze
|
|
49
53
|
|
|
50
54
|
def initialize(config:, root:, target:, template_dir: nil, channel: "direct")
|
|
51
|
-
|
|
52
|
-
@root = root
|
|
53
|
-
@target = target
|
|
54
|
-
@template_dir = template_dir
|
|
55
|
+
super(config: config, root: root, target: target, template_dir: template_dir)
|
|
55
56
|
@channel = channel.to_s
|
|
56
57
|
end
|
|
57
58
|
|
|
@@ -62,6 +63,7 @@ module Everywhere
|
|
|
62
63
|
# Returns the path of the built product.
|
|
63
64
|
def build!(dist_dir:, dev: false, device: false)
|
|
64
65
|
preflight!(dev: dev, device: device)
|
|
66
|
+
@device = device
|
|
65
67
|
template = @template_dir ? File.expand_path(@template_dir) : Paths.ios_dir!
|
|
66
68
|
work = stage(template)
|
|
67
69
|
stamp!(work)
|
|
@@ -77,15 +79,12 @@ module Everywhere
|
|
|
77
79
|
|
|
78
80
|
private
|
|
79
81
|
|
|
80
|
-
def name = @config.name(target: @target)
|
|
81
|
-
def bundle_id = @config.bundle_id(target: @target)
|
|
82
|
-
|
|
83
82
|
def preflight!(dev:, device:)
|
|
84
83
|
unless dev || @config.remote?
|
|
85
84
|
UI.die!("iOS apps are remote mode only for now — set remote.url in config/everywhere.yml " \
|
|
86
85
|
"(the shell wraps your deployed app; nothing is packaged locally)")
|
|
87
86
|
end
|
|
88
|
-
UI.die!("iOS builds need macOS with Xcode installed") unless
|
|
87
|
+
UI.die!("iOS builds need macOS with Xcode installed") unless Host.darwin?
|
|
89
88
|
return device_preflight! if device
|
|
90
89
|
return if Shellout.run?("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path", quiet: true)
|
|
91
90
|
|
|
@@ -102,7 +101,7 @@ module Everywhere
|
|
|
102
101
|
"and run: xcode-select --switch /Applications/Xcode.app")
|
|
103
102
|
end
|
|
104
103
|
|
|
105
|
-
missing = REQUIRED_SIGNING_ENV.reject { |name, _|
|
|
104
|
+
missing = REQUIRED_SIGNING_ENV.reject { |name, _| env_value(name) }
|
|
106
105
|
unless missing.empty?
|
|
107
106
|
UI.die!("device builds need signing credentials in the environment:\n" \
|
|
108
107
|
"#{missing.map { |name, hint| " #{name} — #{hint}" }.join("\n")}")
|
|
@@ -129,20 +128,15 @@ module Everywhere
|
|
|
129
128
|
"#{DEFAULT_EXPORT_METHOD} builds — unset EVERY_IOS_EXPORT_METHOD=#{export_method}")
|
|
130
129
|
end
|
|
131
130
|
|
|
132
|
-
def
|
|
133
|
-
value = ENV[name].to_s.strip
|
|
134
|
-
value.empty? ? nil : value
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def asc_env(field) = ASC_ENV.fetch(field).filter_map { |name| signing_env(name) }.first
|
|
131
|
+
def asc_env(field) = ASC_ENV.fetch(field).filter_map { |name| env_value(name) }.first
|
|
138
132
|
|
|
139
133
|
def store_channel? = STORE_CHANNELS.include?(@channel)
|
|
140
134
|
def upload_to_asc? = store_channel? && ASC_ENV.each_key.all? { |field| asc_env(field) }
|
|
141
135
|
|
|
142
|
-
def team_id =
|
|
143
|
-
def provisioning_profile =
|
|
144
|
-
def signing_identity =
|
|
145
|
-
def export_method =
|
|
136
|
+
def team_id = env_value("EVERY_IOS_TEAM_ID")
|
|
137
|
+
def provisioning_profile = env_value("EVERY_IOS_PROVISIONING_PROFILE")
|
|
138
|
+
def signing_identity = env_value("APPLE_SIGNING_IDENTITY")
|
|
139
|
+
def export_method = env_value("EVERY_IOS_EXPORT_METHOD") || DEFAULT_EXPORT_METHOD
|
|
146
140
|
|
|
147
141
|
# Copy the template into the app's persistent work dir. Persistent (not
|
|
148
142
|
# a tmpdir) so the project path is stable — Xcode's incremental state
|
|
@@ -189,7 +183,7 @@ module Everywhere
|
|
|
189
183
|
return unless @config.deep_linking? && domains.any?
|
|
190
184
|
|
|
191
185
|
values = domains.flat_map { |host| ["applinks:#{host}", "webcredentials:#{host}"] }
|
|
192
|
-
entries = values.map { |v| "\t\t<string>#{v}</string>" }.join("\n")
|
|
186
|
+
entries = values.map { |v| "\t\t<string>#{xml_escape(v)}</string>" }.join("\n")
|
|
193
187
|
UI.step("stamping associated domains #{UI.dim("(#{domains.join(", ")})")}")
|
|
194
188
|
File.write(File.join(work, "App", "App.entitlements"), <<~PLIST)
|
|
195
189
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -215,19 +209,8 @@ module Everywhere
|
|
|
215
209
|
UI.die!("native.ios in config/everywhere.yml:\n#{errors.join("\n")}") unless errors.empty?
|
|
216
210
|
|
|
217
211
|
dest = File.join(work, "App", "Extensions")
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
sources.each do |file|
|
|
222
|
-
relative = file.delete_prefix("#{source}/")
|
|
223
|
-
if File.basename(file) == "EverywhereExtensions.swift"
|
|
224
|
-
UI.die!("native/ios/#{relative}: EverywhereExtensions.swift is generated by " \
|
|
225
|
-
"`every build` — declare your types under native.ios in config/everywhere.yml instead")
|
|
226
|
-
end
|
|
227
|
-
target_file = File.join(dest, relative)
|
|
228
|
-
FileUtils.mkdir_p(File.dirname(target_file))
|
|
229
|
-
FileUtils.cp(file, target_file)
|
|
230
|
-
end
|
|
212
|
+
sources = NativeSources.copy!(from: File.join(@root, "native", "ios"), into: dest, ext: "swift",
|
|
213
|
+
reserved: "EverywhereExtensions.swift", label: "native/ios")
|
|
231
214
|
|
|
232
215
|
if @config.native_ios?
|
|
233
216
|
UI.step("bundling native extensions #{UI.dim("(#{sources.length} Swift files from native/ios)")}")
|
|
@@ -439,15 +422,38 @@ module Everywhere
|
|
|
439
422
|
File.write(File.join(work, "App", "App.xcconfig"), <<~XCCONFIG)
|
|
440
423
|
// Written by `every build --target #{@target}` — do not edit; changes
|
|
441
424
|
// belong in config/everywhere.yml.
|
|
442
|
-
PRODUCT_BUNDLE_IDENTIFIER = #{bundle_id}
|
|
443
|
-
MARKETING_VERSION = #{@config.version(target: @target)}
|
|
425
|
+
PRODUCT_BUNDLE_IDENTIFIER = #{xcconfig_value(bundle_id)}
|
|
426
|
+
MARKETING_VERSION = #{xcconfig_value(@config.version(target: @target))}
|
|
444
427
|
CURRENT_PROJECT_VERSION = 1
|
|
445
|
-
INFOPLIST_KEY_CFBundleDisplayName = #{name}
|
|
446
|
-
DEVELOPMENT_TEAM =#{" #{team_id}" if team_id}
|
|
447
|
-
#{entitlements}
|
|
428
|
+
INFOPLIST_KEY_CFBundleDisplayName = #{xcconfig_value(name)}
|
|
429
|
+
DEVELOPMENT_TEAM =#{" #{xcconfig_value(team_id)}" if team_id}
|
|
430
|
+
#{entitlements}#{device_signing_xcconfig}
|
|
448
431
|
XCCONFIG
|
|
449
432
|
end
|
|
450
433
|
|
|
434
|
+
# Signing lives here, not on the xcodebuild command line: command-line
|
|
435
|
+
# build settings apply to every target in the build — including SPM
|
|
436
|
+
# resource-bundle targets (HotwireNative_HotwireNative), which reject
|
|
437
|
+
# PROVISIONING_PROFILE_SPECIFIER outright. The xcconfig is scoped to the
|
|
438
|
+
# App target.
|
|
439
|
+
def device_signing_xcconfig
|
|
440
|
+
return unless @device
|
|
441
|
+
|
|
442
|
+
identity = signing_identity ||
|
|
443
|
+
(export_method == "development" ? "Apple Development" : "Apple Distribution")
|
|
444
|
+
<<~XCCONFIG
|
|
445
|
+
CODE_SIGN_STYLE = Manual
|
|
446
|
+
PROVISIONING_PROFILE_SPECIFIER = #{xcconfig_value(provisioning_profile)}
|
|
447
|
+
CODE_SIGN_IDENTITY = #{xcconfig_value(identity)}
|
|
448
|
+
XCCONFIG
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# An xcconfig is line-oriented: a value carrying a newline doesn't produce
|
|
452
|
+
# a broken setting, it produces an ADDITIONAL one of the author's
|
|
453
|
+
# choosing (OTHER_CODE_SIGN_FLAGS, a run-script path, …). Strip rather
|
|
454
|
+
# than die — the same values feed non-xcconfig writers that don't care.
|
|
455
|
+
def xcconfig_value(value) = value.to_s.gsub(/[\r\n]+/, " ").strip
|
|
456
|
+
|
|
451
457
|
def write_app_icon(work)
|
|
452
458
|
source = @config.icon
|
|
453
459
|
unless source && File.exist?(source)
|
|
@@ -498,11 +504,8 @@ module Everywhere
|
|
|
498
504
|
log: log, filter: LogFilter.for(:xcodebuild))
|
|
499
505
|
end
|
|
500
506
|
|
|
501
|
-
# Archive for a real device.
|
|
502
|
-
# command-line build settings
|
|
503
|
-
# the template ships CODE_SIGN_STYLE = Automatic and the stamp-only
|
|
504
|
-
# contract forbids editing its pbxproj. CODE_SIGN_IDENTITY is left to
|
|
505
|
-
# xcodebuild ("Apple Distribution") unless the runner names one.
|
|
507
|
+
# Archive for a real device. Signing arrives via the stamped xcconfig
|
|
508
|
+
# (device_signing_xcconfig), never as command-line build settings.
|
|
506
509
|
def archive!(work, dist_dir)
|
|
507
510
|
UI.step("archiving iOS app #{UI.dim("(xcodebuild archive, team #{team_id})")}")
|
|
508
511
|
FileUtils.mkdir_p(dist_dir)
|
|
@@ -515,11 +518,7 @@ module Everywhere
|
|
|
515
518
|
"-destination", "generic/platform=iOS",
|
|
516
519
|
"-archivePath", archive_path(work),
|
|
517
520
|
"-derivedDataPath", Paths.ios_derived_data_dir,
|
|
518
|
-
"-clonedSourcePackagesDirPath", Paths.ios_packages_dir,
|
|
519
|
-
"CODE_SIGN_STYLE=Manual",
|
|
520
|
-
"DEVELOPMENT_TEAM=#{team_id}",
|
|
521
|
-
"PROVISIONING_PROFILE_SPECIFIER=#{provisioning_profile}",
|
|
522
|
-
*("CODE_SIGN_IDENTITY=#{signing_identity}" if signing_identity)],
|
|
521
|
+
"-clonedSourcePackagesDirPath", Paths.ios_packages_dir],
|
|
523
522
|
log: File.join(dist_dir, "ios-archive.log"), filter: LogFilter.for(:xcodebuild))
|
|
524
523
|
end
|
|
525
524
|
|
|
@@ -635,9 +634,7 @@ module Everywhere
|
|
|
635
634
|
path
|
|
636
635
|
end
|
|
637
636
|
|
|
638
|
-
|
|
639
|
-
# the one interpolated plist value that can't be trusted as XML.
|
|
640
|
-
def xml_escape(value) = value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
637
|
+
def xml_escape(value) = Everywhere::Plist.escape(value)
|
|
641
638
|
|
|
642
639
|
def collect_ipa(export, dist_dir)
|
|
643
640
|
built = File.join(export, "App.ipa")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "../ui"
|
|
5
|
+
|
|
6
|
+
module Everywhere
|
|
7
|
+
module Builders
|
|
8
|
+
# The half of the native-extension seam that is the same on all three
|
|
9
|
+
# platforms: copy the app repo's native/<platform>/**/*.<ext> into the
|
|
10
|
+
# staged template, preserving relative paths. What the copied files then
|
|
11
|
+
# have to satisfy (a Kotlin package line) and what gets generated alongside
|
|
12
|
+
# them (a registry, mod.rs) is per-platform and stays in the builders.
|
|
13
|
+
module NativeSources
|
|
14
|
+
# `reserved` is the generated file's own name: an app shipping one would
|
|
15
|
+
# be overwritten by the generator, so we refuse instead. `label` is the
|
|
16
|
+
# source dir as the app sees it ("native/ios"), which doubles as the
|
|
17
|
+
# everywhere.yml key it maps to (native.ios). The block, when given, sees
|
|
18
|
+
# each file before it is copied. Returns the source paths, sorted.
|
|
19
|
+
def self.copy!(from:, into:, ext:, reserved: nil, label: nil)
|
|
20
|
+
sources = File.directory?(from) ? Dir.glob(File.join(from, "**", "*.#{ext}")).sort : []
|
|
21
|
+
sources.each do |file|
|
|
22
|
+
relative = file.delete_prefix("#{from}/")
|
|
23
|
+
if reserved && File.basename(file) == reserved
|
|
24
|
+
UI.die!("#{label}/#{relative}: #{reserved} is generated by " \
|
|
25
|
+
"`every build` — declare your types under #{label.tr("/", ".")} " \
|
|
26
|
+
"in config/everywhere.yml instead")
|
|
27
|
+
end
|
|
28
|
+
yield(file, relative) if block_given?
|
|
29
|
+
|
|
30
|
+
target = File.join(into, relative)
|
|
31
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
32
|
+
FileUtils.cp(file, target)
|
|
33
|
+
end
|
|
34
|
+
sources
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "clock"
|
|
4
|
+
|
|
3
5
|
module Everywhere
|
|
4
6
|
# A thread-scoped registry of the long-running children a task spawned, so the
|
|
5
7
|
# task can be cancelled without hunting pids by hand.
|
|
@@ -38,8 +40,8 @@ module Everywhere
|
|
|
38
40
|
return [] if pids.empty?
|
|
39
41
|
|
|
40
42
|
pids.each { |pid| signal(pid, "TERM") }
|
|
41
|
-
deadline =
|
|
42
|
-
sleep(0.1) while pids.any? { |pid| alive?(pid) } &&
|
|
43
|
+
deadline = Clock.monotonic + grace
|
|
44
|
+
sleep(0.1) while pids.any? { |pid| alive?(pid) } && Clock.monotonic < deadline
|
|
43
45
|
pids.select { |pid| alive?(pid) }.each { |pid| signal(pid, "KILL") }
|
|
44
46
|
end
|
|
45
47
|
|
|
@@ -64,8 +66,6 @@ module Everywhere
|
|
|
64
66
|
true
|
|
65
67
|
end
|
|
66
68
|
|
|
67
|
-
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
68
|
-
|
|
69
69
|
def list
|
|
70
70
|
Thread.current.thread_variable_get(PIDS) ||
|
|
71
71
|
Thread.current.thread_variable_set(PIDS, [])
|