Some checks failed
check / check (push) Has been cancelled
The both-markers diagnostic claimed the debug branch was still live. It is
not: with the __BUILD_DEBUG__ define removed, the emitted bundle carries
`typeof __BUILD_DEBUG__<"u"?__BUILD_DEBUG__:!1`, and in extension context the
identifier is undeclared, so DEBUG evaluates to false at runtime. The message
now states what the check does prove -- DEBUG was not resolved at build time,
so the release/debug distinction is no longer enforced and which way the
unresolved fallback evaluates is an accident a refactor can flip -- and it
remains a hard failure. The other seven failure messages were reviewed and
none needed rewording.
has_marker no longer swallows grep's exit 2 with 2>/dev/null. Match and
no-match are answers about the emitted output; an unreadable file is not, and
is now reported as a permissions or I/O fault instead of as "the emitted
output changed shape". Both paths still fail hard. The manifest-membership
grep gets the same treatment in is_listed: an unreadable manifest is no longer
answered as "this file is not listed".
The unlisted-bundle scan no longer filters by extension, so the endsWith(".js")
test in build.js is the only place that assumption lives. A bundle emitted
under another extension previously escaped the manifest and the cross-check at
once; it now fails as unlisted. Both sites carry a comment naming the other.
That makes the scan the sole guard on build.js's filter, so its walk has to be
exhaustive rather than assumed to be. find's exit status was discarded twice
over -- the pipeline reported sort's status, and set -e does not fire on an
assignment from a successful pipeline -- so a subtree find could not descend
printed to stderr and was then silently omitted, and an unlisted marker-carrying
bundle inside a chmod 000 directory passed green. The status is now captured
and a non-zero find is a hard failure naming the unwalked tree; the sort moved
off the status-bearing pipeline. Symlinks are walked as well: a marker-carrying
bundle reachable under an unlisted path is a stale manifest whether the path is
a link or a file, and a link that cannot be read through fails closed via the
exit-2 path.
Also: the manifest must be readable and a listed bundle must be non-empty,
so a vacuous input fails loudly rather than reaching a marker check that
cannot prove anything.
212 lines
7.6 KiB
JavaScript
212 lines
7.6 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const { execSync } = require("child_process");
|
|
const esbuild = require("esbuild");
|
|
|
|
const DIST = path.join(__dirname, "dist");
|
|
const DIST_CHROME = path.join(DIST, "chrome");
|
|
const DIST_FIREFOX = path.join(DIST, "firefox");
|
|
const SRC = path.join(__dirname, "src");
|
|
|
|
// The module whose compiled DEBUG state script/verify-build asserts, and the
|
|
// manifest naming every emitted bundle that ends up containing it. The
|
|
// manifest is derived from esbuild's own dependency graph rather than from a
|
|
// hardcoded list, so it tracks the bundle layout instead of rotting with it.
|
|
const AUDITED_MODULE = "src/shared/constants.js";
|
|
const BUNDLE_MANIFEST = path.join(DIST, "constants-bundles.txt");
|
|
|
|
function ensureDir(dir) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
// Repo-relative, forward-slashed, so the manifest reads the same on every
|
|
// platform and can be consumed by a POSIX shell script without further work.
|
|
function repoRelative(p) {
|
|
return path.relative(__dirname, p).split(path.sep).join("/");
|
|
}
|
|
|
|
// Collect the outputs of one esbuild run that bundle AUDITED_MODULE. esbuild
|
|
// reports every input that contributed to an output in the metafile, which is
|
|
// the authoritative answer to "is constants.js in this bundle" — unlike
|
|
// searching the minified text, it does not depend on what survived minification.
|
|
//
|
|
// The ".js" filter below is the only place that assumption lives:
|
|
// script/verify-build searches every file and symlink under dist/ for a
|
|
// marker, without filtering by extension, and hard-fails if it cannot walk the
|
|
// whole tree, so a bundle emitted under some other extension fails there as
|
|
// unlisted rather than escaping both checks at once.
|
|
function outputsContainingAuditedModule(metafile) {
|
|
return Object.entries(metafile.outputs)
|
|
.filter(([outFile, info]) => {
|
|
if (!outFile.endsWith(".js")) return false;
|
|
return Object.keys(info.inputs).some(
|
|
(input) => repoRelative(input) === AUDITED_MODULE,
|
|
);
|
|
})
|
|
.map(([outFile]) => repoRelative(outFile));
|
|
}
|
|
|
|
// DEBUG is a build-time flag, off unless explicitly requested. It is the only
|
|
// thing that makes the hardcoded test mnemonic reachable, so the opt-in must be
|
|
// exact: anything other than the literal "1" (unset, empty, "true", a typo)
|
|
// produces a release build. Failing towards the safe mode is deliberate.
|
|
function isDebugBuild() {
|
|
return process.env.AUTISTMASK_DEBUG === "1";
|
|
}
|
|
|
|
function getBuildInfo() {
|
|
const pkg = JSON.parse(
|
|
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
|
|
);
|
|
let commitHash = "unknown";
|
|
try {
|
|
commitHash = execSync("git rev-parse --short HEAD", {
|
|
encoding: "utf8",
|
|
}).trim();
|
|
} catch (_) {
|
|
// not a git repo or git not available
|
|
}
|
|
let commitHashFull = "unknown";
|
|
try {
|
|
commitHashFull = execSync("git rev-parse HEAD", {
|
|
encoding: "utf8",
|
|
}).trim();
|
|
} catch (_) {
|
|
// not a git repo or git not available
|
|
}
|
|
return {
|
|
version: pkg.version,
|
|
license: pkg.license,
|
|
author: pkg.author,
|
|
commitHash,
|
|
commitHashFull,
|
|
buildDate: new Date().toISOString().slice(0, 10),
|
|
};
|
|
}
|
|
|
|
async function build() {
|
|
console.log("Building AutistMask extension...");
|
|
|
|
const buildInfo = getBuildInfo();
|
|
console.log("Build info:", buildInfo);
|
|
|
|
const debugBuild = isDebugBuild();
|
|
console.log(
|
|
debugBuild
|
|
? "Build mode: DEBUG (INSECURE - hardcoded test mnemonic, do not ship)"
|
|
: "Build mode: release (DEBUG off)",
|
|
);
|
|
|
|
const define = {
|
|
__BUILD_DEBUG__: JSON.stringify(debugBuild),
|
|
__BUILD_VERSION__: JSON.stringify(buildInfo.version),
|
|
__BUILD_LICENSE__: JSON.stringify(buildInfo.license),
|
|
__BUILD_AUTHOR__: JSON.stringify(buildInfo.author),
|
|
__BUILD_COMMIT__: JSON.stringify(buildInfo.commitHash),
|
|
__BUILD_COMMIT_FULL__: JSON.stringify(buildInfo.commitHashFull),
|
|
__BUILD_DATE__: JSON.stringify(buildInfo.buildDate),
|
|
};
|
|
|
|
// Emitted bundles that contain constants.js, accumulated across every
|
|
// esbuild run below and written out for script/verify-build.
|
|
const auditedBundles = [];
|
|
|
|
// compile tailwind CSS
|
|
console.log("Compiling Tailwind CSS...");
|
|
const tailwindInput = path.join(SRC, "popup", "styles", "main.css");
|
|
const tailwindOutput = path.join(DIST, "styles.css");
|
|
ensureDir(DIST);
|
|
|
|
// Drop any manifest from a previous build before emitting anything, so a
|
|
// build that never gets around to writing one cannot be verified against
|
|
// a stale list.
|
|
fs.rmSync(BUNDLE_MANIFEST, { force: true });
|
|
execSync(
|
|
`npx @tailwindcss/cli -i ${tailwindInput} -o ${tailwindOutput} --minify`,
|
|
{ stdio: "inherit" },
|
|
);
|
|
|
|
// Every bundle goes through here, so metafile collection cannot be
|
|
// forgotten when a new entry point is added.
|
|
async function bundle(entryPoint, outfile) {
|
|
const result = await esbuild.build({
|
|
entryPoints: [entryPoint],
|
|
bundle: true,
|
|
format: "iife",
|
|
outfile,
|
|
platform: "browser",
|
|
target: ["chrome110", "firefox110"],
|
|
minify: true,
|
|
metafile: true,
|
|
define,
|
|
});
|
|
auditedBundles.push(...outputsContainingAuditedModule(result.metafile));
|
|
}
|
|
|
|
for (const distDir of [DIST_CHROME, DIST_FIREFOX]) {
|
|
ensureDir(path.join(distDir, "src", "popup"));
|
|
ensureDir(path.join(distDir, "src", "background"));
|
|
ensureDir(path.join(distDir, "src", "content"));
|
|
|
|
// bundle popup JS with esbuild (inlines ethers, libsodium, etc.)
|
|
await bundle(
|
|
path.join(SRC, "popup", "index.js"),
|
|
path.join(distDir, "src", "popup", "index.js"),
|
|
);
|
|
|
|
// bundle background script
|
|
await bundle(
|
|
path.join(SRC, "background", "index.js"),
|
|
path.join(distDir, "src", "background", "index.js"),
|
|
);
|
|
|
|
// bundle content script
|
|
await bundle(
|
|
path.join(SRC, "content", "index.js"),
|
|
path.join(distDir, "src", "content", "index.js"),
|
|
);
|
|
|
|
// bundle inpage script (injected into page context, separate file)
|
|
await bundle(
|
|
path.join(SRC, "content", "inpage.js"),
|
|
path.join(distDir, "src", "content", "inpage.js"),
|
|
);
|
|
|
|
// copy popup HTML
|
|
fs.copyFileSync(
|
|
path.join(SRC, "popup", "index.html"),
|
|
path.join(distDir, "src", "popup", "index.html"),
|
|
);
|
|
|
|
// place compiled CSS next to popup HTML
|
|
fs.copyFileSync(
|
|
tailwindOutput,
|
|
path.join(distDir, "src", "popup", "styles.css"),
|
|
);
|
|
}
|
|
|
|
// copy manifests
|
|
fs.copyFileSync(
|
|
path.join(__dirname, "manifest", "chrome.json"),
|
|
path.join(DIST_CHROME, "manifest.json"),
|
|
);
|
|
fs.copyFileSync(
|
|
path.join(__dirname, "manifest", "firefox.json"),
|
|
path.join(DIST_FIREFOX, "manifest.json"),
|
|
);
|
|
|
|
// Written last so a build that died partway through leaves no manifest
|
|
// at all, which script/verify-build treats as a hard failure rather than
|
|
// as "nothing to check".
|
|
const manifest = [...new Set(auditedBundles)].sort();
|
|
fs.writeFileSync(BUNDLE_MANIFEST, manifest.map((p) => `${p}\n`).join(""));
|
|
console.log(
|
|
`Bundles containing ${AUDITED_MODULE}: ${manifest.length} ` +
|
|
`(listed in ${repoRelative(BUNDLE_MANIFEST)})`,
|
|
);
|
|
|
|
console.log("Build complete: dist/chrome/ and dist/firefox/");
|
|
}
|
|
|
|
build();
|