ruby_everywhere 0.12.0 → 0.14.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -33
  3. data/bridge/README.md +23 -23
  4. data/lib/everywhere/android_resources.rb +9 -9
  5. data/lib/everywhere/android_sdk.rb +9 -9
  6. data/lib/everywhere/asset_catalog.rb +7 -7
  7. data/lib/everywhere/blake2b.rb +1 -1
  8. data/lib/everywhere/boot.rb +1 -1
  9. data/lib/everywhere/builders/android.rb +25 -25
  10. data/lib/everywhere/builders/desktop.rb +6 -6
  11. data/lib/everywhere/builders/ios.rb +14 -14
  12. data/lib/everywhere/builders/native_sources.rb +1 -1
  13. data/lib/everywhere/child_supervision.rb +1 -1
  14. data/lib/everywhere/commands/build.rb +54 -24
  15. data/lib/everywhere/commands/clean.rb +1 -1
  16. data/lib/everywhere/commands/dev.rb +11 -11
  17. data/lib/everywhere/commands/doctor.rb +7 -7
  18. data/lib/everywhere/commands/icon.rb +2 -2
  19. data/lib/everywhere/commands/install.rb +21 -21
  20. data/lib/everywhere/commands/logs.rb +6 -6
  21. data/lib/everywhere/commands/platform/auth_status.rb +3 -3
  22. data/lib/everywhere/commands/platform/build.rb +7 -7
  23. data/lib/everywhere/commands/platform/login.rb +5 -5
  24. data/lib/everywhere/commands/platform/logout.rb +1 -1
  25. data/lib/everywhere/commands/platform/runner.rb +6 -6
  26. data/lib/everywhere/commands/preview.rb +14 -14
  27. data/lib/everywhere/commands/publish.rb +14 -14
  28. data/lib/everywhere/commands/release.rb +13 -9
  29. data/lib/everywhere/commands/updates_keygen.rb +4 -4
  30. data/lib/everywhere/config/app.rb +3 -3
  31. data/lib/everywhere/config/mobile.rb +2 -2
  32. data/lib/everywhere/config/native_desktop.rb +2 -2
  33. data/lib/everywhere/config/native_mobile.rb +3 -3
  34. data/lib/everywhere/config/updates.rb +11 -1
  35. data/lib/everywhere/database.rb +3 -3
  36. data/lib/everywhere/desktop_assets.rb +5 -5
  37. data/lib/everywhere/desktop_dev_app.rb +1 -1
  38. data/lib/everywhere/emulator.rb +5 -5
  39. data/lib/everywhere/log_filter.rb +1 -1
  40. data/lib/everywhere/omniauth.rb +1 -1
  41. data/lib/everywhere/paths.rb +1 -1
  42. data/lib/everywhere/platform/credentials.rb +1 -1
  43. data/lib/everywhere/request_context.rb +2 -2
  44. data/lib/everywhere/shellout.rb +2 -2
  45. data/lib/everywhere/simulator.rb +1 -1
  46. data/lib/everywhere/tab_filter.rb +2 -2
  47. data/lib/everywhere/version.rb +1 -1
  48. data/support/agents/AGENTS.md +7 -7
  49. data/support/agents/frameworks/hanami-views.md +8 -8
  50. data/support/agents/frameworks/hanami.md +2 -2
  51. data/support/agents/frameworks/rails-views.md +7 -7
  52. data/support/agents/frameworks/rails.md +8 -8
  53. data/support/agents/frameworks/sinatra-views.md +7 -7
  54. data/support/agents/frameworks/sinatra.md +2 -2
  55. data/support/agents/modes/local.md +2 -2
  56. data/support/agents/modes/remote.md +2 -2
  57. data/support/desktop/src-tauri/src/updater.rs +1 -1
  58. metadata +1 -1
@@ -173,7 +173,7 @@ module Everywhere
173
173
  @version_code = version_code
174
174
 
175
175
  unless dev || @config.remote?
