Class: Everywhere::Builders::Android
- Defined in:
- lib/everywhere/builders/android.rb
Overview
Assembles a Hotwire Native Android app around an already-deployed (remote mode) app — the Android half of Builders::Ios, and deliberately the same five steps: copy the gem's frozen Gradle template, stamp an explicit list of per-app files (never a build script), and hand it to Gradle.
The stamped list, and nothing else:
app/everywhere.properties identity (app/build.gradle.kts reads it)
app/native-packages.gradle.kts native.android.packages
app/src/stamped/AndroidManifest.xml deep links, auth scheme, permissions
app/src/main/assets/everywhere.json Config#to_shell_json
app/src/main/assets/json/path-configuration.json baked tabs + rules
app/src/main/assets/fonts/ the selected Material icon font
app/src/main/res/values/colors.xml tint / background, light (replaced in place)
app/src/main/res/values-night/colors.xml tint / background, dark
app/src/main/res/mipmap-*/ launcher icon layers + anydpi-v26
app/src/stamped/res/drawable*/ native/android/assets/** as drawables
app/src/main/java/com/rubyeverywhere/shell/extensions/ native/android/*.kt + registry
Signing and the format Play accepts are driven from the environment, not from everywhere.yml — see SIGNING_ENV.
Constant Summary collapse
- GRADLE_MODULE =
Fixed by the template contract: one Gradle module, always :app, so build paths and task names never depend on per-app values.
":app"- FORMATS =
What each package format is called in the three places it has a name: the Gradle verb, the outputs/ subdirectory and the file extension.
{ apk: { task: "assemble", dir: "apk", extension: "apk" }, aab: { task: "bundle", dir: "bundle", extension: "aab" } }.freeze
- SIGNING_ENV =
Release signing is read from the environment and handed to Gradle as environment, never stamped into everywhere.properties: that file lives in the work dir for the life of the app, and a keystore password written there outlives the build that needed it. The keystore itself stays where the machine put it — nothing is copied into the project.
%w[EVERY_ANDROID_KEYSTORE EVERY_ANDROID_KEYSTORE_PASSWORD EVERY_ANDROID_KEY_ALIAS EVERY_ANDROID_KEY_PASSWORD].freeze
- REQUIRED_SIGNING_ENV =
The two the keystore can't be used without. EVERY_ANDROID_KEY_PASSWORD is not among them: a key generated without -keypass shares the store's.
%w[EVERY_ANDROID_KEYSTORE_PASSWORD EVERY_ANDROID_KEY_ALIAS].freeze
- VERSION_CODE_ENV =
"EVERY_ANDROID_VERSION_CODE"- SHELL_PACKAGE =
The Kotlin package the shell's sources live in. NOT the applicationId: the app id is stamped per app, the code's package is frozen, and mixing them up is the classic Android manifest mistake — so anything we generate that names a class spells the package out in full.
"com.rubyeverywhere.shell"- EXTENSIONS_PACKAGE =
"#{SHELL_PACKAGE}.extensions"- MAIN_ACTIVITY =
The activity every stamped intent-filter attaches to. Its
element already exists in the frozen main manifest; the stamped source set only adds filters to it. "#{SHELL_PACKAGE}.MainActivity"- ICON_FONT_COMMIT =
google/material-design-icons, pinned by commit for the same reason the iOS side pins Package.resolved: an icon set that silently changes under a cached build is a rendering bug nobody can reproduce. Each entry carries the SHA-256 of both files, so a corrupted or substituted download fails loudly instead of shipping.
"528cb964c01fb2b09bc3b9208f82b6d8f8c1c1e2"- ICON_FONT_BASE =
"https://raw.githubusercontent.com/google/material-design-icons"- FETCHED_ICON_FONTS =
Keyed by the asset filename Config::ANDROID_ICON_FONTS names; "path" is the repo path without an extension (upstream spells the variable fonts with their axis list in brackets — we ship them under the plain name the shell asks Typeface for).
{ "MaterialSymbolsOutlined.ttf" => { "path" => "variablefont/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght]", "ttf" => "b5126c4655e0756f334d684104156b98b82ef7d61212a81c041b9e6cfa8ba925", "codepoints" => "8567a3d0512ce8a735a739d325e83ee7010d9bf7f6f8ed6a699c07c817ea907d" }, "MaterialSymbolsRounded.ttf" => { "path" => "variablefont/MaterialSymbolsRounded[FILL,GRAD,opsz,wght]", "ttf" => "8817f9df195b582d45a6031c1d9e1533595dd7a6f0e76abc309bdc3cc4d72ee1", "codepoints" => "8567a3d0512ce8a735a739d325e83ee7010d9bf7f6f8ed6a699c07c817ea907d" }, "MaterialSymbolsSharp.ttf" => { "path" => "variablefont/MaterialSymbolsSharp[FILL,GRAD,opsz,wght]", "ttf" => "c56a37227b580aa30bcbf352a770a2598dc86bca43bf060ff8d0550c081827e6", "codepoints" => "8567a3d0512ce8a735a739d325e83ee7010d9bf7f6f8ed6a699c07c817ea907d" } }.freeze
- BUNDLED_FONT_DIRS =
Where the bundled (classic) font sits inside the template. Two spellings because the template ships it as the default asset, and a template that keeps its fonts out of assets/ still resolves.
[%w[app src main assets fonts], %w[fonts]].freeze
- APPLICATION_ID =
What AGP accepts as an applicationId: two or more segments, each a Java identifier starting with a letter. No hyphens, unlike a bundle id.
/\A[a-zA-Z]\w*(\.[a-zA-Z]\w*)+\z/
Constants inherited from Base
Class Method Summary collapse
-
.fetch!(url, dest, sha256:, redirects: 3) ⇒ Object
Download to a sibling .part file, verify, then rename — the atomicity the cache-hit rule above depends on.
Instance Method Summary collapse
-
#build!(dist_dir:, dev: false, dev_url: nil, format: :apk) ⇒ Object
Build the app and copy it into dist_dir.
Methods inherited from Base
Constructor Details
This class inherits a constructor from Everywhere::Builders::Base
Class Method Details
.fetch!(url, dest, sha256:, redirects: 3) ⇒ Object
Download to a sibling .part file, verify, then rename — the atomicity the cache-hit rule above depends on. A class method so tests (and the offline path) have one seam to stub instead of a live socket.
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 |
# File 'lib/everywhere/builders/android.rb', line 532 def self.fetch!(url, dest, sha256:, redirects: 3) require "net/http" require "uri" require "digest" uri = URI.parse(url) part = "#{dest}.part" Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http| http.request(Net::HTTP::Get.new(uri)) do |response| case response when Net::HTTPRedirection raise "too many redirects for #{url}" if redirects <= 0 # The font is checksummed, so a swapped body is caught anyway — # but the base is https and a redirect has no business dropping # to http on the way to a cached artifact. target = URI.join(uri, response["location"].to_s) raise "refusing a non-https redirect to #{target.host}" unless target.scheme == "https" return fetch!(target.to_s, dest, sha256: sha256, redirects: redirects - 1) when Net::HTTPSuccess File.open(part, "wb") { |f| response.read_body { |chunk| f.write(chunk) } } else raise "HTTP #{response.code} for #{url}" end end end digest = Digest::SHA256.file(part).hexdigest raise "#{File.basename(dest)} checksum mismatch (got #{digest[0, 12]}…)" unless digest == sha256 File.rename(part, dest) dest ensure FileUtils.rm_f("#{dest}.part") end |
Instance Method Details
#build!(dist_dir:, dev: false, dev_url: nil, format: :apk) ⇒ Object
Build the app and copy it into dist_dir. dev builds the debug variant
and relaxes the remote-mode requirement (the dev URL is stamped into a
debug asset each run — Android has no SIMCTL_CHILD_* equivalent).
format is :apk (installable) or :aab (what Play accepts). Returns the
path of the collected artifact.
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/everywhere/builders/android.rb', line 121 def build!(dist_dir:, dev: false, dev_url: nil, format: :apk) # The template is resolved before preflight, not after it like iOS: # preflight has to settle the icon font, and the bundled font lives in # the template. Everything that can fail slowly (a font download) or # cheaply (a typo'd icon name) is therefore decided before Gradle — the # difference between a 5-second error and one that lands 90 seconds in. template = @template_dir ? File.(@template_dir) : Paths.android_dir! preflight!(dev: dev, format: format, template: template) work = stage(template) stamp!(work) stamp_dev_url!(work, dev_url) variant = dev ? "debug" : "release" gradle!(work, variant, format, dist_dir) collect(work, variant, format, dist_dir) end |