#!/usr/bin/env node import { execFile as execFileCallback, spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { promisify } from "node:util"; const catalog = JSON.parse(readFileSync(new URL("../distribution/node-runtime.json", import.meta.url), "utf8")); const expected = new Map(Object.values(catalog.platforms).map((value) => [value.file, value.sha256])); const execFile = promisify(execFileCallback); const curlAvailable = spawnSync("curl", ["--version"], { stdio: "ignore" }).status === 0; const nullDevice = process.platform === "win32" ? "NUL" : "/dev/null"; const results = await Promise.all(catalog.sourceBases.map(checkSource)); for (const result of results) { console.log(`${result.ok ? "OK" : "FAIL"} ${result.host}: ${result.message}`); } const official = results.find((result) => result.host === "nodejs.org"); const domestic = results.filter((result) => result.host !== "nodejs.org"); if (!official?.ok) throw new Error("Node 官方精确版本源不可用或校验清单不匹配"); if (!domestic.some((result) => result.ok)) throw new Error("没有可用且校验一致的国内 Node 下载源"); async function checkSource(sourceBase) { const base = new URL(sourceBase); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 20_000); timeout.unref(); try { const checksums = parseChecksums(await requestText(new URL("SHASUMS256.txt", base), controller.signal)); for (const [file, sha256] of expected) { if (checksums.get(file) !== sha256) return failure(base, `${file} 的官方 SHA-256 不匹配或缺失`); } await Promise.all([...expected.keys()].map((file) => assertAvailable(new URL(file, base), controller.signal))); return { host: base.host, ok: true, message: `${expected.size} 个三平台制品可用且哈希一致` }; } catch (error) { return failure(base, error instanceof Error ? error.message : String(error)); } finally { clearTimeout(timeout); } } async function requestText(url, signal) { if (!curlAvailable) { const response = await fetch(url, { signal }); if (!response.ok) throw new Error(`${url.pathname} HTTP ${response.status}`); return response.text(); } const { stdout } = await execFile("curl", [ "--fail", "--silent", "--show-error", "--location", "--connect-timeout", "10", "--max-time", "20", url.toString(), ], { encoding: "utf8", maxBuffer: 2 * 1024 * 1024, signal }); return stdout; } async function assertAvailable(url, signal) { if (!curlAvailable) { const response = await fetch(url, { method: "HEAD", signal }); if (!response.ok) throw new Error(`${url.pathname} HTTP ${response.status}`); return; } await execFile("curl", [ "--fail", "--silent", "--show-error", "--location", "--head", "--connect-timeout", "10", "--max-time", "20", "--output", nullDevice, url.toString(), ], { encoding: "utf8", maxBuffer: 256 * 1024, signal }); } function parseChecksums(body) { const values = new Map(); for (const line of body.split(/\r?\n/)) { const match = /^([a-f0-9]{64})\s+\*?(.+)$/.exec(line.trim()); if (match) values.set(match[2], match[1]); } return values; } function failure(base, message) { return { host: base.host, ok: false, message }; }