70 lines
3.1 KiB
TypeScript
70 lines
3.1 KiB
TypeScript
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<string, string> {
|
|
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<string, string> {
|
|
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}`);
|