130 lines
4.4 KiB
TypeScript
130 lines
4.4 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import { spawnSync } from "node:child_process";
|
|
import path from "node:path";
|
|
|
|
const root = path.resolve(import.meta.dir, "..");
|
|
const args = process.argv.slice(2);
|
|
|
|
const app = value("--app") ?? "MoanNote";
|
|
const profiles: Record<
|
|
string,
|
|
{
|
|
patchScript: string;
|
|
legacyPackage: string;
|
|
patchedPackage: string;
|
|
removeLegacy: boolean;
|
|
legacySystemImage: boolean;
|
|
}
|
|
> = {
|
|
MoanNote: {
|
|
patchScript: "patch:moannotes",
|
|
legacyPackage: "com.moan.notes",
|
|
patchedPackage: "rip.crit.ms.com.moan.notes",
|
|
removeLegacy: false,
|
|
legacySystemImage: true,
|
|
},
|
|
MoanLauncher: {
|
|
patchScript: "patch:moanlauncher",
|
|
legacyPackage: "com.note.launcher",
|
|
patchedPackage: "rip.crit.ms.com.note.launcher",
|
|
removeLegacy: false,
|
|
legacySystemImage: true,
|
|
},
|
|
};
|
|
const profile = profiles[app];
|
|
if (!profile) throw new Error(`Unsupported app ${app}`);
|
|
|
|
function value(flag: string): string | undefined {
|
|
const index = args.indexOf(flag);
|
|
return index >= 0 ? args[index + 1] : undefined;
|
|
}
|
|
|
|
function run(command: string, commandArgs: string[]): void {
|
|
const result = spawnSync(command, commandArgs, {
|
|
cwd: root,
|
|
stdio: "inherit",
|
|
});
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0)
|
|
throw new Error(`${command} ${commandArgs.join(" ")} failed`);
|
|
}
|
|
|
|
function uninstallIfPresent(serial: string, packageName: string): void {
|
|
const result = spawnSync("adb", ["-s", serial, "uninstall", packageName], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
});
|
|
if (result.error) throw result.error;
|
|
if (result.status === 0) {
|
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
return;
|
|
}
|
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
if (/not installed/i.test(output)) {
|
|
console.log(`[info] ${packageName} was not installed`);
|
|
return;
|
|
}
|
|
// OEM apps in /system cannot be uninstalled from the system partition. For a
|
|
// clean, per-user replacement, Package Manager can remove that system app
|
|
// for user 0. This also clears its user data.
|
|
if (/DELETE_FAILED_INTERNAL_ERROR|unknown system package/i.test(output)) {
|
|
const perUserResult = spawnSync(
|
|
"adb",
|
|
["-s", serial, "shell", "pm", "uninstall", "--user", "0", packageName],
|
|
{ cwd: root, encoding: "utf8" },
|
|
);
|
|
if (perUserResult.error) throw perUserResult.error;
|
|
if (perUserResult.status === 0) {
|
|
if (perUserResult.stdout) process.stdout.write(perUserResult.stdout);
|
|
console.log(`[info] Removed system app ${packageName} for user 0`);
|
|
return;
|
|
}
|
|
const perUserOutput = `${perUserResult.stdout ?? ""}${perUserResult.stderr ?? ""}`;
|
|
if (/not installed/i.test(perUserOutput)) {
|
|
console.log(`[info] ${packageName} was not installed for user 0`);
|
|
return;
|
|
}
|
|
if (perUserOutput) process.stderr.write(perUserOutput);
|
|
}
|
|
if (output) process.stderr.write(output);
|
|
throw new Error(`Unable to remove ${packageName}`);
|
|
}
|
|
|
|
function requireConnectedDevice(serial: string): void {
|
|
const result = spawnSync("adb", ["-s", serial, "get-state"], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
});
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0 || result.stdout.trim() !== "device")
|
|
throw new Error(`ADB device ${serial} is not connected and authorized`);
|
|
}
|
|
|
|
const serial = value("--serial");
|
|
if (!serial)
|
|
throw new Error("Specify the Xiaomi target with --serial <adb-serial>");
|
|
if (!args.includes("--confirm-reinstall"))
|
|
throw new Error("Refusing to replace an app without --confirm-reinstall");
|
|
|
|
requireConnectedDevice(serial);
|
|
run("bun", ["run", profile.patchScript]);
|
|
const apk =
|
|
value("--apk") ?? path.join(root, "outputs", `${app}_patched-signed.apk`);
|
|
if (!existsSync(apk)) {
|
|
throw new Error(
|
|
`Signed APK is missing at ${path.join("outputs", `${app}_patched-signed.apk`)}`,
|
|
);
|
|
}
|
|
// Android can reject an uninstall of an already-patched package with
|
|
// DELETE_FAILED_INTERNAL_ERROR, even though `adb install -r` updates the same
|
|
// package normally. Keep the patched app in place and only remove the original
|
|
// package when its unchanged provider authority would conflict with the patch.
|
|
if (profile.removeLegacy) {
|
|
uninstallIfPresent(serial, profile.legacyPackage);
|
|
} else {
|
|
console.log(`[info] Keeping protected OEM package ${profile.legacyPackage}`);
|
|
}
|
|
run("adb", ["-s", serial, "install", "-r", "--no-incremental", apk]);
|
|
console.log(
|
|
`[ok] Installed ${path.basename(apk)}${profile.legacySystemImage ? ` alongside protected OEM system image ${profile.legacyPackage}` : profile.removeLegacy ? ` after replacing ${profile.legacyPackage}` : " as a side-by-side update"}.`,
|
|
);
|