import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; const root = path.resolve(import.meta.dir, ".."); const app = process.argv[2] ?? "MoanNote"; if (!/^[A-Za-z0-9_-]+$/.test(app)) throw new Error("App name must be a simple file name"); const unsignedApk = path.join(root, "outputs", `${app}_patched.apk`); const signedApk = path.join(root, "outputs", `${app}_patched-signed.apk`); const signingDir = path.join(root, "build", "signing"); const keyName = `${app.toLowerCase()}-dev`; const keystore = path.join(signingDir, `${keyName}.jks`); const credentials = path.join(signingDir, `${keyName}.env`); function run(command: string, args: string[]): void { const result = spawnSync(command, args, { cwd: root, stdio: "inherit" }); if (result.error) throw result.error; if (result.status !== 0) throw new Error( `${path.basename(command)} failed with status ${result.status}`, ); } function readCredentials(): Record { return Object.fromEntries( fs .readFileSync(credentials, "utf8") .trim() .split("\n") .filter(Boolean) .map((line) => line.split("=", 2) as [string, string]), ); } function androidTool(name: "apksigner" | "zipalign"): string { const sdk = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT ?? "/var/home/hellings/android-sdk"; for (const version of ["36.0.0", "35.0.0", "34.0.0"]) { const tool = path.join(sdk, "build-tools", version, name); if (fs.existsSync(tool)) return tool; } throw new Error(`Android ${name} was not found under ${sdk}/build-tools`); } function ensureDevKey(): Record { fs.mkdirSync(signingDir, { recursive: true, mode: 0o700 }); if (!fs.existsSync(keystore)) { const password = crypto.randomBytes(32).toString("base64url"); const values = { storePassword: password, keyPassword: password, alias: keyName, }; run("keytool", [ "-genkeypair", "-keystore", keystore, "-storetype", "PKCS12", "-storepass", password, "-alias", values.alias, "-keypass", password, "-keyalg", "RSA", "-keysize", "2048", "-validity", "3650", "-dname", `CN=${app} Development,O=rip.crit.ms,C=US`, ]); fs.writeFileSync( credentials, Object.entries(values) .map(([key, value]) => `${key}=${value}`) .join("\n") + "\n", { mode: 0o600 }, ); console.log(`[ok] Created local ${app} development key at ${keystore}`); } if (!fs.existsSync(credentials)) throw new Error( `Development key credentials are missing at ${credentials}`, ); return readCredentials(); } if (!fs.existsSync(unsignedApk)) throw new Error(`Patched APK is missing at ${unsignedApk}`); const values = ensureDevKey(); const aligned = path.join(root, "build", `${app}_patched-aligned.apk`); fs.mkdirSync(path.dirname(aligned), { recursive: true }); run(androidTool("zipalign"), ["-f", "-p", "4", unsignedApk, aligned]); run(androidTool("apksigner"), [ "sign", "--ks", keystore, "--ks-key-alias", values.alias!, "--ks-pass", `pass:${values.storePassword!}`, "--key-pass", `pass:${values.keyPassword!}`, "--out", signedApk, aligned, ]); run(androidTool("apksigner"), [ "verify", "--verbose", "--print-certs", signedApk, ]); console.log(`[ok] Signed ${signedApk}`);