chore: adopt node-addon-landlock-run source as native/ subtree

Bring the node-addon-landlock-run tree (tag v0.0.1, commit 614f7fd) into
native/landlock-run as its source of record: launcher development happens
here, next to the harness consumers, and the standalone repository becomes
the release mirror the tree is exported to for packing and publishing
(procedure in native/README.md). The subtree keeps its own pnpm workspace
and lockfile and is NOT added to the harness workspace: harness installs,
gates, and CI never touch it. The mirror's .github/ stays out of the
subtree; a separate manually-dispatched workflow
(.github/workflows/landlock-run.yml) runs the subtree's CI legs — the
per-architecture native builds, real-kernel launcher proofs, and pack
rehearsal — adapted with working-directory/cache paths.

eslint ignores the subtree like vendor/; AGENTS.md gains the native/
layout line (+5 words on its budget ceiling).
This commit is contained in:
kingwl
2026-07-14 23:34:47 +08:00
parent 513a8d2177
commit 0a486f09c9
46 changed files with 2582 additions and 1 deletions

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env node
/**
* Assemble downloaded release artifacts into the platform packages and
* verify the result. The Release workflow's build legs upload one
* `prebuild-<package>` artifact per platform package (its `bin/` payload);
* this script copies each into `packages/<package>/bin/` and then checks
* every declared binary for presence and ELF architecture.
*
* Usage: `node scripts/assemble-prebuilds.mjs <artifact-root>`.
*/
import fs from 'node:fs';
import path from 'node:path';
import { platformDirs, root, verifyPlatformBinaries } from './repo.mjs';
const artifactRoot = path.resolve(process.argv[2] || '.release/prebuild-artifacts');
if (!fs.existsSync(artifactRoot)) {
throw new Error(`prebuild artifact directory does not exist: ${artifactRoot}`);
}
const platforms = platformDirs().map((dir) => path.basename(dir));
for (const name of platforms) {
const binDir = path.join(root, 'packages', name, 'bin');
fs.rmSync(binDir, { recursive: true, force: true });
fs.mkdirSync(binDir, { recursive: true });
}
for (const artifactName of fs.readdirSync(artifactRoot)) {
const artifactDir = path.join(artifactRoot, artifactName);
if (!fs.statSync(artifactDir).isDirectory()) continue;
const name = platforms.find((candidate) => artifactName === `prebuild-${candidate}`);
if (!name) {
throw new Error(`cannot map artifact to a platform package: ${artifactName}`);
}
for (const file of fs.readdirSync(artifactDir)) {
const source = path.join(artifactDir, file);
const destination = path.join(root, 'packages', name, 'bin', file);
fs.copyFileSync(source, destination);
fs.chmodSync(destination, 0o755);
console.log(`Copied ${path.relative(root, source)} -> ${path.relative(root, destination)}`);
}
}
for (const dir of platformDirs()) {
const { name, count } = verifyPlatformBinaries(path.join(root, dir));
console.log(`Verified ${name}: ${count} binaries`);
}

View File

