#!/usr/bin/env node /** * Builds the release APK and stages it under release/ under a name a stranger * can recognise, next to the checksum to verify a download against. * * It verifies the signature rather than trusting that the signing config took: * an unsigned APK builds happily and only fails at install time, on someone * else's phone. */ import { createHash } from "node:crypto"; import { execFileSync } from "node:child_process"; import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); execFileSync("npm", ["run", "apk:release"], { cwd: root, stdio: "inherit" }); const built = join(root, "android/app/build/outputs/apk/release/app-release.apk"); const name = `404-Layers-${version}.apk`; const out = join(root, "release", name); mkdirSync(dirname(out), { recursive: true }); copyFileSync(built, out); const bytes = readFileSync(out); const sha = createHash("sha256").update(bytes).digest("hex"); writeFileSync(`${out}.sha256`, `${sha} ${name}\n`); // build-tools is pinned in CLAUDE.md; a missing apksigner should fail loudly. const sdk = process.env.ANDROID_HOME ?? join(process.env.HOME ?? "", "Android/Sdk"); const certs = execFileSync(join(sdk, "build-tools/36.1.0/apksigner"), ["verify", "--print-certs", out], { encoding: "utf8", }); const digest = certs.match(/certificate SHA-256 digest: (\w+)/)?.[1] ?? ""; if (digest === "") throw new Error(`apksigner reported no certificate:\n${certs}`); console.log(`\n${name} ${(bytes.length / 1e6).toFixed(2)} MB`); console.log(`file sha256 ${sha}`); console.log(`cert sha256 ${digest}`); console.log(`\nStaged at ${out}`);