Class: Everywhere::Builders::Android

Inherits:
Base
  • Object
show all
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

Base::STAGE_MARKER

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#initialize

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.expand_path(@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