Files

262 lines
10 KiB
JavaScript

import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import {
accessSync,
closeSync,
constants,
fstatSync,
ftruncateSync,
openSync,
readFileSync,
writeFileSync,
writeSync,
} from "node:fs";
import { constants as osConstants } from "node:os";
import path from "node:path";
import { createInterface } from "node:readline";
import { fileURLToPath } from "node:url";
const MAX_PACKAGES = 100_000;
const CACHE_SEEDS = new Set(["seed_used", "seed_unavailable", "seed_lockfile_mismatch", "decision_unavailable", "not_applicable"]);
const STORE_STATES = new Set(["created", "seeded", "reused", "unavailable"]);
// The caller may use operational status only for
// initial-setup fallback; an established project always retains pnpm. Broad
// fetch failures can include TLS/auth errors and stay fatal.
const OPERATIONAL_FAILURE_CODES = new Set([
"ERR_PNPM_UNEXPECTED_STORE",
"ERR_PNPM_UNEXPECTED_VIRTUAL_STORE",
"ERR_PNPM_OUTDATED_LOCKFILE",
"ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE",
"ERR_PNPM_NO_LOCKFILE",
"ERR_PNPM_FETCH_408",
"ERR_PNPM_FETCH_429",
"ERR_PNPM_FETCH_500",
"ERR_PNPM_FETCH_502",
"ERR_PNPM_FETCH_503",
"ERR_PNPM_FETCH_504",
]);
// These are pnpm-reported package IDs, not an assertion about network bytes or
// image-cache provenance. Do not interpret an installed-tree no-op as 100% reuse.
export class InstallProgress {
constructor(project) {
this.project = path.resolve(project);
this.reused = new Set();
this.downloaded = new Set();
this.complete = false;
this.added = undefined;
this.invalid = false;
}
accept(event) {
if (event?.name === "pnpm:stats" && event.prefix === this.project &&
Number.isSafeInteger(event.added) && event.added >= 0) this.added = event.added;
if (event?.name === "pnpm:stage" && event.stage === "importing_done" &&
event.prefix === this.project) this.complete = true;
if (event?.name !== "pnpm:progress" || event.requester !== this.project ||
!["fetched", "found_in_store"].includes(event.status)) return;
if (typeof event.packageId !== "string" || !event.packageId || event.packageId.length > 4096 ||
this.reused.size + this.downloaded.size >= MAX_PACKAGES) {
this.invalid = true;
return;
}
if (event.status === "fetched") {
this.reused.delete(event.packageId);
this.downloaded.add(event.packageId);
} else if (!this.downloaded.has(event.packageId)) {
this.reused.add(event.packageId);
}
}
counts(success) {
if (!success || !this.complete || !(this.added > 0) || this.invalid ||
this.reused.size + this.downloaded.size === 0) return {};
return { packages_reused: this.reused.size, packages_downloaded: this.downloaded.size };
}
}
function openReport() {
const report = process.env.SITES_INSTALL_REPORT_PATH;
if (!report || !constants.O_NOFOLLOW) return undefined;
let fd;
try {
fd = openSync(report, constants.O_WRONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
if (fstatSync(fd).isFile()) return fd;
} catch {}
if (fd !== undefined) closeSync(fd);
return undefined;
}
function writeReport(fd, report) {
if (fd === undefined) return;
try {
const body = `${JSON.stringify(report)}\n`;
writeSync(fd, body, 0, "utf8");
ftruncateSync(fd, Buffer.byteLength(body));
} catch {
// Optional telemetry must not change installation behavior or expose its path.
}
}
function showLine(line, progress, failure) {
let event;
try {
event = JSON.parse(line);
} catch {
process.stdout.write(`${line}\n`);
return;
}
progress.accept(event);
if (event?.level === "error" && failure.code !== 65) {
failure.code = event.name === "pnpm" && OPERATIONAL_FAILURE_CODES.has(event.err?.code)
? 70 : 65;
}
const message = event?.message ?? event?.err?.message;
if (event?.name === "pnpm:lifecycle" && typeof event.line === "string") {
process.stdout.write(`${event.line}\n`);
} else if (typeof message === "string" && ["warn", "error", "info"].includes(event.level)) {
const output = event.level === "error" ? process.stderr : process.stdout;
output.write(`${message}\n`);
}
}
async function openLock(filename, waitSeconds) {
let fd;
try {
if (!filename || !constants.O_NOFOLLOW || !constants.O_NONBLOCK) throw new Error("Unsupported lock");
fd = openSync(filename, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600);
if (!fstatSync(fd).isFile()) throw new Error("Expected a regular lock file");
const child = spawn("flock", ["-w", waitSeconds, "3"], { stdio: ["ignore", "ignore", "ignore", fd] });
const code = await new Promise((resolve) => {
child.once("error", () => resolve(undefined));
child.once("close", resolve);
});
if (code === 0) return { status: "locked", fd };
closeSync(fd);
return { status: code === undefined ? "unavailable" : "busy" };
} catch {
if (fd !== undefined) closeSync(fd);
return { status: "unavailable" };
}
}
async function holdInstallLocks(projectLock, sharedLock, waitSeconds) {
// The shell holds both leases through stdin. EOF (including caller exit)
// releases the same open file descriptions that flock locked in its child.
const requests = createInterface({ input: process.stdin, crlfDelay: Infinity });
const project = await openLock(projectLock, "0");
let shared;
try {
process.stdout.write(`${project.status}\n`);
for await (const request of requests) {
if (request === "shared" && project.fd !== undefined) {
shared ??= await openLock(sharedLock, waitSeconds);
process.stdout.write(`${shared.status}\n`);
} else if (request === "release-shared") {
if (shared?.fd !== undefined) closeSync(shared.fd);
shared = undefined;
process.stdout.write("released\n");
} else {
process.stdout.write("unavailable\n");
}
}
} finally {
if (shared?.fd !== undefined) closeSync(shared.fd);
if (project.fd !== undefined) closeSync(project.fd);
requests.close();
}
}
async function main() {
const argsIn = process.argv.slice(2);
if (argsIn[0] === "--hold-install-locks") {
await holdInstallLocks(argsIn[1], argsIn[2], argsIn[3]);
return;
}
if (argsIn[0] === "--report-store") {
const [, cacheSeed, storeScope, storeState, stdout] = argsIn;
if (!CACHE_SEEDS.has(cacheSeed) || !["project", "workspace", "unknown"].includes(storeScope) ||
!STORE_STATES.has(storeState)) { process.exitCode = 64; return; }
const report = { version: 1, cache_seed: cacheSeed, store_scope: storeScope, store_state: storeState };
const fd = openReport();
writeReport(fd, report);
if (fd !== undefined) closeSync(fd);
if (stdout === "1") process.stdout.write(`${JSON.stringify(report)}\n`);
return;
}
const [cacheSeed, storeScope, storeState, store, executable, ...prefix] = argsIn;
if (!CACHE_SEEDS.has(cacheSeed) || !["project", "workspace"].includes(storeScope) ||
!STORE_STATES.has(storeState) || storeState === "unavailable" ||
!store || !path.isAbsolute(store) || !executable) {
process.stderr.write("Invalid pnpm installation arguments.\n");
process.exitCode = 64;
return;
}
const fd = openReport();
const report = { version: 1, cache_seed: cacheSeed, store_scope: storeScope, store_state: storeState };
writeReport(fd, report);
const progress = new InstallProgress(process.cwd());
const failure = {};
const env = { ...process.env };
delete env.SITES_INSTALL_REPORT_PATH;
const args = [...prefix, "install", "--prod=false", "--ignore-scripts=false",
"--frozen-lockfile", "--prefer-offline", "--store-dir", store,
"--cache-dir", path.join(store, "policy-cache"), "--fetch-retries=0",
"--fetch-timeout=30000", "--network-concurrency=1", "--reporter=ndjson",
"--package-import-method=clone-or-copy"];
let result = { code: 1, signal: null };
let receivedSignal;
try {
const child = spawn(executable, args, { env, stdio: ["inherit", "pipe", "inherit"] });
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
lines.on("line", (line) => showLine(line, progress, failure));
// timeout/exec owns the inherited group. Relaying would deliver signals twice.
const signalHandlers = ["SIGINT", "SIGHUP", "SIGTERM"].map((signal) => {
const handler = () => { receivedSignal ??= signal; };
process.on(signal, handler);
return [signal, handler];
});
let spawnError;
child.once("error", (error) => { spawnError = error; });
result = await new Promise((resolve) => child.once("close", (code, signal) => {
resolve({ code: spawnError ? (spawnError.code === "ENOENT" ? 127 : 1) : code, signal });
}));
for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
lines.close();
result.signal ??= receivedSignal;
if (spawnError) {
process.stderr.write("Unable to start pnpm.\n");
result.code = spawnError.code === "ENOENT" ? 127 : 65;
} else if (result.code !== 0 && !result.signal) {
result.code = failure.code ?? 65;
}
if (result.code === 0 && !result.signal) {
accessSync("node_modules/.bin/vinext", constants.X_OK);
const lock = readFileSync("pnpm-lock.yaml");
writeFileSync("node_modules/.sites-install.json", `${JSON.stringify({
package_manager: "pnpm@11.25.0",
lockfile_sha256: createHash("sha256").update(lock).digest("hex"),
node: process.version,
platform: `${process.platform}-${process.arch}`,
}, null, 2)}\n`);
}
} catch {
process.stderr.write("Dependency setup did not produce a usable Vinext installation.\n");
result = { code: 65, signal: receivedSignal ?? null };
} finally {
Object.assign(report, progress.counts(result.code === 0 && !result.signal));
writeReport(fd, report);
if (fd !== undefined) closeSync(fd);
}
if (report.packages_reused !== undefined) {
process.stdout.write(`[sites] pnpm reused ${report.packages_reused} packages and downloaded ${report.packages_downloaded}\n`);
}
if (result.code === 0 && !result.signal) process.stdout.write("[sites] dependency setup passed\n");
process.exitCode = result.signal ? 128 + (osConstants.signals[result.signal] ?? 0) : result.code ?? 1;
if (result.signal) {
try { process.kill(process.pid, result.signal); } catch {}
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();