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:
51
native/landlock-run/scripts/assemble-prebuilds.mjs
Normal file
51
native/landlock-run/scripts/assemble-prebuilds.mjs
Normal 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`);
|
||||
}
|
||||
86
native/landlock-run/scripts/build.ts
Normal file
86
native/landlock-run/scripts/build.ts
Normal 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}`)
|
||||
}
|
||||
90
native/landlock-run/scripts/bump-release.mjs
Normal file
90
native/landlock-run/scripts/bump-release.mjs
Normal 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}`);
|
||||
42
native/landlock-run/scripts/commit-release.mjs
Normal file
42
native/landlock-run/scripts/commit-release.mjs
Normal 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}`);
|
||||
66
native/landlock-run/scripts/github-matrix.mjs
Normal file
66
native/landlock-run/scripts/github-matrix.mjs
Normal 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]()));
|
||||
76
native/landlock-run/scripts/pack-release.mjs
Normal file
76
native/landlock-run/scripts/pack-release.mjs
Normal 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)}`);
|
||||
88
native/landlock-run/scripts/repo.mjs
Normal file
88
native/landlock-run/scripts/repo.mjs
Normal 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 };
|
||||
}
|
||||
25
native/landlock-run/scripts/verify-entry-lib.mjs
Normal file
25
native/landlock-run/scripts/verify-entry-lib.mjs
Normal 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.`);
|
||||
31
native/landlock-run/scripts/verify-launcher-binary.mjs
Normal file
31
native/landlock-run/scripts/verify-launcher-binary.mjs
Normal 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);
|
||||
}
|
||||
223
native/landlock-run/scripts/verify-packed-install.mjs
Normal file
223
native/landlock-run/scripts/verify-packed-install.mjs
Normal 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.');
|
||||
52
native/landlock-run/scripts/verify-release.mjs
Normal file
52
native/landlock-run/scripts/verify-release.mjs
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user