Files
2026-02-11 22:37:32 +08:00

186 lines
5.3 KiB
TypeScript
Executable File

#!/usr/bin/env runts -- --allow-all
import {
args,
execCommand,
execCommandShell,
existsPath,
log,
readFileToString,
stringifyPretty,
term,
uint8ArrayToHexString,
writeStringToFile,
} from "https://script.hatter.ink/@39/deno-commons-mod.ts";
import {parseArgs} from "jsr:@std/cli/parse-args";
function handleHelp() {
console.log(term.auto(`Build scripts to bundle and sign.
Usage:
[blue][bold]build.ts[//] --help
[blue][bold]build.ts[//] [-M|--no-min] [-S|--skip-sign] [-F|--force] [bold]FILES[/]`));
}
async function main() {
const flags = parseArgs(args(), {
boolean: [
"help",
"no-min",
"skip-sign",
"force",
],
alias: {
h: "help",
S: "skip-sign",
M: "no-min",
F: "force",
},
});
if (flags.help) {
handleHelp();
return;
}
if (flags._.length === 0) {
log.error(`No files specified`);
return;
}
flags.min = !flags["no-min"];
const files: string[] = flags._.filter((f) => {
return f.endsWith(".ts") && !f.includes(".bundle.");
});
const filesCount = files.length;
log.info(`Total ${filesCount} file(s)`);
for (let i = 0; i < filesCount; i++) {
const file = files[i];
if (file.includes(".bundle.") || !file.endsWith(".ts")) {
log.warn(`Skip bundle file: ${file} #${i + 1} of ${filesCount}`);
continue;
}
log.info(`Building file: ${file} #${i + 1} of ${filesCount}`);
await buildFile(file, flags);
}
}
function getBundleFilename(file: string): string {
const lastIndexOfDot = file.lastIndexOf(".");
if (lastIndexOfDot === -1) {
return `${file}.bundle.ts`;
}
return `${file.substring(0, lastIndexOfDot)}.bundle.${
file.substring(lastIndexOfDot + 1)
}`;
}
async function sha256OfString(input: string): Promise<string> {
const data = new TextEncoder().encode(input);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return uint8ArrayToHexString(new Uint8Array(hashBuffer));
}
interface SourceFileMeta {
sha256: string;
builtTime: number;
min: boolean;
}
async function writeBundleSourceSha256(
sourceFile: string,
bundleFile: string,
flags: any,
) {
const sourceFileSha256 = `${bundleFile}.source-meta`;
const sourceFileContent = await readFileToString(sourceFile);
const sourceFileContentSha256 = await sha256OfString(sourceFileContent);
await writeStringToFile(
sourceFileSha256,
stringifyPretty({
sha256: sourceFileContentSha256,
builtTime: Date.now(),
min: flags.min,
} as SourceFileMeta),
);
}
async function checkBundleSourceSha256(
sourceFile: string,
bundleFile: string,
flags: any,
) {
const sourceFileSha256 = `${bundleFile}.source-meta`;
if (!await existsPath(sourceFileSha256)) {
return false;
}
const sourceFileSha256Content = await readFileToString(sourceFileSha256);
const sourceFileSha256Meta = JSON.parse(
sourceFileSha256Content,
) as SourceFileMeta;
if (sourceFileSha256Meta.min !== flags.min) {
return false;
}
const sourceFileContent = await readFileToString(sourceFile);
const sourceFileContentSha256 = await sha256OfString(sourceFileContent);
return sourceFileContentSha256 === sourceFileSha256Meta.sha256;
}
async function buildFile(file: string, flags: any) {
const denoCmd = "deno";
const bundleFile = getBundleFilename(file);
const denoBundleArgs = ["bundle"];
if (flags.min) {
denoBundleArgs.push("--minify");
}
denoBundleArgs.push("--allow-import");
denoBundleArgs.push(file);
denoBundleArgs.push("-o");
denoBundleArgs.push(bundleFile);
const isSourceFileMatches = await checkBundleSourceSha256(
file,
bundleFile,
flags,
);
if (isSourceFileMatches && !flags.force) {
log.info(`Check file ${file} sha256 matches, skip build`);
return;
}
if (isSourceFileMatches) {
log.info(`Force build file is on`);
}
log.debug("Run deno build: ", denoCmd, denoBundleArgs);
await execCommandShell(denoCmd, denoBundleArgs);
if (flags["skip-sign"]) {
log.warn(`Skip signature for file: ${bundleFile}`);
} else {
const signScriptUserPinOutput = await execCommand("keyring.rs", [
"-gRU",
"yubikey:script-sign",
]);
const scriptSignArgs: string[] = [];
if (signScriptUserPinOutput.code !== 0) {
log.warn(`Read script sign PIN failed: `, signScriptUserPinOutput);
} else {
scriptSignArgs.push("--pin");
scriptSignArgs.push(signScriptUserPinOutput.stdoutThenTrim());
}
scriptSignArgs.push(bundleFile);
const ret = await execCommandShell("script-sign.rs", scriptSignArgs);
if (ret !== 0) {
log.error(`Sign script: ${bundleFile} failed, ret code: ${ret}`);
return;
}
}
await execCommandShell("chmod", ["+x", bundleFile]);
await writeBundleSourceSha256(file, bundleFile, flags);
}
await main();
// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20260211T222821+08:00.MEQCIAweu8hlz6/t/UPWtHzB
// 6fm4DZPTSfP7NBPSjad5qmB9AiAkpgAvpLAD1qKd+Cz0KBJT6GQq30yt1RKEbYtgKEQjHA==