Files
workinf_Blender_Wasm/tools/web/check-binary-archive.mjs
mes123456 7c16b279ae
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M7 workflows and release operations
2026-08-15 17:43:53 -04:00

140 lines
7.7 KiB
JavaScript

import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const releaseRoot = path.join(repoRoot, "release");
const archive = path.resolve(process.env.M6_BINARY_ARCHIVE ?? path.join(releaseRoot, "blender-web-offline.tar.gz"));
const sourceArchive = path.resolve(process.env.M6_SOURCE_ARCHIVE ?? path.join(releaseRoot, "blender-web-corresponding-source.tar.gz"));
const sumsPath = path.join(releaseRoot, "SHA256SUMS.txt");
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "blender-binary-archive-"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
function walk(directory, base = directory) {
const files = [];
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) files.push(...walk(absolute, base));
else files.push(path.relative(base, absolute).replaceAll(path.sep, "/"));
}
return files;
}
function safeEntries(file) {
const verbose = execFileSync("tar", ["-tvzf", file], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
for (const line of verbose.split("\n").filter(Boolean)) {
assert.ok(["-", "d"].includes(line[0]), `binary archive contains a non-regular entry: ${line}`);
}
const entries = execFileSync("tar", ["-tzf", file], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }).split("\n").filter(Boolean);
assert.ok(entries.length > 0, "binary archive is empty");
for (const entry of entries) {
assert.ok(!entry.startsWith("/") && !entry.split("/").includes(".."), `unsafe binary archive path: ${entry}`);
assert.ok(entry === "blender-web-offline/" || entry.startsWith("blender-web-offline/"), `unexpected binary archive root: ${entry}`);
}
}
try {
assert.ok(fs.statSync(archive).isFile(), `binary archive is missing: ${archive}`);
assert.ok(fs.statSync(sourceArchive).isFile(), `source archive is missing: ${sourceArchive}`);
assert.ok(fs.statSync(sumsPath).isFile(), `release checksum file is missing: ${sumsPath}`);
execFileSync("sha256sum", ["-c", path.basename(sumsPath)], { cwd: releaseRoot, stdio: "pipe" });
safeEntries(archive);
execFileSync("tar", ["--no-same-owner", "--no-same-permissions", "-xzf", archive, "-C", workspace]);
const bundle = path.join(workspace, "blender-web-offline");
assert.ok(!fs.realpathSync(bundle).startsWith(`${repoRoot}${path.sep}`), "binary archive was not extracted independently");
const required = [
"COPYING",
"DEPLOYMENT.md",
"SOURCE_OFFER.txt",
"deployment-contract.json",
"manifest.json",
"KNOWN_LIMITATIONS.md",
"operations-diagnostics.json",
"parity-ledger.json",
"RELEASE_NOTES.md",
"RELEASE_RECOVERY.md",
"release-metadata.json",
"sbom.spdx.json",
"third-party-notices.json",
"V1_SCOPE.md",
"app/index.html",
"app/engine-manifest.json",
];
for (const relative of required) assert.ok(fs.statSync(path.join(bundle, relative)).isFile(), `binary archive omits ${relative}`);
const manifest = JSON.parse(fs.readFileSync(path.join(bundle, "manifest.json"), "utf8"));
assert.equal(manifest.schemaVersion, 1, "binary manifest schema drifted");
assert.ok(Array.isArray(manifest.files) && manifest.files.length > 0, "binary manifest has no files");
const actualFiles = walk(bundle).filter((relative) => relative !== "manifest.json");
assert.deepEqual(manifest.files.map((entry) => entry.path), actualFiles, "binary manifest file list drifted");
for (const entry of manifest.files) {
const file = path.join(bundle, entry.path);
assert.equal(fs.statSync(file).size, entry.bytes, `binary manifest byte length drifted: ${entry.path}`);
assert.equal(sha256(file), entry.sha256, `binary manifest SHA-256 drifted: ${entry.path}`);
}
const contract = JSON.parse(fs.readFileSync(path.join(bundle, "deployment-contract.json"), "utf8"));
assert.equal(contract.schemaVersion, 1);
assert.equal(contract.transport.production, "https");
assert.equal(contract.transport.localDevelopment, "http://127.0.0.1");
assert.equal(contract.transport.fileProtocolSupported, false);
assert.deepEqual(contract.methods, ["GET", "HEAD"]);
const sbom = JSON.parse(fs.readFileSync(path.join(bundle, "sbom.spdx.json"), "utf8"));
assert.equal(sbom.spdxVersion, "SPDX-2.3");
assert.ok(sbom.packages.length > 0, "binary SBOM has no packages");
const engine = JSON.parse(fs.readFileSync(path.join(bundle, "app/engine-manifest.json"), "utf8"));
assert.equal(engine.schemaVersion, 2);
assert.equal(new Set(engine.variants.map((variant) => variant.id)).size, 2);
for (const variant of engine.variants) {
for (const resource of Object.values(variant.resources)) {
const file = path.join(bundle, "app", new URL(resource.url, "http://archive.local").pathname.slice(1));
assert.equal(sha256(file), resource.sha256, `${variant.id} ${resource.url} hash drifted`);
}
}
const releaseMetadata = JSON.parse(fs.readFileSync(path.join(bundle, "release-metadata.json"), "utf8"));
assert.equal(releaseMetadata.schemaVersion, 1);
assert.equal(releaseMetadata.engineReleaseId, engine.releaseId);
assert.equal(releaseMetadata.storage.indexedDbSchemaVersion, 6);
assert.equal(releaseMetadata.storage.opfsProjectManifestSchemaVersion, 1);
assert.equal(releaseMetadata.storage.migrationDirection, "forward-only");
assert.equal(releaseMetadata.storage.originBound, true);
assert.ok(releaseMetadata.app.workerAssets.length >= 3);
for (const relative of [...releaseMetadata.app.workerAssets, ...releaseMetadata.app.entryAssets]) {
assert.ok(fs.statSync(path.join(bundle, "app", relative)).isFile(), `release metadata asset is missing: ${relative}`);
}
const diagnostics = JSON.parse(fs.readFileSync(path.join(bundle, "operations-diagnostics.json"), "utf8"));
assert.deepEqual(diagnostics.entries.map((entry) => entry.domain).sort(), ["gpu", "hash", "isolation", "mime", "quota", "range", "worker"]);
const releaseNotes = fs.readFileSync(path.join(bundle, "RELEASE_NOTES.md"), "utf8");
assert.match(releaseNotes, /All 12 V1 family release slices are `READY`/);
assert.match(releaseNotes, /all 12 complete Blender 5\.2 family parity states are\s+`BLOCKED`/);
assert.match(releaseNotes, /\[Known limitations\]\(KNOWN_LIMITATIONS\.md\)/);
assert.match(releaseNotes, /\[Release recovery\]\(RELEASE_RECOVERY\.md\)/);
assert.match(fs.readFileSync(path.join(bundle, "KNOWN_LIMITATIONS.md"), "utf8"), /32,768-page ceiling \(2 GiB\)/);
assert.match(fs.readFileSync(path.join(bundle, "RELEASE_RECOVERY.md"), "utf8"), /sha256sum --check RC_MANIFEST\.json\.sha256/);
const report = {
schemaVersion: 1,
task: "M6-11",
status: "READY",
archive: { path: path.basename(archive), bytes: fs.statSync(archive).size, sha256: sha256(archive) },
correspondingSource: { path: path.basename(sourceArchive), bytes: fs.statSync(sourceArchive).size, sha256: sha256(sourceArchive) },
extractedRootOutsideWorkspace: true,
filesVerified: manifest.files.length,
keyFiles: Object.fromEntries(required.map((relative) => [relative, sha256(path.join(bundle, relative))])),
};
const reportRoot = path.join(releaseRoot, "archive-reports");
fs.mkdirSync(reportRoot, { recursive: true });
fs.writeFileSync(path.join(reportRoot, "binary.json"), `${JSON.stringify(report, null, 2)}\n`);
process.stdout.write(`binary-archive-ok files=${report.filesVerified} sha256=${report.archive.sha256}\n`);
}
finally {
fs.rmSync(workspace, { recursive: true, force: true });
}