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.
- checksums.yaml +4 -4
- data/README.md +35 -33
- data/bridge/README.md +23 -23
- data/lib/everywhere/android_resources.rb +9 -9
- data/lib/everywhere/android_sdk.rb +9 -9
- data/lib/everywhere/asset_catalog.rb +7 -7
- data/lib/everywhere/blake2b.rb +1 -1
- data/lib/everywhere/boot.rb +1 -1
- data/lib/everywhere/builders/android.rb +25 -25
- data/lib/everywhere/builders/desktop.rb +6 -6
- data/lib/everywhere/builders/ios.rb +14 -14
- data/lib/everywhere/builders/native_sources.rb +1 -1
- data/lib/everywhere/child_supervision.rb +1 -1
- data/lib/everywhere/commands/build.rb +54 -24
- data/lib/everywhere/commands/clean.rb +1 -1
- data/lib/everywhere/commands/dev.rb +11 -11
- data/lib/everywhere/commands/doctor.rb +7 -7
- data/lib/everywhere/commands/icon.rb +2 -2
- data/lib/everywhere/commands/install.rb +21 -21
- data/lib/everywhere/commands/logs.rb +6 -6
- data/lib/everywhere/commands/platform/auth_status.rb +3 -3
- data/lib/everywhere/commands/platform/build.rb +7 -7
- data/lib/everywhere/commands/platform/login.rb +5 -5
- data/lib/everywhere/commands/platform/logout.rb +1 -1
- data/lib/everywhere/commands/platform/runner.rb +6 -6
- data/lib/everywhere/commands/preview.rb +14 -14
- data/lib/everywhere/commands/publish.rb +14 -14
- data/lib/everywhere/commands/release.rb +13 -9
- data/lib/everywhere/commands/updates_keygen.rb +4 -4
- data/lib/everywhere/config/app.rb +3 -3
- data/lib/everywhere/config/mobile.rb +2 -2
- data/lib/everywhere/config/native_desktop.rb +2 -2
- data/lib/everywhere/config/native_mobile.rb +3 -3
- data/lib/everywhere/config/updates.rb +11 -1
- data/lib/everywhere/database.rb +3 -3
- data/lib/everywhere/desktop_assets.rb +5 -5
- data/lib/everywhere/desktop_dev_app.rb +1 -1
- data/lib/everywhere/emulator.rb +5 -5
- data/lib/everywhere/log_filter.rb +1 -1
- data/lib/everywhere/omniauth.rb +1 -1
- data/lib/everywhere/paths.rb +1 -1
- data/lib/everywhere/platform/credentials.rb +1 -1
- data/lib/everywhere/request_context.rb +2 -2
- data/lib/everywhere/shellout.rb +2 -2
- data/lib/everywhere/simulator.rb +1 -1
- data/lib/everywhere/tab_filter.rb +2 -2
- data/lib/everywhere/version.rb +1 -1
- data/support/agents/AGENTS.md +7 -7
- data/support/agents/frameworks/hanami-views.md +8 -8
- data/support/agents/frameworks/hanami.md +2 -2
- data/support/agents/frameworks/rails-views.md +7 -7
- data/support/agents/frameworks/rails.md +8 -8
- data/support/agents/frameworks/sinatra-views.md +7 -7
- data/support/agents/frameworks/sinatra.md +2 -2
- data/support/agents/modes/local.md +2 -2
- data/support/agents/modes/remote.md +2 -2
- data/support/desktop/src-tauri/src/updater.rs +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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}
|
|
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)
|
|
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)}
|
|
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
|
|
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}
|
|
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}
|
|
634
|
-
"
|
|
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"]}
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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)
|
|
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
|
-
//
|
|
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)}
|
|
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
|
|
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
|
|
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:?}
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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}
|
|
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)
|
|
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)}
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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 (
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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
|
|
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("/")})
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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
|
|
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})
|
|
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)
|
|
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
|
|
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
|
|
39
|
+
UI.success("#{human_size(freed)} freed; the next build recompiles the shells cold")
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
private
|