209 lines
7.3 KiB
JavaScript
209 lines
7.3 KiB
JavaScript
/**
|
|
* This file executes build.js in a child process, this is needed for two things:
|
|
* 1. Enable interrupts like Ctrl+C for regular builds
|
|
* 2. Support building older versions of Dark Reader and then inserting signatures into archives
|
|
*/
|
|
|
|
// @ts-check
|
|
import assert from 'node:assert/strict';
|
|
import {fork} from 'node:child_process';
|
|
import {rm, stat} from 'node:fs/promises';
|
|
import {join} from 'node:path';
|
|
import process from 'node:process';
|
|
import {fileURLToPath} from 'node:url';
|
|
|
|
import signature from './bundle-signature.js';
|
|
import {PLATFORM} from './platform.js';
|
|
import {runTasks} from './task.js';
|
|
import {execute, log} from './utils.js';
|
|
import zip from './zip.js';
|
|
|
|
|
|
const __filename = join(fileURLToPath(import.meta.url), '../build.js');
|
|
|
|
function getSignatureDir(version) {
|
|
return join(fileURLToPath(import.meta.url), `../../integrity/firefox/`, version);
|
|
}
|
|
|
|
async function executeChildProcess(args) {
|
|
const child = fork(__filename, args);
|
|
// Send SIGINTs as SIGKILLs, which are not ignored
|
|
process.on('SIGINT', () => {
|
|
child.kill('SIGKILL');
|
|
process.exit(130);
|
|
});
|
|
return new Promise((resolve, reject) => child.on('error', reject).on('close', resolve));
|
|
}
|
|
|
|
function printHelp() {
|
|
console.log([
|
|
'Dark Reader build utility',
|
|
'',
|
|
'Usage: build [build parameters]',
|
|
'',
|
|
'To narrow down the list of build targets (for efficiency):',
|
|
' --api Library build (published to NPM)',
|
|
' --chrome MV2 for Chromium-based browsers (published to Chrome Web Store)',
|
|
' --chrome-mv3 MV3 for Chromium-based browsers (will replace MV2 version eventually)',
|
|
' --firefox MV2 for Firefox (published to Mozilla Add-on store)',
|
|
' --thunderbird Thunderbird',
|
|
'',
|
|
'To specify type of build:',
|
|
' --release Release bundle for signing prior to publication',
|
|
' --version=* Released bundle complete with digital signature (Firefox only)',
|
|
' --debug Build for development',
|
|
' --watch Incremental build for development',
|
|
'',
|
|
'To log errors to disk (for debugging and bug reports):',
|
|
' --log-info Log lots of data',
|
|
' --log-warn Log only warnings',
|
|
'',
|
|
'Build for testing (not to be used by humans):',
|
|
' --test',
|
|
].join('\n'));
|
|
}
|
|
|
|
function getVersion(args) {
|
|
const prefix = '--version=';
|
|
const arg = args.find((arg) => arg.startsWith(prefix));
|
|
if (!arg) {
|
|
return null;
|
|
}
|
|
const version = arg.substring(prefix.length);
|
|
if (/^\d+(.\d+){0,3}$/.test(version)) {
|
|
return version;
|
|
}
|
|
throw new Error(`Invalid version argument ${version}`);
|
|
}
|
|
|
|
async function ensureGitClean() {
|
|
const diff = await execute('git diff');
|
|
if (diff) {
|
|
throw new Error('git source tree is not clean. Pease commit your work and try again');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks out a particular revision of source code and dependencies,
|
|
* audits dependencies and applies fixes to vulnerabilities.
|
|
* Fixes for vulnerabilities should not affect build output since most
|
|
* vulnerabilities reside in code which never gets reached during build.
|
|
* However, fixing the vulnerabilities and obtaining a build with all "clean"
|
|
* dependencies which is identical to already published version serves as a proof
|
|
* that the published version was always free of (now known) vulnerabilities.
|
|
*
|
|
* @param {string} version The desired git version, e.g., 'v4.9.63' or 'v4.9.37.1'
|
|
* @param {boolean} fixVulnerabilities Whether of not to attempt to fix known vulnerabilities
|
|
*/
|
|
async function checkoutVersion(version, fixVulnerabilities) {
|
|
log.ok(`Checking out version ${version}`);
|
|
// Use -- to disambiguate the tag (release version) and file paths
|
|
await rm('src', {force: true, recursive: true});
|
|
await execute(`git restore --source v${version} -- package.json package-lock.json src/ tasks/`);
|
|
log.ok(`Installing dependencies`);
|
|
await execute('npm install --ignore-scripts');
|
|
if (!fixVulnerabilities) {
|
|
log.ok(`Skipping dependency audit`);
|
|
return;
|
|
}
|
|
log.ok(`Dependency audit`);
|
|
const deps = JSON.parse(await execute('npm audit fix --force --ignore-scripts --json'));
|
|
if (deps.audit.auditReportVersion !== 2) {
|
|
throw new Error('Could not audit dependencies');
|
|
}
|
|
if (deps.audit.metadata.vulnerabilities.total !== 0) {
|
|
throw new Error('Dependency vulnerability without a fix found, please audit manually');
|
|
}
|
|
}
|
|
|
|
async function checkoutHead() {
|
|
// Restore current files
|
|
await execute('git restore --source HEAD -- package.json package-lock.json src/ tasks/');
|
|
// Clean up files which existed earlier but were deleted
|
|
await execute('git clean -f -- package.json package-lock.json src/ tasks/');
|
|
await execute('npm install --ignore-scripts');
|
|
}
|
|
|
|
function validateArguments(args) {
|
|
const validationErrors = [];
|
|
|
|
const validFlags = ['--api', '--chrome', '--chrome-mv2', '--chrome-mv3', '--firefox', '--firefox-mv2', '--thunderbird', '--release', '--debug', '--watch', '--plus', '--log-info', '--log-warn', '--test'];
|
|
const invalidFlags = args.filter((flag) => !validFlags.includes(flag) && !flag.startsWith('--version='));
|
|
invalidFlags.forEach((flag) => validationErrors.push(`Invalid flag ${flag}`));
|
|
|
|
if (args.some((arg) => arg.startsWith('--version='))) {
|
|
if (!args.includes('--firefox') || !args.includes('--release') || args.length !== 3) {
|
|
validationErrors.push('Only Firefox build currently supports signed builds');
|
|
}
|
|
}
|
|
return validationErrors;
|
|
}
|
|
|
|
function parseArguments(args) {
|
|
return args.filter((arg) => !arg.startsWith('--version='));
|
|
}
|
|
|
|
async function run() {
|
|
const args = process.argv.slice(3);
|
|
|
|
const shouldPrintHelp = args.length === 0 || process.argv[2] !== 'build' || args.includes('-h') || args.includes('--help');
|
|
if (shouldPrintHelp) {
|
|
printHelp();
|
|
process.exit(0);
|
|
}
|
|
|
|
const validationErrors = validateArguments(args);
|
|
if (validationErrors.length > 0) {
|
|
validationErrors.forEach(log.error);
|
|
printHelp();
|
|
process.exit(130);
|
|
}
|
|
|
|
const version = getVersion(args);
|
|
|
|
// If building signed build, check that required signature files exist
|
|
if (version) {
|
|
try {
|
|
const signatureDir = getSignatureDir(version);
|
|
const stats = await stat(signatureDir);
|
|
assert(stats.isDirectory());
|
|
} catch (e) {
|
|
console.log(`Could not find signature files for version ${version}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// We need to install new deps prior to forking for them to be loaded properly
|
|
if (version) {
|
|
try {
|
|
await ensureGitClean();
|
|
await checkoutVersion(version, args.includes('--fix-deps'));
|
|
} catch (e) {
|
|
log.error(`Could not check out tag ${version}. ${e}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const childArgs = parseArguments(args);
|
|
|
|
await executeChildProcess(childArgs);
|
|
|
|
if (version) {
|
|
log.ok('PACKING SIGNATURES');
|
|
await checkoutHead();
|
|
|
|
await runTasks([signature, zip], {
|
|
version,
|
|
platforms: {
|
|
[PLATFORM.FIREFOX_MV2]: true,
|
|
},
|
|
debug: false,
|
|
watch: false,
|
|
log: false,
|
|
test: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
run();
|