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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/exe/every +2 -13
  3. data/exe/rbe +2 -13
  4. data/lib/everywhere/boot.rb +9 -2
  5. data/lib/everywhere/builders/android.rb +16 -42
  6. data/lib/everywhere/builders/base.rb +53 -0
  7. data/lib/everywhere/builders/desktop.rb +7 -23
  8. data/lib/everywhere/builders/ios.rb +49 -52
  9. data/lib/everywhere/builders/native_sources.rb +38 -0
  10. data/lib/everywhere/child_processes.rb +4 -4
  11. data/lib/everywhere/child_supervision.rb +172 -0
  12. data/lib/everywhere/clock.rb +12 -0
  13. data/lib/everywhere/commands/build.rb +20 -9
  14. data/lib/everywhere/commands/dev.rb +59 -284
  15. data/lib/everywhere/commands/doctor.rb +6 -6
  16. data/lib/everywhere/commands/install.rb +1 -0
  17. data/lib/everywhere/commands/platform/auth_status.rb +1 -0
  18. data/lib/everywhere/commands/platform/build.rb +5 -6
  19. data/lib/everywhere/commands/platform/login.rb +4 -4
  20. data/lib/everywhere/commands/platform/logout.rb +1 -0
  21. data/lib/everywhere/commands/platform/runner.rb +62 -7
  22. data/lib/everywhere/commands/preview.rb +24 -97
  23. data/lib/everywhere/commands/publish.rb +2 -0
  24. data/lib/everywhere/commands/release.rb +14 -11
  25. data/lib/everywhere/commands/shell_dir.rb +2 -0
  26. data/lib/everywhere/commands/updates_keygen.rb +25 -1
  27. data/lib/everywhere/config/app.rb +126 -0
  28. data/lib/everywhere/config/auth.rb +108 -0
  29. data/lib/everywhere/config/data.rb +50 -0
  30. data/lib/everywhere/config/deep_linking.rb +107 -0
  31. data/lib/everywhere/config/desktop_ui.rb +153 -0
  32. data/lib/everywhere/config/mobile.rb +211 -0
  33. data/lib/everywhere/config/native_desktop.rb +168 -0
  34. data/lib/everywhere/config/native_mobile.rb +337 -0
  35. data/lib/everywhere/config/shell.rb +57 -0
  36. data/lib/everywhere/config/updates.rb +63 -0
  37. data/lib/everywhere/config.rb +30 -1423
  38. data/lib/everywhere/desktop_dev_app.rb +138 -0
  39. data/lib/everywhere/dock/state.rb +3 -1
  40. data/lib/everywhere/entrypoint.rb +24 -0
  41. data/lib/everywhere/error.rb +8 -0
  42. data/lib/everywhere/framework.rb +2 -2
  43. data/lib/everywhere/host.rb +11 -0
  44. data/lib/everywhere/ignore.rb +11 -5
  45. data/lib/everywhere/line_pump.rb +3 -1
  46. data/lib/everywhere/minisign.rb +1 -0
  47. data/lib/everywhere/native_platform.rb +44 -0
  48. data/lib/everywhere/paths.rb +23 -7
  49. data/lib/everywhere/platform/client.rb +19 -1
  50. data/lib/everywhere/platform/snapshot.rb +6 -4
  51. data/lib/everywhere/plist.rb +17 -0
  52. data/lib/everywhere/png.rb +1 -0
  53. data/lib/everywhere/raw_tty.rb +51 -0
  54. data/lib/everywhere/s3.rb +1 -0
  55. data/lib/everywhere/shell_pages.rb +109 -0
  56. data/lib/everywhere/tab_filter.rb +36 -0
  57. data/lib/everywhere/task_pool.rb +3 -4
  58. data/lib/everywhere/ui.rb +6 -1
  59. data/lib/everywhere/version.rb +6 -4
  60. data/lib/everywhere.rb +1 -2
  61. data/support/mobile/ios/App.xcodeproj/project.pbxproj +0 -2
  62. data/support/release/macos/notarize.sh +3 -0
  63. metadata +24 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83b4d5f0135bc80870fdacccaf5014ee478cbaa2bad08541871ab67a6485d3fc
4
- data.tar.gz: dd86792cae35b098877012c42ebe0286ada230909a8588c9a4c933c7c8771137
3
+ metadata.gz: a1644d6c9be14fdb8ef738eaee6f33225e5916ea401945f7c6c494e8963a2c16
4
+ data.tar.gz: 03621b7846f16b5621a352e7d4fc42ac38c8859095fc1be1098eef48b081fb0f
5
5
  SHA512:
6
- metadata.gz: 97ad2a4b22e6255abcf6e68850ef9c2615ca725bfac46f62db9d7f2423314afdd882088589670cd609e3f9a8da854616b1b7af0015dd29e2bafff587664ff838
7
- data.tar.gz: ec09e6586570025a8e02acb4d61f323f841201441341de7aa1cf3cfa621c74d331d1e48794e6e88e87990511e4a215233b990c86cfca11829aa52be0ea82b12a
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/cli"
5
- require "everywhere/ui"
4
+ require "everywhere/entrypoint"
6
5
 
7
- begin
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/cli"
5
- require "everywhere/ui"
4
+ require "everywhere/entrypoint"
6
5
 
7
- begin
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
@@ -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
- File.write(secret_file, SecureRandom.hex(64))
145
- File.chmod(0o600, secret_file)
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
- def name = @config.name(target: @target)
151
- def application_id = @config.bundle_id(target: @target)
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
- File.write(File.join(work, STAGE_MARKER), "#{Everywhere::VERSION}\n")
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
- return fetch!(response["location"], dest, sha256: sha256, redirects: redirects - 1)
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
- source = File.join(@root, "native", "android")
768
- sources = File.directory?(source) ? Dir.glob(File.join(source, "**", "*.kt")).sort : []
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
- File.write(File.join(work, STAGE_MARKER), "#{Everywhere::VERSION}\n")
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
- sources = File.directory?(source) ? Dir.glob(File.join(source, "**", "*.rs")).sort : []
198
- sources.each do |path|
199
- relative = path.delete_prefix("#{source}/")
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
- @config = config
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 RUBY_PLATFORM.include?("darwin")
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, _| signing_env(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 signing_env(name)
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 = signing_env("EVERY_IOS_TEAM_ID")
143
- def provisioning_profile = signing_env("EVERY_IOS_PROVISIONING_PROFILE")
144
- def signing_identity = signing_env("APPLE_SIGNING_IDENTITY")
145
- def export_method = signing_env("EVERY_IOS_EXPORT_METHOD") || DEFAULT_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
- source = File.join(@root, "native", "ios")
219
- sources = File.directory?(source) ? Dir.glob(File.join(source, "**", "*.swift")).sort : []
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. The signing settings are passed as
502
- # command-line build settings because those override the project file —
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
- # Profile names are free-form and come from outside the CLI, so this is
639
- # the one interpolated plist value that can't be trusted as XML.
640
- def xml_escape(value) = value.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
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 = now + grace
42
- sleep(0.1) while pids.any? { |pid| alive?(pid) } && now < deadline
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, [])