171 lines
5.4 KiB
JavaScript
171 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
import { createHash } from "node:crypto";
|
|
import { execFile } from "node:child_process";
|
|
import {
|
|
access,
|
|
chmod,
|
|
copyFile,
|
|
cp,
|
|
lstat,
|
|
mkdir,
|
|
mkdtemp,
|
|
readFile,
|
|
readdir,
|
|
rename,
|
|
rm,
|
|
utimes,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { fileURLToPath } from "node:url";
|
|
const execute = promisify(execFile);
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const pkg = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
|
|
const argument = (name, fallback) => {
|
|
const index = process.argv.indexOf(name);
|
|
return index >= 0 ? process.argv[index + 1] : fallback;
|
|
};
|
|
const output = path.resolve(
|
|
root,
|
|
argument("--output", "release/" + pkg.name + "-" + pkg.version + ".zip"),
|
|
);
|
|
const checksumOutput = output + ".sha256";
|
|
const force = process.argv.includes("--force");
|
|
if (
|
|
path.extname(output).toLowerCase() !== ".zip" ||
|
|
output === root ||
|
|
output === path.parse(output).root
|
|
)
|
|
throw new Error("Unsafe release target");
|
|
const exists = (file) =>
|
|
access(file).then(
|
|
() => true,
|
|
() => false,
|
|
);
|
|
if (!force && ((await exists(output)) || (await exists(checksumOutput))))
|
|
throw new Error("Release output exists");
|
|
const input = path.join(root, "dist");
|
|
for (const name of [
|
|
"index.html",
|
|
"manifest.webmanifest",
|
|
"sw.js",
|
|
"toolbox-app.json",
|
|
"favicon.svg",
|
|
"ocr/worker.min.js",
|
|
"ocr/core/tesseract-core-lstm.wasm.js",
|
|
"ocr/core/tesseract-core-simd-lstm.wasm.js",
|
|
"ocr/core/tesseract-core-relaxedsimd-lstm.wasm.js",
|
|
"ocr/lang/eng.traineddata.gz",
|
|
"README.md",
|
|
"CHANGELOG.md",
|
|
"CONTRIBUTING.md",
|
|
"LICENSE",
|
|
"SECURITY.md",
|
|
"SOURCE.md",
|
|
"THIRD_PARTY_NOTICES.md",
|
|
"LICENSES/README.md",
|
|
"LICENSES/npm-runtime-licenses.txt",
|
|
"docs/ACCESSIBILITY.md",
|
|
"docs/ARCHITECTURE.md",
|
|
"docs/PRIVACY-SECURITY.md",
|
|
]) {
|
|
const details = await lstat(path.join(input, name)).catch(() => null);
|
|
if (!details?.isFile() || details.isSymbolicLink())
|
|
throw new Error("Missing release file: " + name);
|
|
}
|
|
const manifest = JSON.parse(
|
|
await readFile(path.join(input, "toolbox-app.json"), "utf8"),
|
|
);
|
|
const repository = "https://git.add-ideas.de/lotobo/" + pkg.name;
|
|
if (
|
|
manifest.id !== "de.add-ideas." + pkg.name ||
|
|
manifest.version !== pkg.version ||
|
|
manifest.entry !== "./" ||
|
|
manifest.icon !== "./favicon.svg" ||
|
|
manifest.source?.repository !== repository
|
|
)
|
|
throw new Error("Packaged manifest identity is invalid");
|
|
if (
|
|
/\b(?:src|href)=["']\//iu.test(
|
|
await readFile(path.join(input, "index.html"), "utf8"),
|
|
)
|
|
)
|
|
throw new Error("Root-absolute asset reference");
|
|
async function collect(directory, prefix = "") {
|
|
const files = [];
|
|
for (const entry of (await readdir(directory, { withFileTypes: true })).sort(
|
|
(a, b) => a.name.localeCompare(b.name),
|
|
)) {
|
|
const absolute = path.join(directory, entry.name);
|
|
const relative = prefix ? prefix + "/" + entry.name : entry.name;
|
|
if (entry.isSymbolicLink())
|
|
throw new Error("Symlink in release: " + relative);
|
|
if (entry.isDirectory()) files.push(...(await collect(absolute, relative)));
|
|
else if (entry.isFile()) files.push({ absolute, relative });
|
|
else throw new Error("Unsupported release entry: " + relative);
|
|
}
|
|
return files;
|
|
}
|
|
const sourceFiles = await collect(input);
|
|
for (const file of sourceFiles)
|
|
if (
|
|
file.relative.endsWith(".map") ||
|
|
/(?:^|\/)(?:\.env(?:\.|$)|id_rsa|id_ed25519|.*\.pem$|.*\.key$)/iu.test(
|
|
file.relative,
|
|
) ||
|
|
file.relative.split("/").includes("..")
|
|
)
|
|
throw new Error("Forbidden release entry: " + file.relative);
|
|
await mkdir(path.dirname(output), { recursive: true });
|
|
const stagingRoot = await mkdtemp(
|
|
path.join(os.tmpdir(), pkg.name + "-release-"),
|
|
);
|
|
const publicationRoot = await mkdtemp(
|
|
path.join(path.dirname(output), "." + pkg.name + "-publish-"),
|
|
);
|
|
const stagedTree = path.join(stagingRoot, "tree");
|
|
const stagedArchive = path.join(stagingRoot, path.basename(output));
|
|
try {
|
|
await cp(input, stagedTree, { recursive: true });
|
|
const timestamp = new Date("1980-01-01T00:00:00.000Z");
|
|
for (const file of await collect(stagedTree)) {
|
|
await chmod(file.absolute, 0o644);
|
|
await utimes(file.absolute, timestamp, timestamp);
|
|
}
|
|
await execute(
|
|
"zip",
|
|
[
|
|
"-X",
|
|
"-q",
|
|
"-9",
|
|
stagedArchive,
|
|
...sourceFiles.map((file) => file.relative),
|
|
],
|
|
{
|
|
cwd: stagedTree,
|
|
env: { ...process.env, TZ: "UTC" },
|
|
maxBuffer: 1024 * 1024,
|
|
},
|
|
);
|
|
const archive = await readFile(stagedArchive);
|
|
const digest = createHash("sha256").update(archive).digest("hex");
|
|
const stagedChecksum = stagedArchive + ".sha256";
|
|
await writeFile(stagedChecksum, digest + " " + path.basename(output) + "\n");
|
|
const publicationArchive = path.join(publicationRoot, path.basename(output));
|
|
const publicationChecksum = publicationArchive + ".sha256";
|
|
await copyFile(stagedArchive, publicationArchive);
|
|
await copyFile(stagedChecksum, publicationChecksum);
|
|
if (force) {
|
|
await rm(output, { force: true });
|
|
await rm(checksumOutput, { force: true });
|
|
}
|
|
await rename(publicationArchive, output);
|
|
await rename(publicationChecksum, checksumOutput);
|
|
console.log("Created " + path.relative(root, output) + "\nSHA-256 " + digest);
|
|
} finally {
|
|
await rm(stagingRoot, { recursive: true, force: true });
|
|
await rm(publicationRoot, { recursive: true, force: true });
|
|
}
|