176
- UI.die!("Android apps are remote mode only for now set remote.url in config/everywhere.yml " \
176
+ UI.die!("Android apps are remote mode only for now. Set remote.url in config/everywhere.yml " \
177
177
  "(the shell wraps your deployed app; nothing is packaged locally)")
178
178
  end
179
179
 
@@ -184,7 +184,7 @@ module Everywhere
184
184
  # package name. Caught here so the answer is a config key, not a
185
185
  # Gradle stacktrace.
186
186
  unless application_id.match?(APPLICATION_ID)
187
- UI.die!("#{application_id.inspect} can't be an Android applicationId it needs two or " \
187
+ UI.die!("#{application_id.inspect} can't be an Android applicationId. It needs two or " \
188
188
  "more dot-separated segments of letters, digits and _, each starting with a " \
189
189
  "letter. Set platforms.android.bundle_id in config/everywhere.yml")
190
190
  end
@@ -208,12 +208,12 @@ module Everywhere
208
208
 
209
209
  def check_format!(dev:, format:)
210
210
  unless FORMATS.key?(format)
211
- UI.die!("#{format.inspect} isn't an Android package format :apk (installable) or :aab " \
211
+ UI.die!("#{format.inspect} isn't an Android package format. Use :apk (installable) or :aab " \
212
212
  "(what Google Play accepts)")
213
213
  end
214
214
  return unless format == :aab && dev
215
215
 
216
- UI.die!("an .aab can't be installed on a device or an emulator — it's an upload format Play " \
216
+ UI.die!("an .aab can't be installed on a device or an emulator. It's an upload format Play " \
217
217
  "re-signs into per-device APKs. `every dev` builds an APK; ask for the bundle when " \
218
218
  "you're publishing")
219
219
  end
@@ -231,7 +231,7 @@ module Everywhere
231
231
  missing = REQUIRED_SIGNING_ENV.reject { |name| env_value(name) }
232
232
  unless missing.empty?
233
233
  UI.die!("EVERY_ANDROID_KEYSTORE is set but #{missing.join(" and ")} " \
234
- "#{missing.one? ? "is" : "are"} not signing needs the keystore, its password " \
234
+ "#{missing.one? ? "is" : "are"} not. Signing needs the keystore, its password " \
235
235
  "and the alias of the key inside it (EVERY_ANDROID_KEY_PASSWORD falls back to " \
236
236
  "the keystore password)")
237
237
  end
@@ -316,7 +316,7 @@ module Everywhere
316
316
  xml = <<~XML
317
317
  <?xml version="1.0" encoding="utf-8"?>
318
318
  <!-- Written by `every build` because config/everywhere.yml sets
319
- remote.instances: true previewed apps arrive as plain http on
319
+ remote.instances: true, and previewed apps arrive as plain http on
320
320
  LAN addresses that cannot be enumerated at build time. Do not
321
321
  edit; the template's strict default returns if instances is
322
322
  turned off. -->
@@ -344,7 +344,7 @@ module Everywhere
344
344
  # opted in, so the shell's own tested level is the default.
345
345
  def write_properties(work)
346
346
  File.write(File.join(work, "app", "everywhere.properties"), <<~PROPERTIES)
347
- # Written by `every build --target #{@target}` do not edit; changes
347
+ # Written by `every build --target #{@target}`. Do not edit; changes
348
348
  # belong in config/everywhere.yml.
349
349
  everywhere.applicationId=#{properties_value(application_id)}
350
350
  everywhere.versionName=#{properties_value(@config.version(target: @target))}
@@ -365,7 +365,7 @@ module Everywhere
365
365
  override = env_value(VERSION_CODE_ENV) or return derived_version_code
366
366
 
367
367
  unless override.match?(/\A[1-9]\d*\z/)
368
- UI.die!("#{VERSION_CODE_ENV}=#{override.inspect} a versionCode is a positive integer, " \
368
+ UI.die!("#{VERSION_CODE_ENV}=#{override.inspect}: a versionCode is a positive integer, " \
369
369
  "and Play only accepts an upload whose code is higher than the last one's")
370
370
  end
371
371
  override.to_i
@@ -431,7 +431,7 @@ module Everywhere
431
431
  def write_launcher_icons(work)
432
432
  source = @config.icon
433
433
  unless source && File.exist?(source)
434
- UI.warn "no app icon (set app.icon or add icon.png) shipping the placeholder"
434
+ UI.warn "no app icon (set app.icon or add icon.png). Shipping the placeholder"
435
435
  return
436
436
  end
437
437
 
@@ -439,7 +439,7 @@ module Everywhere
439
439
  res = File.join(work, "app", "src", "main", "res")
440
440
  return if Icon.write_android_mipmaps(source, res, background: background)
441
441
 
442
- UI.warn "couldn't read #{File.basename(source)} shipping the placeholder icon"
442
+ UI.warn "couldn't read #{File.basename(source)}. Shipping the placeholder icon"
443
443
  end
444
444
 
445
445
  # App-supplied images: native/android/assets/** → drawable resources, so
@@ -495,12 +495,12 @@ module Everywhere
495
495
  return if tabs.length <= BAR_SLOTS
496
496
 
497
497
  overflow = tabs.drop(BAR_TABS_WITH_MORE).map { |tab| tab["title"] }
498
- UI.detail "#{tabs.length} tabs: Android's bar holds #{BAR_TABS_WITH_MORE} plus a \"More\" tab " \
498
+ UI.detail "#{tabs.length} tabs: Android's bar holds #{BAR_TABS_WITH_MORE} plus a \"More\" tab. " \
499
499
  "#{overflow.map(&:inspect).join(", ")} open from there (iOS does the same past five)"
500
500
 
501
501
  return if tabs.length <= CROWDED_TABS
502
502
 
503
- UI.warn "#{overflow.length} tabs behind \"More\" is a long list to scroll on a phone " \
503
+ UI.warn "#{overflow.length} tabs behind \"More\" is a long list to scroll on a phone. " \
504
504
  "consider moving the rarer ones into the app's own navigation"
505
505
  end
506
506
 
@@ -514,7 +514,7 @@ module Everywhere
514
514
  def resolve_icon_font!(template)
515
515
  file = @config.native_android_icon_font_file
516
516
  @icon_font = nil
517
- return unless file # icon_font: none the app brings its own drawables
517
+ return unless file # icon_font: none, so the app brings its own drawables
518
518
 
519
519
  @icon_font = if @config.native_android_icon_font_fetched?
520
520
  fetch_icon_font!(file)
@@ -529,7 +529,7 @@ module Everywhere
529
529
  dir = BUNDLED_FONT_DIRS.map { |parts| File.join(template, *parts) }
530
530
  .find { |candidate| File.file?(File.join(candidate, file)) }
531
531
  unless dir
532
- UI.die!("the Android template has no #{file} reinstall ruby_everywhere, or set " \
532
+ UI.die!("the Android template has no #{file}. Reinstall ruby_everywhere, or set " \
533
533
  "native.android.icon_font: symbols in config/everywhere.yml to fetch one instead")
534
534
  end
535
535
 
@@ -557,7 +557,7 @@ module Everywhere
557
557
  self.class.fetch!(icon_font_url(pin["path"], ".codepoints"), codepoints, sha256: pin["codepoints"])
558
558
  resolved
559
559
  rescue StandardError => e
560
- UI.die!("couldn't fetch the #{@config.native_android_icon_font} icon font (#{e.message}) " \
560
+ UI.die!("couldn't fetch the #{@config.native_android_icon_font} icon font (#{e.message}). " \
561
561
  "set native.android.icon_font: classic in config/everywhere.yml to use the 2,235-icon " \
562
562
  "set bundled in the gem, which needs no download and works offline")
563
563
  end
@@ -630,8 +630,8 @@ module Everywhere
630
630
  icon = tab["icon"].to_s
631
631
  base = icon.sub(/\.fill\z/, "")
632
632
  if base != icon && !fillable
633
- "tab #{tab["title"].inspect}: #{icon.inspect} icon_font: #{@config.native_android_icon_font} " \
634
- "has no FILL axis; use #{base.inspect}, or switch to icon_font: symbols"
633
+ "tab #{tab["title"].inspect}: #{icon.inspect} needs a FILL axis, which icon_font: " \
634
+ "#{@config.native_android_icon_font} lacks; use #{base.inspect}, or switch to icon_font: symbols"
635
635
  elsif !names.include?(base)
636
636
  "tab #{tab["title"].inspect}: #{icon.inspect} isn't an icon in #{font["file"]}"
637
637
  end
@@ -639,7 +639,7 @@ module Everywhere
639
639
  return if problems.empty?
640
640
 
641
641
  UI.die!("icons.android in config/everywhere.yml (#{names.size} icons in " \
642
- "#{font["file"]} browse them at https://fonts.google.com/icons):\n#{problems.join("\n")}")
642
+ "#{font["file"]}, browse them at https://fonts.google.com/icons):\n#{problems.join("\n")}")
643
643
  end
644
644
 
645
645
  # "name<space>hex" per line. Parsed in Ruby rather than shelled out to awk
@@ -690,7 +690,7 @@ module Everywhere
690
690
  Do not edit; this source set is merged into app/src/main/AndroidManifest.xml.
691
691
  Note for whoever edits this header next: XML forbids a double hyphen
692
692
  anywhere inside a comment, so the flag spelling of the target must not
693
- appear here. aapt2 does not warn the manifest merger just fails to
693
+ appear here. aapt2 does not warn: the manifest merger just fails to
694
694
  parse the file, minutes into a Gradle run. -->
695
695
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
696
696
  #{body.empty? ? " <!-- nothing declared: no permissions, deep links or sign-in scheme -->" : body}
@@ -731,7 +731,7 @@ module Everywhere
731
731
  UI.step("stamping deep links #{UI.dim("(#{domains.join(", ")})")}")
732
732
  unless @config.deep_linking_android?
733
733
  UI.warn "deep links won't auto-verify until deep_linking.android.package and " \
734
- ".sha256_cert_fingerprints are set until then they open in the browser"
734
+ ".sha256_cert_fingerprints are set. Until then they open in the browser"
735
735
  end
736
736
 
737
737
  domains.map do |host|
@@ -804,7 +804,7 @@ module Everywhere
804
804
  UI.step("bundling native extensions #{UI.dim("(#{sources.length} Kotlin files from native/android)")}")
805
805
  elsif sources.any?
806
806
  UI.warn "native/android has Kotlin files but everywhere.yml declares nothing under " \
807
- "native.android they compile in, but no components/screens/splash are hooked up"
807
+ "native.android. They compile in, but no components/screens/splash are hooked up"
808
808
  end
809
809
  File.write(File.join(dest, "EverywhereExtensions.kt"), extensions_registry)
810
810
  end
@@ -826,7 +826,7 @@ module Everywhere
826
826
  return if declared == EXTENSIONS_PACKAGE
827
827
 
828
828
  found = declared ? "declares package #{declared}" : "declares no package"
829
- UI.die!("native/android/#{relative} #{found} every Kotlin file here compiles into the " \
829
+ UI.die!("native/android/#{relative} #{found}. Every Kotlin file here compiles into the " \
830
830
  "shell's extension package, so it has to start with:\n\n package #{EXTENSIONS_PACKAGE}\n")
831
831
  end
832
832
 
@@ -846,7 +846,7 @@ module Everywhere
846
846
  splash = @config.native_android_splash
847
847
 
848
848
  <<~KOTLIN
849
- // Written by `every build` from config/everywhere.yml (native.android) — do
849
+ // Written by `every build` from config/everywhere.yml (native.android). Do
850
850
  // not edit; changes belong in native/android/ and everywhere.yml.
851
851
  //
852
852
  // A declared component is registered under the name its own class states:
@@ -897,7 +897,7 @@ module Everywhere
897
897
  lines = packages.map { |coordinate| " add(\"implementation\", \"#{coordinate}\")" }
898
898
  File.write(File.join(work, "app", "native-packages.gradle.kts"), <<~KOTLIN)
899
899
  // Written by `every build` from config/everywhere.yml (native.android.packages)
900
- // do not edit; changes belong in config/everywhere.yml.
900
+ // Do not edit; changes belong in config/everywhere.yml.
901
901
  dependencies {
902
902
  #{lines.join("\n")}
903
903
  }
@@ -966,7 +966,7 @@ module Everywhere
966
966
  UI.die!("Gradle reported success but no #{kind} in #{UI.short_path(dir)}") unless built
967
967
 
968
968
  if @signing && built == unsigned
969
- UI.die!("Gradle wrote #{File.basename(built)} EVERY_ANDROID_KEYSTORE is set, so the " \
969
+ UI.die!("Gradle wrote #{File.basename(built)}, but EVERY_ANDROID_KEYSTORE is set, so the " \
970
970
  "#{kind} should have been signed. Check the keystore's alias and passwords")
971
971
  end
972
972
 
@@ -119,7 +119,7 @@ module Everywhere
119
119
  path = File.join(work, "Cargo.toml")
120
120
  source = File.read(path)
121
121
  unless source.include?(CRATES_MARKER)
122
- UI.die!("the shell's Cargo.toml has no #{CRATES_MARKER} marker " \
122
+ UI.die!("the shell's Cargo.toml has no #{CRATES_MARKER} marker. " \
123
123
  "reinstall ruby_everywhere or pass --shell-dir at your own risk")
124
124
  end
125
125
 
@@ -186,7 +186,7 @@ module Everywhere
186
186
  # declare the file in everywhere.yml, it's to delete it.
187
187
  sources = NativeSources.copy!(from: source, into: dest, ext: "rs") do |_path, relative|
188
188
  if File.basename(relative) == MODULE_FILE
189
- UI.die!("#{File.join(SOURCE_DIR, relative)} is generated by `every build` " \
189
+ UI.die!("#{File.join(SOURCE_DIR, relative)} is generated by `every build`. " \
190
190
  "remove it and let the CLI write the module declarations")
191
191
  end
192
192
  end
@@ -222,7 +222,7 @@ module Everywhere
222
222
 
223
223
  def child_module(children)
224
224
  <<~RUST
225
- // GENERATED by `every build` edits are overwritten.
225
+ // GENERATED by `every build`. Edits are overwritten.
226
226
  // Declares the .rs files this directory holds in native/desktop/.
227
227
  #{children.map { |name| "pub mod #{name};" }.join("\n")}
228
228
  RUST
@@ -244,7 +244,7 @@ module Everywhere
244
244
  <<~RUST
245
245
  // GENERATED by `every build` / `every dev` from `native.desktop` in
246
246
  // config/everywhere.yml, alongside the app's own .rs files copied from
247
- // native/desktop/. Edits are overwritten see support/desktop/README.md.
247
+ // native/desktop/. Edits are overwritten; see support/desktop/README.md.
248
248
 
249
249
  use crate::commands;
250
250
 
@@ -256,7 +256,7 @@ module Everywhere
256
256
  // is the right way to find out.
257
257
  #{glob_imports(paths)}
258
258
 
259
- // The shell's own commands. App extensions answer over events instead
259
+ // The shell's own commands. App extensions answer over events instead,
260
260
  // see extension_host.rs for why invoke isn't available to them.
261
261
  pub fn handler() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
262
262
  tauri::generate_handler![commands::notify_command]
@@ -274,7 +274,7 @@ module Everywhere
274
274
  ) -> Result<serde_json::Value, String> {
275
275
  match name {
276
276
  #{dispatch_arms(commands)} _ => Err(format!(
277
- "no desktop command named {name:?} declare it under native.desktop.commands"
277
+ "no desktop command named {name:?}. Declare it under native.desktop.commands"
278
278
  )),
279
279
  }
280
280
  }
@@ -90,14 +90,14 @@ module Everywhere
90
90
 
91
91
  def preflight!(dev:, device:)
92
92
  unless dev || @config.remote?
93
- UI.die!("iOS apps are remote mode only for now set remote.url in config/everywhere.yml " \
93
+ UI.die!("iOS apps are remote mode only for now. Set remote.url in config/everywhere.yml " \
94
94
  "(the shell wraps your deployed app; nothing is packaged locally)")
95
95
  end
96
96
  UI.die!("iOS builds need macOS with Xcode installed") unless Host.darwin?
97
97
  return device_preflight! if device
98
98
  return if Shellout.run?("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path", quiet: true)
99
99
 
100
- UI.die!("no iOS Simulator SDK install full Xcode (not just the Command Line Tools) " \
100
+ UI.die!("no iOS Simulator SDK. Install full Xcode (not just the Command Line Tools) " \
101
101
  "and run: xcode-select --switch /Applications/Xcode.app")
102
102
  end
103
103
 
@@ -106,17 +106,17 @@ module Everywhere
106
106
  # its own vocabulary.
107
107
  def device_preflight!
108
108
  unless Shellout.run?("xcrun", "--sdk", "iphoneos", "--show-sdk-path", quiet: true)
109
- UI.die!("no iOS device SDK install full Xcode (not just the Command Line Tools) " \
109
+ UI.die!("no iOS device SDK. Install full Xcode (not just the Command Line Tools) " \
110
110
  "and run: xcode-select --switch /Applications/Xcode.app")
111
111
  end
112
112
 
113
113
  missing = REQUIRED_SIGNING_ENV.reject { |name, _| env_value(name) }
114
114
  unless missing.empty?
115
115
  UI.die!("device builds need signing credentials in the environment:\n" \
116
- "#{missing.map { |name, hint| " #{name} #{hint}" }.join("\n")}")
116
+ "#{missing.map { |name, hint| " #{name}: #{hint}" }.join("\n")}")
117
117
  end
118
118
  unless EXPORT_METHODS.include?(export_method)
119
- UI.die!("EVERY_IOS_EXPORT_METHOD=#{export_method} isn't an Xcode export method " \
119
+ UI.die!("EVERY_IOS_EXPORT_METHOD=#{export_method} isn't an Xcode export method. " \
120
120
  "use one of: #{EXPORT_METHODS.join(", ")}")
121
121
  end
122
122
  asc_preflight! if store_channel?
@@ -129,12 +129,12 @@ module Everywhere
129
129
  .map { |_field, names| names.first }
130
130
  unless missing.empty?
131
131
  UI.die!("--channel #{@channel} uploads to App Store Connect, which needs an API key " \
132
- "in the environment — missing: #{missing.join(", ")}")
132
+ "in the environment. Missing: #{missing.join(", ")}")
133
133
  end
134
134
  return if export_method == DEFAULT_EXPORT_METHOD
135
135
 
136
136
  UI.die!("--channel #{@channel} uploads to App Store Connect, which only accepts " \
137
- "#{DEFAULT_EXPORT_METHOD} builds unset EVERY_IOS_EXPORT_METHOD=#{export_method}")
137
+ "#{DEFAULT_EXPORT_METHOD} builds. Unset EVERY_IOS_EXPORT_METHOD=#{export_method}")
138
138
  end
139
139
 
140
140
  def asc_env(field) = ASC_ENV.fetch(field).filter_map { |name| env_value(name) }.first
@@ -284,7 +284,7 @@ module Everywhere
284
284
  FileUtils.mkdir_p(dest)
285
285
  File.write(File.join(dest, "EverywhereExtensions.swift"), extensions_registry)
286
286
  elsif sources.any?
287
- UI.warn "native/ios has Swift files but everywhere.yml declares nothing under native.ios " \
287
+ UI.warn "native/ios has Swift files but everywhere.yml declares nothing under native.ios. " \
288
288
  "they compile in, but no components/screens/splash are hooked up"
289
289
  end
290
290
  end
@@ -309,7 +309,7 @@ module Everywhere
309
309
  end
310
310
 
311
311
  <<~SWIFT
312
- // Written by `every build` from config/everywhere.yml (native.ios) — do
312
+ // Written by `every build` from config/everywhere.yml (native.ios). Do
313
313
  // not edit; changes belong in native/ios/ and everywhere.yml.
314
314
  import HotwireNative
315
315
  import SwiftUI
@@ -368,7 +368,7 @@ module Everywhere
368
368
  <<~SWIFT
369
369
  // swift-tools-version:5.9
370
370
  // Written by `every build` from config/everywhere.yml (native.ios.packages)
371
- // do not edit; changes belong in config/everywhere.yml.
371
+ // Do not edit; changes belong in config/everywhere.yml.
372
372
  import PackageDescription
373
373
 
374
374
  let package = Package(
@@ -390,7 +390,7 @@ module Everywhere
390
390
  def native_package_exports(packages)
391
391
  header = <<~SWIFT
392
392
  // Written by `every build` from config/everywhere.yml (native.ios.packages)
393
- // — re-exports each pinned product so native/ios code reaches it with a
393
+ // Re-exports each pinned product so native/ios code reaches it with a
394
394
  // single `import NativeExtensions`. Do not edit.
395
395
  SWIFT
396
396
  modules = packages.flat_map { |pkg| pkg["products"] }.uniq.sort
@@ -489,7 +489,7 @@ module Everywhere
489
489
  # and no deep linking gets the setting just the same.
490
490
  entitlements = "CODE_SIGN_ENTITLEMENTS = App/App.entitlements\n" if entitlement_keys.any?
491
491
  File.write(File.join(work, "App", "App.xcconfig"), <<~XCCONFIG)
492
- // Written by `every build --target #{@target}` do not edit; changes
492
+ // Written by `every build --target #{@target}`. Do not edit; changes
493
493
  // belong in config/everywhere.yml.
494
494
  PRODUCT_BUNDLE_IDENTIFIER = #{xcconfig_value(bundle_id)}
495
495
  MARKETING_VERSION = #{xcconfig_value(@config.version(target: @target))}
@@ -526,7 +526,7 @@ module Everywhere
526
526
  def write_app_icon(work)
527
527
  source = @config.icon
528
528
  unless source && File.exist?(source)
529
- UI.warn "no app icon (set app.icon or add icon.png) shipping the placeholder"
529
+ UI.warn "no app icon (set app.icon or add icon.png). Shipping the placeholder"
530
530
  return
531
531
  end
532
532
 
@@ -534,7 +534,7 @@ module Everywhere
534
534
  dest = File.join(work, "App", "Assets.xcassets", "AppIcon.appiconset")
535
535
  return if Icon.write_ios_appiconset(source, dest, background: background)
536
536
 
537
- UI.warn "couldn't read #{File.basename(source)} shipping the placeholder icon"
537
+ UI.warn "couldn't read #{File.basename(source)}. Shipping the placeholder icon"
538
538
  end
539
539
 
540
540
  # App-supplied images: native/ios/assets/** → imagesets in the template's
@@ -22,7 +22,7 @@ module Everywhere
22
22
  relative = file.delete_prefix("#{from}/")
23
23
  if reserved && File.basename(file) == reserved
24
24
  UI.die!("#{label}/#{relative}: #{reserved} is generated by " \
25
- "`every build` declare your types under #{label.tr("/", ".")} " \
25
+ "`every build`. Declare your types under #{label.tr("/", ".")} " \
26
26
  "in config/everywhere.yml instead")
27
27
  end
28
28
  yield(file, relative) if block_given?
@@ -160,7 +160,7 @@ module Everywhere
160
160
  deadline = Clock.monotonic + timeout
161
161
  until port_open?(port)
162
162
  if pid && !ChildProcesses.alive?(pid)
163
- UI.die!("the dev server exited before it finished booting scroll up for its output#{dead_hint}")
163
+ UI.die!("the dev server exited before it finished booting. Scroll up for its output#{dead_hint}")
164
164
  end
165
165
  if Clock.monotonic > deadline
166
166
  UI.die!("dev server never came up on port #{port} after #{timeout}s#{slow_hint}")
@@ -41,18 +41,40 @@ module Everywhere
41
41
 
42
42
  ANDROID_FORMATS = %w[apk aab].freeze
43
43
 
44
- desc "Package the app nativelydesktop (tebako press + shell + .app), iOS (--ios) or Android (--android)"
44
+ # Valid --channel per target os the pairing the Platform accepts, minus
45
+ # nothing: macOS has no store channel (every macOS release signs with
46
+ # Developer ID and notarizes regardless), iOS `direct` exports an .ipa
47
+ # without the App Store Connect upload, and Android's `play` selects the
48
+ # .aab path even though the Play upload itself happens platform-side.
49
+ CHANNELS = {
50
+ "macos" => %w[direct],
51
+ "ios" => %w[direct testflight app_store],
52
+ "android" => %w[direct play]
53
+ }.freeze
54
+
55
+ # An os/channel pair nothing implements has to be refused before the
56
+ # build runs — it used to land in the receipt, where the only thing
57
+ # reading it (a store upload, a Platform distributor) has no idea what
58
+ # it means. Unknown os falls through: builder dispatch owns that error.
59
+ def self.check_channel!(os, channel)
60
+ allowed = CHANNELS[os] || return
61
+ return if allowed.include?(channel)
62
+
63
+ UI.die!("unsupported --channel #{channel} for #{os}: use one of #{allowed.join(", ")}")
64
+ end
65
+
66
+ desc "Package the app natively: desktop (tebako press + shell + .app), iOS (--ios) or Android (--android)"
45
67
 
46
68
  option :root, default: ".", desc: "App root directory"
47
69
  option :output, default: nil, desc: "Output binary path (default: dist/<app>)"
48
70
  option :ruby, default: "4.0.6", desc: "Ruby version to package"
49
71
  option :entry, default: "native_boot.rb", desc: "Entry point script relative to root"
50
72
  option :app_name, default: nil, desc: "Bundle name (default: capitalized app dir)"
51
- option :target, default: nil, desc: "Build target (os-arch) selects any platforms: overrides, default: \"#{DEFAULT_TARGET}\""
73
+ option :target, default: nil, desc: "Build target (os-arch); selects any platforms: overrides, default: \"#{DEFAULT_TARGET}\""
52
74
  option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
53
75
  option :template_dir, default: nil, desc: "Path to the mobile shell template (iOS/Android)"
54
76
  option :format, default: nil, desc: "Android artifact format (#{ANDROID_FORMATS.join(" | ")}), default: apk"
55
- option :channel, default: "direct", desc: "Distribution channel (direct | testflight | app_store | ...)"
77
+ option :channel, default: "direct", desc: "Distribution channel (#{CHANNELS.map { |os, chs| "#{os}: #{chs.join("|")}" }.join(" · ")})"
56
78
  option :ios, type: :boolean, default: false, desc: "Build the iOS shell (shorthand for --target #{IOS_TARGET})"
57
79
  option :android, type: :boolean, default: false, desc: "Build the Android shell (shorthand for --target #{ANDROID_TARGET})"
58
80
  option :device, type: :boolean, default: false, desc: "iOS: archive and export a device .ipa instead of a simulator .app"
@@ -62,6 +84,7 @@ module Everywhere
62
84
  app_name: nil, target: nil, shell_dir: nil, template_dir: nil, format: nil,
63
85
  channel: "direct", ios: false, android: false, device: false, skip_press: false, **)
64
86
  target = resolve_target(target, ios: ios, android: android)
87
+ self.class.check_channel!(Everywhere::Config.os_of(target), channel)
65
88
  root_path = File.expand_path(root)
66
89
  config = Everywhere::Config.load(root_path)
67
90
 
@@ -70,11 +93,7 @@ module Everywhere
70
93
  identity_errors = config.identity_errors
71
94
  UI.die!("app: in config/everywhere.yml:\n#{identity_errors.join("\n")}") unless identity_errors.empty?
72
95
 
73
- # Baked into the shell here, so this is the last moment an unusable
74
- # feed URL can still be a message instead of a shipped app that never
75
- # updates.
76
- updates_errors = config.updates_errors
77
- UI.die!("updates: in config/everywhere.yml:\n#{updates_errors.join("\n")}") unless updates_errors.empty?
96
+ check_updates!(config)
78
97
 
79
98
  # Builder dispatch on the target's os (build-engine.md §5): macOS is
80
99
  # the tebako+tauri path below; the mobile targets are Hotwire Native
@@ -96,7 +115,7 @@ module Everywhere
96
115
  # thirty minutes in, at the first codesign.
97
116
  UI.die!("desktop builds are macOS-only for now") unless Host.darwin?
98
117
  else
99
- UI.die!("no builder for target #{target} macos-*, ios-* and android-* are supported")
118
+ UI.die!("no builder for target #{target}: macos-*, ios-* and android-* are supported")
100
119
  end
101
120
 
102
121
  # Desktop chrome is validated here rather than in the shell: a typo in
@@ -131,7 +150,7 @@ module Everywhere
131
150
  entry_path = File.join(framework.root, entry)
132
151
 
133
152
  unless File.exist?(entry_path)
134
- UI.die!("#{entry} not found in #{framework.root} the app needs a packaged-boot entry point")
153
+ UI.die!("#{entry} not found in #{framework.root}: the app needs a packaged-boot entry point")
135
154
  end
136
155
 
137
156
  UI.step("#{UI.bold(framework.name)} app at #{framework.root}")
@@ -150,6 +169,17 @@ module Everywhere
150
169
 
151
170
  private
152
171
 
172
+ # The `updates:` section is baked into the shell during this build, so
173
+ # this is the last moment an unusable feed URL — or a missing signing key,
174
+ # which turns the updater off without failing anything — can still be a
175
+ # message instead of a shipped app that never updates. Once per build:
176
+ # every target stamps everywhere.json from the same config.
177
+ def check_updates!(config)
178
+ errors = config.updates_errors
179
+ UI.die!("updates: in config/everywhere.yml:\n#{errors.join("\n")}") unless errors.empty?
180
+ config.updates_warnings.each { |warning| UI.warn(warning) }
181
+ end
182
+
153
183
  # `--ios` is the shorthand `every dev` already has; --target stays the
154
184
  # general escape hatch. Both given is only an error when they disagree —
155
185
  # `--ios --target ios-arm64` is redundant, not wrong.
@@ -168,10 +198,10 @@ module Everywhere
168
198
  # `--format aab` on an iOS build is a mistake we name rather than drop.
169
199
  def mobile_options(target, device:, format:)
170
200
  if Everywhere::Config.os_of(target) == "android"
171
- UI.die!("--device is an iOS flag Android always builds an installable artifact") if device
201
+ UI.die!("--device is an iOS flag; Android always builds an installable artifact") if device
172
202
  { format: android_format(format) }
173
203
  else
174
- UI.die!("--format is an Android flag (#{ANDROID_FORMATS.join("/")}) iOS builds a .app or .ipa") if format
204
+ UI.die!("--format is an Android flag (#{ANDROID_FORMATS.join("/")}); iOS builds a .app or .ipa") if format
175
205
  { device: device }
176
206
  end
177
207
  end
@@ -192,7 +222,7 @@ module Everywhere
192
222
  def resolve_target(target, ios:, android:)
193
223
  asked = { ios: ios, android: android }.select { |_flag, given| given }.keys
194
224
  if asked.length > 1
195
- UI.die!("#{asked.map { |f| "--#{f}" }.join(" and ")} are separate builds run one at a time")
225
+ UI.die!("#{asked.map { |f| "--#{f}" }.join(" and ")} are separate builds; run one at a time")
196
226
  end
197
227
 
198
228
  flag = asked.first or return target || DEFAULT_TARGET
@@ -200,7 +230,7 @@ module Everywhere
200
230
  return shorthand if target.nil?
201
231
 
202
232
  unless Everywhere::Config.os_of(target) == flag.to_s
203
- UI.die!("--#{flag} and --target #{target} disagree drop one " \
233
+ UI.die!("--#{flag} and --target #{target} disagree; drop one " \
204
234
  "(--#{flag} means --target #{shorthand})")
205
235
  end
206
236
  target
@@ -213,7 +243,7 @@ module Everywhere
213
243
  def ensure_gems!(framework)
214
244
  return if Shellout.run?(bundle_env, "bundle", "check", chdir: framework.root, quiet: true)
215
245
 
216
- UI.step("installing gems #{UI.dim("(bundle install first build on this machine)")}")
246
+ UI.step("installing gems #{UI.dim("(bundle install, first build on this machine)")}")
217
247
  Shellout.run!(bundle_env, "bundle", "install", chdir: framework.root)
218
248
  end
219
249
 
@@ -242,7 +272,7 @@ module Everywhere
242
272
  FileUtils.mkdir_p(out_dir)
243
273
  log = "#{output_path}.build.log"
244
274
 
245
- UI.step("rbe-tebako press #{UI.dim("(Ruby #{ruby} grab a coffee on first run)")}")
275
+ UI.step("rbe-tebako press #{UI.dim("(Ruby #{ruby}, grab a coffee on first run)")}")
246
276
  UI.detail("full build log: #{UI.short_path(log)}")
247
277
  # Press into a staging dir OUTSIDE the app root for the same reason
248
278
  # (tebako prints a big warning about exactly this). Block form: the
@@ -275,7 +305,7 @@ module Everywhere
275
305
  # Rails binary (as "app-server") + Info.plist + icon. An .app bundle is
276
306
  # just a directory layout, so we build it by hand — no tauri-cli needed.
277
307
  def bundle_app(server_binary, app_name, shell_dir, config, target: DEFAULT_TARGET, dist_dir: nil)
278
- Shellout.ensure_tool!("cargo", "the desktop shell is built with Rust install it from https://rustup.rs")
308
+ Shellout.ensure_tool!("cargo", "the desktop shell is built with Rust; install it from https://rustup.rs")
279
309
 
280
310
  # cargo always runs in a STAGED copy of the shell under
281
311
  # ~/.rubyeverywhere, never in the gem (or in whatever --shell-dir
@@ -290,7 +320,7 @@ module Everywhere
290
320
 
291
321
  icon_source = config.icon
292
322
  if icon_source && !File.exist?(icon_source)
293
- UI.warn "app icon #{icon_source} not found using placeholder"
323
+ UI.warn "app icon #{icon_source} not found, using placeholder"
294
324
  icon_source = nil
295
325
  end
296
326
  UI.step("app icon: #{icon_source ? icon_source.sub("#{config.root}/", "") : "placeholder"}")
@@ -307,7 +337,7 @@ module Everywhere
307
337
  if Everywhere::Icon.macos_master(icon_source, shaped)
308
338
  macos_icon = shaped
309
339
  else
310
- UI.warn "couldn't read #{File.basename(icon_source)} (unusual PNG flavor) using it unshaped"
340
+ UI.warn "couldn't read #{File.basename(icon_source)} (unusual PNG flavor), using it unshaped"
311
341
  end
312
342
  end
313
343
 
@@ -329,7 +359,7 @@ module Everywhere
329
359
  true
330
360
  end
331
361
  unless dock_ok
332
- UI.warn "couldn't convert #{File.basename(icon_source)} to RGBA (unusual PNG flavor) Dock icon will use the placeholder after launch"
362
+ UI.warn "couldn't convert #{File.basename(icon_source)} to RGBA (unusual PNG flavor); the Dock icon will use the placeholder after launch"
333
363
  end
334
364
  FileUtils.touch(File.join(shell_dir, "build.rs"))
335
365
  end
@@ -359,7 +389,7 @@ module Everywhere
359
389
  FileUtils.cp(splash, File.join(resources, "splash.html"))
360
390
  UI.step("custom splash: #{splash.sub("#{config.root}/", "")}")
361
391
  else
362
- UI.warn "splash #{splash} not found using the default splash"
392
+ UI.warn "splash #{splash} not found, using the default splash"
363
393
  end
364
394
  end
365
395
  icns = make_icns(macos_icon || shell_icon, resources)
@@ -403,7 +433,7 @@ module Everywhere
403
433
  out = File.join(resources_dir, "AppIcon.icns")
404
434
  Everywhere::Icon.write_icns(png, out) ? "AppIcon" : nil
405
435
  rescue StandardError => e
406
- UI.warn "icon generation failed (#{e.message}) bundling without a custom icon"
436
+ UI.warn "icon generation failed (#{e.message}), bundling without a custom icon"
407
437
  nil
408
438
  end
409
439
 
@@ -450,7 +480,7 @@ module Everywhere
450
480
  end
451
481
 
452
482
  press_warnings = text.scan(/^.*(?:WARNING|Warning):(?!.*bitcode).*$/).size
453
- UI.warn "tebako emitted #{press_warnings} warning(s) details in the build log" if press_warnings.positive?
483
+ UI.warn "tebako emitted #{press_warnings} warning(s), details in the build log" if press_warnings.positive?
454
484
  end
455
485
 
456
486
  def press_env
@@ -467,7 +497,7 @@ module Everywhere
467
497
  # a silently dropped PATH entry.
468
498
  def bison_prefix!
469
499
  @bison_prefix ||= brew_bison_prefix ||
470
- UI.die!("tebako needs Homebrew bison brew install bison")
500
+ UI.die!("tebako needs Homebrew bison: brew install bison")
471
501
  end
472
502
 
473
503
  def brew_bison_prefix
@@ -36,7 +36,7 @@ module Everywhere
36
36
  FileUtils.rm_rf(dir)
37
37
  UI.ok("removed #{dir} #{UI.dim("(#{human_size(bytes)})")}")
38
38
  end
39
- UI.success("#{human_size(freed)} freed the next build recompiles the shells cold")
39
+ UI.success("#{human_size(freed)} freed; the next build recompiles the shells cold")
40
40
  end
41
41
 
42
42
  private