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 }); }