185 lines
5.4 KiB
TypeScript
Executable File
185 lines
5.4 KiB
TypeScript
Executable File
#!/usr/bin/env runts -- --allow-all
|
|
|
|
import {
|
|
execCommand,
|
|
execCommandShell,
|
|
existsPath,
|
|
log,
|
|
stringifyPretty,
|
|
term,
|
|
uint8ArrayToHexString,
|
|
writeStringToFile,
|
|
} from "https://script.hatter.ink/@36/deno-commons-mod.ts";
|
|
import {parseArgs} from "jsr:@std/cli/parse-args";
|
|
import {readFileToString} from "https://global.hatter.ink/script/get/@1/deno-commons-1.6.0-mod.ts";
|
|
|
|
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(Deno.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.20260209T235604+08:00.MEUCIQCrTkAmvn7P/n8fONFi
|
|
// cpqS5X1+UY+jH+0z/qf1CVy15QIgUv0z4zF0gIuFKWz16/XzGDL1Hae43OEv0qT+h2ooYE8=
|