Class: Everywhere::Commands::Build
- Inherits:
-
Dry::CLI::Command
- Object
- Dry::CLI::Command
- Everywhere::Commands::Build
- Defined in:
- lib/everywhere/commands/build.rb
Overview
Press the app into a single-file binary and make it runnable on this Mac.
Encodes the macOS 26 workarounds discovered in the Phase 0 spike:
1. BUNDLE_FORCE_RUBY_PLATFORM so native gems compile from source
(tebako's strip corrupts precompiled gems' signatures -> SIGKILL)
2. Homebrew bison on PATH + LG_VADDR for tebako itself
3. Post-press ad-hoc re-sign (macOS 26 rejects the pressed signature)
Constant Summary collapse
- DEFAULT_TARGET =
Derived from the host, not fixed: on an Intel Mac the pressed binary is x86_64, and a hardcoded arm64 would put that lie in the bundle metadata, the release receipt and every platform override keyed off the target.
RUBY_PLATFORM.match?(/arm64|aarch64/) ? "macos-arm64" : "macos-x86_64"
- IOS_TARGET =
"ios-arm64"- ANDROID_TARGET =
The APK/AAB is universal, so the arch half is nominal — it exists only so every target reads as os-arch.
"android-arm64"- SHORTHAND_TARGETS =
The --ios/--android shorthands and the os they stand for, so adding a mobile platform is one entry rather than a third copy of resolve_target.
{ ios: IOS_TARGET, android: ANDROID_TARGET }.freeze
- ANDROID_FORMATS =
%w[apk aab].freeze
Instance Method Summary collapse
Instance Method Details
#call(root: ".", output: nil, ruby: "4.0.6", entry: "native_boot.rb", app_name: nil, target: nil, shell_dir: nil, template_dir: nil, format: nil, channel: "direct", ios: false, android: false, device: false, skip_press: false) ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/everywhere/commands/build.rb', line 61 def call(root: ".", output: nil, ruby: "4.0.6", entry: "native_boot.rb", app_name: nil, target: nil, shell_dir: nil, template_dir: nil, format: nil, channel: "direct", ios: false, android: false, device: false, skip_press: false, **) target = resolve_target(target, ios: ios, android: android) root_path = File.(root) config = Everywhere::Config.load(root_path) # Before any builder runs: the bundle id names the work dirs they wipe # and recreate, so a malformed one is a wrong path, not a wrong label. identity_errors = config.identity_errors UI.die!("app: in config/everywhere.yml:\n#{identity_errors.join("\n")}") unless identity_errors.empty? # Baked into the shell here, so this is the last moment an unusable # feed URL can still be a message instead of a shipped app that never # updates. updates_errors = config.updates_errors UI.die!("updates: in config/everywhere.yml:\n#{updates_errors.join("\n")}") unless updates_errors.empty? # Builder dispatch on the target's os (build-engine.md §5): macOS is # the tebako+tauri path below; the mobile targets are Hotwire Native # shells, each built by its own builder. Anything else has no builder yet. case Everywhere::Config.os_of(target) when "ios", "android" # --output names a file for the pressed desktop binary, but a mobile # build's product is whatever the platform toolchain emits — so here it # names the directory those products land in. dist_dir = output ? File.(output) : File.("dist", root_path) app = mobile_builder(target).new(config: config, root: root_path, target: target, template_dir: template_dir, **channel_option(target, channel)) .build!(dist_dir: dist_dir, **(target, device: device, format: format)) UI.success("built #{UI.rel_path(app, root_path)}") return when "macos" # ...which is the rest of this method # Every step of it ends in codesign and an .app bundle, so a Linux # host can only fail — the question is whether it fails here or # thirty minutes in, at the first codesign. UI.die!("desktop builds are macOS-only for now") unless Host.darwin? else UI.die!("no builder for target #{target} — macos-*, ios-* and android-* are supported") end # Desktop chrome is validated here rather than in the shell: a typo in # `window:` would otherwise fall through Rust's match arms as a silent # default, and you'd learn about it by squinting at a title bar after a # full press+compile. The mobile builders validate their own sections # the same way (Builders::Ios#write_extensions). window_errors = config.window_errors unless window_errors.empty? UI.die!("window: in config/everywhere.yml:\n#{window_errors.join("\n")}") end # The os of `target` selects any per-platform overrides (bundle id, # version, name) — local builds always target the host, macOS today. app_name ||= config.name(target: target) # Remote mode: thin shell around an already-deployed app. No Rails app # required locally, nothing to press — just shell + config in a bundle. if config.remote? UI.step("#{UI.bold("remote")} mode → #{UI.cyan(config.remote_url)} #{UI.dim("(no local server packaged)")}") bundle_app(nil, app_name, shell_dir, config, target: target, dist_dir: File.("dist", root_path)) return end framework = Framework.detect(root_path) # dist/ belongs to the APP, not to the shell's cwd: `every platform # runner` invokes us with --root <snapshot> from its own working # directory, and `every release` looks for the .app under <root>/dist. # Resolving a relative default against Dir.pwd scattered the build # products into the runner's checkout instead. output_path = output ? File.(output) : File.join(root_path, "dist", File.basename(root_path)) entry_path = File.join(framework.root, entry) unless File.exist?(entry_path) UI.die!("#{entry} not found in #{framework.root} — the app needs a packaged-boot entry point") end UI.step("#{UI.bold(framework.name)} app at #{framework.root}") if skip_press UI.die!("--skip-press given but #{UI.rel_path(output_path, root_path)} doesn't exist") unless File.exist?(output_path) UI.step("reusing pressed binary #{UI.rel_path(output_path, root_path)}") else press!(framework, entry, ruby, output_path) end bundle_app(output_path, app_name, shell_dir, config, target: target) UI.success("built #{UI.rel_path(output_path, root_path)} (#{(File.size(output_path) / 1024.0 / 1024.0).round}MB)") end |