@@ -0,0 +1,86 @@
/**
* Build every native tool this host can build, into its per-platform
* package.
*
* Targets are derived from the checked-in matrix: each
* `packages/<name>/prebuilds.json` whose `platform` matches this host names
* the binaries to produce; the TOOLS table below maps each `tool` to its C
* source. Builds are NATIVE-ONLY — each Linux architecture compiles its own
* binary with the distro's `musl-gcc` (static musl: runs on glibc and musl
* distros alike, no loader or libc expectations on the consumer host), and
* CI's per-arch runners are the builders of record. No cross toolchain
* exists here on purpose: native runners replace it, and the audit surface
* is the reviewed C source plus CI provenance.
*
* Binaries land in `packages/<name>/bin/` — git-ignored (root
* `.gitignore`), packed into the platform package's npm tarball behind its
* `prepack` gate (`scripts/verify-launcher-binary.mjs`).
*
* Run: `pnpm run build:native` (Linux with musl-gcc on PATH:
* `apt-get install musl-tools`). Non-Linux hosts fail fast — no platform
* package exists for them to build.
*/
import { spawnSync } from 'node:child_process'
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs'
import { basename, dirname, join, resolve } from 'node:path'
/** Each native tool's C source, keyed by the `tool` field in prebuilds.json. */
const TOOLS: Record<string, { source: string }> = {
'landlock-run': { source: 'packages/entry/src/main.c' },
}
const repoRoot = resolve(import.meta.dirname, '..')
if (process.platform !== 'linux') {
console.error(`build: native tools are built natively per Linux architecture (no cross toolchain) — nothing to build on ${process.platform}. CI's per-arch runners build and rehearse every platform package.`)
process.exit(1)
}
const hostPlatform = `linux-${process.arch}`
/** This host's platform packages, from the checked-in matrix. */
const targets: { packageDir: string; tool: string; binaryPath: string; kind: string }[] = []
const packagesRoot = join(repoRoot, 'packages')
for (const name of readdirSync(packagesRoot).sort()) {
const prebuildsFile = join(packagesRoot, name, 'prebuilds.json')
if (!existsSync(prebuildsFile)) continue
const prebuilds = JSON.parse(readFileSync(prebuildsFile, 'utf8')) as {
platform: string
binaries: { tool: string; kind: string; path: string }[]
}
if (prebuilds.platform !== hostPlatform) continue
for (const binary of prebuilds.binaries) {
targets.push({ packageDir: join(packagesRoot, name), tool: binary.tool, binaryPath: binary.path, kind: binary.kind })
}
}
if (targets.length === 0) {
console.error(`build: no platform package declares binaries for ${hostPlatform} — supported platforms are the packages/*/prebuilds.json "platform" values.`)
process.exit(1)
}
for (const target of targets) {
const tool = TOOLS[target.tool]
if (tool === undefined) {
console.error(`build: prebuilds.json names unknown tool "${target.tool}" — add it to the TOOLS table in scripts/build.ts.`)
process.exit(1)
}
if (target.kind !== 'static-musl') {
console.error(`build: unknown binary kind "${target.kind}" — the only toolchain here is static musl.`)
process.exit(1)
}
const binary = join(target.packageDir, target.binaryPath)
mkdirSync(dirname(binary), { recursive: true })
// -static against musl: self-contained, no loader/libc expectations on the
// consumer host. -Werror is safe to keep hard: CI pins the builder images,
// and a new warning on a toolchain bump deserves a look, not a pass.
const result = spawnSync('musl-gcc', [
'-std=c11', '-Os', '-Wall', '-Wextra', '-Werror', '-static', '-s',
'-o', binary, join(repoRoot, tool.source),
], { stdio: ['ignore', 'inherit', 'inherit'] })
if (result.error !== undefined || result.status !== 0) {
console.error('build: musl-gcc failed' +
(result.error ? ` (${result.error.message} — is musl-tools installed?)` : ''))
process.exit(1)
}
console.log(`build: built ${basename(target.packageDir)}/${target.binaryPath}`)
}

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Bump every package (workspace root + packages/*) to one version, refresh
* the lockfile, and verify. Usage: `pnpm release:bump <major|minor|patch|x.y.z>`.
*/
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { packageDirs, readJson, root } from './repo.mjs';
const bump = process.argv[2];
const releaseTypes = new Set(['major', 'minor', 'patch']);
function writeJson(file, value) {
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
}
function run(command, args) {
const result = spawnSync(command, args, {
cwd: root,
stdio: 'inherit',
env: { ...process.env, CI: 'true' },
});
if (result.error) throw result.error;
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
function packageFiles() {
return ['package.json', ...packageDirs().map((dir) => path.join(dir, 'package.json'))];
}
function parseVersion(version) {
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
if (!match) {
throw new Error(`increment types need a plain x.y.z current version (current: ${version}) — pass an explicit target version instead`);
}
return match.slice(1).map((part) => Number(part));
}
/** Explicit target versions accept full semver, prereleases included (test publishes). */
const EXPLICIT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/;
function nextVersion(current, release) {
if (EXPLICIT_VERSION.test(release)) return release;
if (!releaseTypes.has(release)) {
throw new Error('Usage: pnpm release:bump <major|minor|patch|x.y.z>');
}
const [major, minor, patch] = parseVersion(current);
if (release === 'major') return `${major + 1}.0.0`;
if (release === 'minor') return `${major}.${minor + 1}.0`;
return `${major}.${minor}.${patch + 1}`;
}
function currentPublishedVersion(files) {
const versions = new Set(
files
.filter((file) => file.startsWith('packages/'))
.map((file) => readJson(path.join(root, file)).version),
);
if (versions.size !== 1) {
throw new Error(`published package versions differ: ${[...versions].join(', ')}`);
}
return [...versions][0];
}
if (!bump) {
console.error('Usage: pnpm release:bump <major|minor|patch|x.y.z>');
process.exit(1);
}
const files = packageFiles();
const targetVersion = nextVersion(currentPublishedVersion(files), bump);
for (const file of files) {
const fullPath = path.join(root, file);
const json = readJson(fullPath);
json.version = targetVersion;
writeJson(fullPath, json);
console.log(`${file}: ${targetVersion}`);
}
run('pnpm', ['install', '--ignore-scripts', '--lockfile-only']);
run('node', ['./scripts/verify-release.mjs']);
console.log(`Release version bumped to ${targetVersion}`);

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env node
/**
* Bump, stage, and commit a release in one command:
* `pnpm release:commit <major|minor|patch|x.y.z>`. The tag stays manual —
* create it from the merged release commit.
*/
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { packageDirs, readJson, root } from './repo.mjs';
const bump = process.argv[2];
function run(command, args) {
const result = spawnSync(command, args, {
cwd: root,
stdio: 'inherit',
env: { ...process.env, CI: 'true' },
});
if (result.error) throw result.error;
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
if (!bump) {
console.error('Usage: pnpm release:commit <major|minor|patch|x.y.z>');
process.exit(1);
}
run('node', ['./scripts/bump-release.mjs', bump]);
const version = readJson(path.join(root, packageDirs()[0], 'package.json')).version;
run('git', [
'add',
'package.json',
'packages/*/package.json',
'pnpm-lock.yaml',
]);
run('git', ['commit', '-m', `release: ${version}`]);
console.log(`Committed release ${version}. Create the tag manually: git tag v${version}`);

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env node
/**
* Derive the GitHub Actions matrices from the checked-in package matrix
* (`packages/<name>/prebuilds.json`). Single source: adding a platform
* package extends CI and Release without editing a workflow.
*
* node scripts/github-matrix.mjs ci → one leg per distinct platform
* node scripts/github-matrix.mjs release-prebuild → one leg per platform package
*/
import path from 'node:path';
import { platformDirs, readJson, root } from './repo.mjs';
/** GitHub runner per prebuilds.json `platform` value — native builders only, no cross toolchain. */
const RUNNERS = {
'linux-x64': 'ubuntu-24.04',
'linux-arm64': 'ubuntu-24.04-arm',
};
function runnerFor(platform) {
const runner = RUNNERS[platform];
if (!runner) {
throw new Error(`missing GitHub runner for platform: ${platform}`);
}
return runner;
}
function platformManifests() {
return platformDirs().map((dir) => ({
dir,
name: path.basename(dir),
prebuilds: readJson(path.join(root, dir, 'prebuilds.json')),
}));
}
function ciMatrix() {
const platforms = [...new Set(platformManifests().map(({ prebuilds }) => prebuilds.platform))].sort();
return {
include: platforms.map((platform) => ({ platform, runner: runnerFor(platform) })),
};
}
function releasePrebuildMatrix() {
return {
include: platformManifests().map(({ dir, name, prebuilds }) => ({
platform: prebuilds.platform,
package: name,
dir,
runner: runnerFor(prebuilds.platform),
artifact: `prebuild-${name}`,
})),
};
}
const target = process.argv[2];
const matrices = {
ci: ciMatrix,
'release-prebuild': releasePrebuildMatrix,
};
if (!target || !matrices[target]) {
console.error(`Usage: node scripts/github-matrix.mjs <${Object.keys(matrices).join('|')}>`);
process.exit(1);
}
process.stdout.write(JSON.stringify(matrices[target]()));

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env node
/**
* Pack every published package into release tarballs, in publish order
* (platform packages first, then the entries that optionally depend on
* them), and write `publish-order.txt` next to them. `pnpm pack` produces
* the EXACT bytes `pnpm publish` would upload and runs each package's
* `prepack` gate, so a missing binary or unbuilt `lib/` refuses here.
*
* Usage: `node scripts/pack-release.mjs [dest] [--current-platform-only]`.
* The flag packs only THIS host's platform package plus the entries — for
* per-architecture CI legs, where the other architecture's binary does not
* exist (the exact refusal its prepack gate exists for).
*/
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { entryDirs, platformDirs, readJson, root } from './repo.mjs';
const args = process.argv.slice(2);
const currentPlatformOnly = args.includes('--current-platform-only');
const destination = path.resolve(args.find((arg) => !arg.startsWith('--')) || path.join(root, 'dist', 'npm'));
function hostPlatformDirs() {
const hostPlatform = `${process.platform}-${process.arch}`;
return platformDirs().filter((dir) => readJson(path.join(root, dir, 'prebuilds.json')).platform === hostPlatform);
}
function run(command, args) {
const result = spawnSync(command, args, {
cwd: root,
stdio: 'inherit',
});
if (result.error) throw result.error;
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
function tarballName(manifest) {
if (manifest.name.startsWith('@')) {
return `${manifest.name.slice(1).replace('/', '-')}-${manifest.version}.tgz`;
}
return `${manifest.name}-${manifest.version}.tgz`;
}
fs.rmSync(destination, { recursive: true, force: true });
fs.mkdirSync(destination, { recursive: true });
const dirs = [...(currentPlatformOnly ? hostPlatformDirs() : platformDirs()), ...entryDirs()];
const platformSet = new Set(platformDirs());
const publishOrder = [];
for (const dir of dirs) {
const manifest = readJson(path.join(root, dir, 'package.json'));
// Platform packages are packed with npm: pnpm pack (observed on 11.7.0)
// normalizes file modes and STRIPS the executable bit, which ships a
// launcher no consumer can spawn; npm pack preserves it. Platform packages
// have no dependencies by construction, so they need none of pnpm's
// workspace-protocol conversion — the entry packages do, and carry no
// executables, so they keep pnpm pack.
if (platformSet.has(dir)) {
run('npm', ['pack', `./${dir}`, '--pack-destination', destination]);
} else {
run('pnpm', ['--dir', dir, 'pack', '--pack-destination', destination]);
}
const tarball = tarballName(manifest);
const tarballPath = path.join(destination, tarball);
if (!fs.existsSync(tarballPath)) {
throw new Error(`expected pack output not found: ${tarballPath}`);
}
publishOrder.push(tarball);
}
fs.writeFileSync(path.join(destination, 'publish-order.txt'), `${publishOrder.join('\n')}\n`);
console.log(`Packed ${publishOrder.length} packages into ${path.relative(root, destination)}`);

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env node
/**
* Shared helpers for the repo scripts: package discovery, the checked-in
* prebuild matrix, and binary verification. The package matrix is explicit
* metadata — `packages/<name>/prebuilds.json` marks a platform package and
* declares its binaries; everything else under `packages/` is an entry
* package. Scripts derive from these files and never guess.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
export const root = fileURLToPath(new URL('..', import.meta.url));
export const packagesRoot = path.join(root, 'packages');
/** ELF `e_machine` (offset 18, little-endian) per platform-package `cpu` value. */
export const E_MACHINE = { x64: 62, arm64: 183 };
export function readJson(file) {
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
/** Platform packages: every `packages/<name>` carrying a `prebuilds.json`. */
export function platformDirs() {
return fs.readdirSync(packagesRoot)
.filter((name) => fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json')))
.sort()
.map((name) => path.join('packages', name));
}
/** Entry packages: every other `packages/<name>` with a `package.json`. */
export function entryDirs() {
return fs.readdirSync(packagesRoot)
.filter((name) => !fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json')))
.filter((name) => fs.existsSync(path.join(packagesRoot, name, 'package.json')))
.sort()
.map((name) => path.join('packages', name));
}
/** All published packages in publish order: platform packages before the entries that optionally depend on them. */
export function packageDirs() {
return [...platformDirs(), ...entryDirs()];
}
/**
* Verify one platform package's binaries against its `prebuilds.json`:
* every declared binary exists, nothing undeclared sits in `bin/`, and each
* file's ELF `e_machine` matches the package's declared `cpu`. Throws with
* a remediation message on the first mismatch.
*/
export function verifyPlatformBinaries(packageDir) {
const manifest = readJson(path.join(packageDir, 'package.json'));
const prebuilds = readJson(path.join(packageDir, 'prebuilds.json'));
const cpu = manifest.cpu?.[0];
if (cpu === undefined || !(cpu in E_MACHINE)) {
throw new Error(`${manifest.name}: unsupported or missing "cpu" in package.json (expected one of: ${Object.keys(E_MACHINE).join(', ')})`);
}
for (const binary of prebuilds.binaries) {
const file = path.join(packageDir, binary.path);
if (!fs.existsSync(file)) {
throw new Error(`${manifest.name}: missing ${binary.path} — run \`pnpm build:native\` on a ${prebuilds.platform} host (or assemble release artifacts) before packing.`);
}
try {
fs.accessSync(file, fs.constants.X_OK);
} catch {
// Only reachable when the mode was mangled somewhere between build and
// here (e.g. an archive step that normalized permissions) — the build
// itself always produces 755.
throw new Error(`${manifest.name}: ${binary.path} is not executable — a pack/extract step stripped the mode bit.`);
}
const machine = fs.readFileSync(file).readUInt16LE(18);
if (machine !== E_MACHINE[cpu]) {
throw new Error(`${manifest.name}: ${binary.path} has ELF e_machine ${machine}, expected ${E_MACHINE[cpu]} for ${cpu} — the binary was built for a different architecture.`);
}
}
const declared = prebuilds.binaries.map((binary) => path.basename(binary.path)).sort();
const binDir = path.join(packageDir, 'bin');
const actual = fs.existsSync(binDir) ? fs.readdirSync(binDir).sort() : [];
const extra = actual.filter((name) => !declared.includes(name));
if (extra.length) {
throw new Error(`${manifest.name}: bin/ contains files not declared in prebuilds.json: ${extra.join(', ')}`);
}
return { name: manifest.name, count: prebuilds.binaries.length };
}

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env node
/**
* Prepack gate for entry packages: refuse to pack a tarball whose built
* `lib/` is missing. Entry `files` lists use globs, and a glob matching
* nothing packs a silently JS-less tarball instead of failing — this gate
* turns that into a loud refusal on a checkout that never ran
* `pnpm build:ts`.
*
* Runs from each entry package's `prepack` hook (pnpm sets the script cwd
* to the package directory).
*/
import fs from 'node:fs';
import path from 'node:path';
const packageDir = process.cwd();
const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
for (const file of ['lib/index.js', 'lib/index.d.ts']) {
if (!fs.existsSync(path.join(packageDir, file))) {
console.error(`verify-entry-lib: ${manifest.name} has no ${file} — run \`pnpm build:ts\` before packing.`);
process.exit(1);
}
}
console.log(`verify-entry-lib: ${manifest.name} built lib/ present.`);

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env node
/**
* Prepack gate for platform packages: refuse to pack a tarball whose
* declared binaries are missing or built for the wrong architecture.
*
* Without it, `pnpm pack` on a checkout that never ran
* `pnpm run build:native` would ship an EMPTY platform package — the
* binary's absence surfacing only at runtime as a failed probe on every
* consumer — and a binary copied across packages would advertise an
* architecture it cannot execute. The check is presence + ELF `e_machine`
* against the package's declared `cpu`; byte provenance is
* `verify-packed-install.mjs`'s concern (it pins the installed tarball
* against the workspace build).
*
* Runs from each platform package's `prepack` hook (pnpm sets the script
* cwd to the package directory). Also callable directly with an explicit
* package directory: `node scripts/verify-launcher-binary.mjs packages/<name>`.
*/
import path from 'node:path';
import { root, verifyPlatformBinaries } from './repo.mjs';
const packageDir = process.argv[2] ? path.resolve(root, process.argv[2]) : process.cwd();
try {
const { name, count } = verifyPlatformBinaries(packageDir);
console.log(`verify-launcher-binary: ${name}${count} binaries present with the right ELF architecture.`);
} catch (error) {
console.error(`verify-launcher-binary: ${error instanceof Error ? error.message : error}`);
process.exit(1);
}

View File

@@ -0,0 +1,223 @@
#!/usr/bin/env node
/**
* Publish-path rehearsal without publishing: verify the packed tarballs are
* exactly what a consumer install needs. `pnpm pack` already produced the
* bytes `pnpm publish` would upload; this script checks the payload
* (coverage, concrete dependency versions, NO lifecycle install scripts —
* this family has no install fallback on purpose), unpacks the entry plus
* THIS host's platform tarball into a throwaway consumer OUTSIDE the repo,
* byte-pins the installed binary against the workspace build it was packed
* from, and drives the INSTALLED entry under plain `node` — resolution,
* probe, and a real confinement world-proof through the installed launcher.
*
* On non-Linux hosts (no platform package exists) it instead proves the
* documented degradation: resolution falls back to a nonexistent path and
* the probe reports `unusable`.
*
* Usage: `node scripts/verify-packed-install.mjs [tarball-dir] [--current-platform-only]`.
* The flag skips the all-platforms tarball-presence check for
* per-architecture CI legs. `NALR_REQUIRE_LANDLOCK=1` makes an unenforcing
* kernel a failure instead of a skipped world-proof (set on CI, where the
* kernel is known).
*/
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { entryDirs, packageDirs, platformDirs, readJson, root } from './repo.mjs';
const args = process.argv.slice(2);
const currentPlatformOnly = args.includes('--current-platform-only');
const tarballDir = path.resolve(args.find((arg) => !arg.startsWith('--')) || path.join(root, 'dist', 'npm'));
const entryPackageName = 'node-addon-landlock-run';
function tarballName(manifest) {
if (manifest.name.startsWith('@')) {
return `${manifest.name.slice(1).replace('/', '-')}-${manifest.version}.tgz`;
}
return `${manifest.name}-${manifest.version}.tgz`;
}
function tarballPath(manifest) {
const tarball = path.join(tarballDir, tarballName(manifest));
if (!fs.existsSync(tarball)) {
throw new Error(`missing packed tarball: ${tarball}`);
}
return tarball;
}
function run(command, commandArgs, options = {}) {
const result = spawnSync(command, commandArgs, {
cwd: options.cwd || root,
stdio: 'inherit',
env: { ...process.env, ...options.env },
});
if (result.error) throw result.error;
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
function runCapture(command, commandArgs) {
const result = spawnSync(command, commandArgs, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
if (result.error) throw result.error;
if (result.status !== 0) {
process.stderr.write(result.stderr);
process.exit(result.status ?? 1);
}
return result.stdout;
}
function readPackedManifest(manifest) {
return JSON.parse(runCapture('tar', ['-xOf', tarballPath(manifest), 'package/package.json']));
}
function verifyPackedManifest(packed) {
const lifecycle = ['preinstall', 'install', 'postinstall', 'prepare'];
for (const script of lifecycle) {
if (packed.scripts?.[script]) {
throw new Error(`${packed.name}: packed manifest carries a "${script}" lifecycle script — this family has no install fallback`);
}
}
for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
for (const [name, version] of Object.entries(packed[field] ?? {})) {
if (version.includes('workspace:')) {
throw new Error(`${packed.name}: packed ${field} still uses the workspace protocol: ${name}@${version}`);
}
}
}
}
function sha256(file) {
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
}
function packageInstallDir(packageName) {
return path.join(tempRoot, 'node_modules', ...packageName.split('/'));
}
function unpackTarball(manifest) {
const extractRoot = fs.mkdtempSync(path.join(tempRoot, 'extract-'));
run('tar', ['-xzf', tarballPath(manifest), '-C', extractRoot]);
const source = path.join(extractRoot, 'package');
const destination = packageInstallDir(manifest.name);
fs.rmSync(destination, { recursive: true, force: true });
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.renameSync(source, destination);
fs.rmSync(extractRoot, { recursive: true, force: true });
console.log(`Unpacked ${manifest.name} -> ${path.relative(tempRoot, destination)}`);
}
const manifests = packageDirs().map((dir) => ({ dir, manifest: readJson(path.join(root, dir, 'package.json')) }));
const entryManifest = manifests.find(({ manifest }) => manifest.name === entryPackageName)?.manifest;
if (!entryManifest) throw new Error(`missing source manifest for ${entryPackageName}`);
const hostPlatform = `${process.platform}-${process.arch}`;
const currentPlatformEntry = manifests.find(
({ dir, manifest }) => platformDirs().includes(dir) && manifest.name === `${entryPackageName}-${hostPlatform}`,
);
// Payload checks: every expected tarball exists (full mode), the packed
// entry's optional-dependency set names exactly the platform packages, and
// no packed manifest carries workspace versions or install lifecycle.
const expectedTarballs = currentPlatformOnly
? manifests.filter(({ dir }) => entryDirs().includes(dir) || dir === currentPlatformEntry?.dir)
: manifests;
for (const { manifest } of expectedTarballs) {
tarballPath(manifest);
}
const packedEntry = readPackedManifest(entryManifest);
const platformPackageNames = manifests
.filter(({ dir }) => platformDirs().includes(dir))
.map(({ manifest }) => manifest.name)
.sort();
const optionalNames = Object.keys(packedEntry.optionalDependencies || {}).sort();
if (optionalNames.join('\n') !== platformPackageNames.join('\n')) {
throw new Error(`packed entry optionalDependencies mismatch\nactual:\n${optionalNames.join('\n')}\nexpected:\n${platformPackageNames.join('\n')}`);
}
for (const { manifest } of expectedTarballs) {
verifyPackedManifest(readPackedManifest(manifest));
}
// Throwaway ESM consumer, built from local tarballs only — no registry.
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-packed-install-'));
fs.writeFileSync(
path.join(tempRoot, 'package.json'),
`${JSON.stringify({ name: 'nalr-packed-install-check', version: '0.0.0', private: true, type: 'module' }, null, 2)}\n`,
);
console.log(`Verifying packed install in ${tempRoot}`);
unpackTarball(entryManifest);
if (currentPlatformEntry) {
unpackTarball(currentPlatformEntry.manifest);
// Byte-pin: the installed binary must be the workspace build it was packed
// from — any divergence means the tarball did not carry the built bytes.
const prebuilds = readJson(path.join(root, currentPlatformEntry.dir, 'prebuilds.json'));
for (const binary of prebuilds.binaries) {
const workspaceFile = path.join(root, currentPlatformEntry.dir, binary.path);
const installedFile = path.join(packageInstallDir(currentPlatformEntry.manifest.name), binary.path);
if (sha256(workspaceFile) !== sha256(installedFile)) {
throw new Error(`installed ${binary.path} differs from the workspace build it was packed from`);
}
console.log(`Byte-pinned ${binary.path} against the workspace build`);
}
} else if (process.platform === 'linux') {
throw new Error(`linux host without a platform package in the matrix: ${hostPlatform}`);
}
// Drive the INSTALLED entry under plain node: resolution, probe, and (on an
// enforcing kernel) a real confinement world-proof through the installed
// launcher.
const driver = path.join(tempRoot, 'driver.mjs');
fs.writeFileSync(driver, `
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { grantArgs, launcherPath, probe } from 'node-addon-landlock-run';
const requireLandlock = process.env.NALR_REQUIRE_LANDLOCK === '1';
const platformPackage = 'node-addon-landlock-run-' + process.platform + '-' + process.arch;
const resolved = launcherPath();
assert.ok(path.isAbsolute(resolved), 'launcherPath must be absolute');
assert.ok(resolved.includes(path.join(...platformPackage.split('/'))), 'launcherPath must point into the platform package: ' + resolved);
if (process.platform === 'linux') {
assert.ok(fs.existsSync(resolved), 'installed launcher missing at ' + resolved);
try {
fs.accessSync(resolved, fs.constants.X_OK);
} catch {
throw new Error('installed launcher is not executable — the pack path stripped the mode bit: ' + resolved);
}
const enforcement = probe(resolved);
console.log('probe through the installed launcher: ' + enforcement);
if (enforcement === 'unusable') {
if (requireLandlock) throw new Error('NALR_REQUIRE_LANDLOCK=1 but the probe reports unusable');
console.log('kernel does not enforce Landlock — skipping the confinement world-proof');
} else {
const work = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-confine-'));
const denied = path.join(work, 'denied.txt');
const deniedRun = spawnSync(resolved, [...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', 'echo x > ' + denied], { encoding: 'utf8' });
assert.notEqual(deniedRun.status, 0, 'write outside the grants must fail');
assert.ok(!fs.existsSync(denied), 'denied write must not land on disk');
const granted = path.join(work, 'granted.txt');
const grantedRun = spawnSync(resolved, [...grantArgs({ readOnly: ['/'], readWrite: [work] }), '--', '/bin/sh', '-c', 'echo ok > ' + granted], { encoding: 'utf8' });
assert.equal(grantedRun.status, 0, 'granted write must succeed: ' + grantedRun.stderr);
assert.equal(fs.readFileSync(granted, 'utf8').trim(), 'ok');
console.log('confinement world-proof passed through the installed launcher');
}
} else {
assert.ok(!fs.existsSync(resolved), 'no platform package exists for this host — the fallback path must not exist');
assert.equal(probe(resolved), 'unusable');
console.log('non-linux host: fallback resolution and unusable probe verified');
}
`);
run(process.execPath, [driver], { cwd: tempRoot });
console.log('Packed install verification passed.');

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env node
/**
* Release verification. Always: every published package carries one shared
* version, and — when running from a tag or publishing — the `vX.Y.Z` tag
* matches it. With `--prebuilds`: every platform package's declared
* binaries exist with the right ELF architecture (run after
* `assemble-prebuilds.mjs` or a local `build:native`).
*/
import path from 'node:path';
import { packageDirs, platformDirs, readJson, root, verifyPlatformBinaries } from './repo.mjs';
function verifyVersions() {
const packages = packageDirs().map((dir) => ({
dir,
manifest: readJson(path.join(root, dir, 'package.json')),
}));
const versions = new Set(packages.map((pkg) => pkg.manifest.version));
if (versions.size !== 1) {
throw new Error([
'published package versions must match:',
...packages.map((pkg) => `${pkg.dir}: ${pkg.manifest.version}`),
].join('\n'));
}
const version = packages[0].manifest.version;
const ref = process.env.GITHUB_REF || '';
const publish = process.env.RELEASE_PUBLISH === 'true';
if (publish && !ref.startsWith('refs/tags/v')) {
throw new Error('publishing requires running the workflow from a v* tag');
}
if (ref.startsWith('refs/tags/v')) {
const tagVersion = ref.slice('refs/tags/v'.length);
if (tagVersion !== version) {
throw new Error(`tag/version mismatch: tag v${tagVersion}, packages ${version}`);
}
}
console.log(`Verified release version ${version}`);
}
function verifyPrebuilds() {
for (const dir of platformDirs()) {
const { name, count } = verifyPlatformBinaries(path.join(root, dir));
console.log(`Verified ${name}: ${count} binaries`);
}
}
verifyVersions();
if (process.argv.includes('--prebuilds')) {
verifyPrebuilds();
}