/** * Temp Corestore + Hyperdrive + stock createVfs / runBinCommand — same path as * scripts/probe-kernel-bin.mjs, extended for run-bin (buffers, stubs, fixtures). */ import b4a from 'b4a' import Corestore from 'corestore' import Hyperdrive from 'hyperdrive' import os from 'node:os' import path from 'node:path' import { mkdtemp, readdir, readFile, rm, stat } from 'node:fs/promises' import { createBareOsIpc } from '../../packages/bare-os-booter/lib/bare-os-ipc.js' import { runBinCommand } from '../../packages/bare-os-booter/lib/kernel-runner.js' import { createVfs } from '../../packages/bare-os-booter/lib/vfs.js' import { BARE_OS_CTX_API_VERSION } from '../../packages/bare-os-booter/lib/bare-os-ctx-api.js' const textDecoder = new TextDecoder() /** * Backing path on the personal drive for logical `/home//…` (see `vfs.js` + `test.js`). * @param {string} logicalPath e.g. `/home/user/note.txt` * @returns {string | null} null when not under `/home/…` */ function personalDrivePathForHome(logicalPath) { const p = String(logicalPath || '').replace(/\\/g, '/') if (!p.startsWith('/home/')) return null return '/.bare-os/home/' + p.slice('/home/'.length) } const DEFAULT_SYSTEM_FILES = { '/etc/passwd': 'root:x:0:0:root:/root:/bin/sh\nuser:x:1000:1000:user:/home/user:/bin/sh\n', '/etc/os-release': 'NAME="Bare OS"\nID=bare-os\nVERSION="run-bin"\nPRETTY_NAME="Bare OS (run-bin harness)"\n' } const DEFAULT_HOME_FILES = { '/home/user/.profile': '# run-bin harness\n', '/home/user/note.txt': 'hello from fixture layout\n' } /** * @param {string} repoRoot * @param {{ * timeoutMs?: number, * cwd?: string, * identitySession?: 'guest' | 'unlocked' * }} [opts] */ export async function createRunBinHarness(repoRoot, opts = {}) { const timeoutMs = Number.parseInt(process.env.BARE_OS_RUNBIN_DEFAULT_TIMEOUT_MS || '', 10) || opts.timeoutMs || 120_000 const cwd = opts.cwd ?? '/' const identitySession = opts.identitySession ?? 'guest' const tmp = await mkdtemp(path.join(os.tmpdir(), 'bare-os-run-bin-')) const store = new Corestore(tmp) const drive = new Hyperdrive(store) const personal = new Hyperdrive(store.namespace('run-bin-personal')) await drive.ready() await personal.ready() const env = { HOME: '/home/user', PATH: '/bin:/usr/bin', USER: 'user', LOGNAME: 'user', SHELL: '/bin/sh', UID: '1000', GID: '1000', PWD: cwd, BARE_OS_EXIT_STATUS: '0', BARE_OS_RUNBIN_DEFAULT_TIMEOUT_MS: String(timeoutMs), BARE_OS_DELEGATE_ALLOW: 'none', BARE_OS_DELEGATE_AUDIT_ONLY: '1', BARE_OS_AUDIT: '1', TERM: 'dumb', LANG: 'C.UTF-8' } const bareOsIpc = createBareOsIpc() const vfs = createVfs(drive, personal, env, null, { bareOsIpc, bareOsIdentityVfsRef: { session: identitySession } }) /** @type {string[]} */ const logs = [] /** @type {string[]} */ const errs = [] /** @type {string[]} */ const binWriteChunks = [] function resetBuffers() { logs.length = 0 errs.length = 0 binWriteChunks.length = 0 } /** @type {Record} */ const ctx = { drive, personalDrive: personal, vfs, bareOsIpc, env, b4a, bareOsCtxApiVersion: BARE_OS_CTX_API_VERSION, platform: process.platform, shellStdin: '', exitCode: 0, console: { log: (...a) => logs.push(a.join(' ')), error: (...a) => errs.push(a.join(' ')) }, bareOsBinWrite: (chunk) => { const u8 = typeof chunk === 'string' ? b4a.from(chunk) : chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk) binWriteChunks.push(textDecoder.decode(u8)) }, bareOsDiagnosticsSubscribe(_cb) { return () => {} }, async bareOsHrpcRequest() { throw new Error( 'run-bin: bare_os RPC unavailable (no swarm — use full booter for HRPC)' ) }, async bareOsRunCurlCli() { errs.push('run-bin: curl delegate disabled (stub — no network)') this.exitCode = 126 }, async bareOsRunWgetCli() { errs.push('run-bin: wget delegate disabled (stub — no network)') this.exitCode = 126 }, async bareOsRunHolesailCli() { errs.push('run-bin: holesail delegate disabled (stub — no swarm)') this.exitCode = 126 }, async bareOsRunOpensslCli() { errs.push( 'run-bin: openssl delegate disabled (stub — use host openssl outside run-bin)' ) this.exitCode = 126 } } ctx.runBinCommand = function runSelf(argv, runOpts) { return runBinCommand(this, argv, runOpts) } const kernelBinDir = path.join(repoRoot, 'kernel', 'bin') async function loadKernelBinNames() { const ents = await readdir(kernelBinDir, { withFileTypes: true }) return ents .filter((ent) => ent.isFile()) .map((ent) => ent.name) .sort((a, b) => a.localeCompare(b)) } async function seedKernelBins() { const names = await loadKernelBinNames() for (const name of names) { const src = await readFile(path.join(kernelBinDir, name), 'utf8') await drive.put('/bin/' + name, b4a.from(src)) } return names } async function putPath(logicalPath, body) { const u8 = typeof body === 'string' ? b4a.from(body) : body const pp = personalDrivePathForHome(logicalPath) if (pp) await personal.put(pp, u8) else await drive.put(logicalPath, u8) } async function seedLayout() { for (const [p, body] of Object.entries(DEFAULT_SYSTEM_FILES)) { await drive.put(p, b4a.from(body)) } for (const [p, body] of Object.entries(DEFAULT_HOME_FILES)) { await putPath(p, body) } } /** * Overlay files from test/fixtures/coreutils/overlays// as /-rooted paths. * @param {string} fixtureName */ async function applyFixture(fixtureName) { if (!fixtureName) return const root = path.join( repoRoot, 'test/fixtures/coreutils/overlays', fixtureName ) let st try { st = await stat(root) } catch { throw new Error(`run-bin: fixture overlay not found: ${root}`) } if (!st.isDirectory()) { throw new Error(`run-bin: fixture overlay is not a directory: ${root}`) } async function walkDir(absDir) { const ents = await readdir(absDir, { withFileTypes: true }) for (const ent of ents) { const full = path.join(absDir, ent.name) if (ent.isDirectory()) { await walkDir(full) } else { const rel = path.relative(root, full) const vfsPath = '/' + rel.split(path.sep).join('/') const buf = await readFile(full) await putPath(vfsPath, buf) } } } await walkDir(root) } /** * @param {{ skip?: boolean }} [o] */ async function vfsSnapshot(o = {}) { if (o.skip === true) return undefined /** @type {Record} */ const out = {} const roots = ['/', '/bin', '/etc', '/home/user', '/proc', '/sys', '/dev'] for (const r of roots) { try { const names = await vfs.readdir(r) out[r] = Array.isArray(names) ? names.slice().sort((a, b) => a.localeCompare(b)) : [] } catch { out[r] = [] } } return out } function combinedStdout() { const text = logs.join('\n') const raw = binWriteChunks.join('') if (!raw) return text return text ? text + '\n' + raw : raw } async function cleanup() { await personal.close() await drive.close() await store.close() await rm(tmp, { recursive: true, force: true }) } /** * @param {string[]} argv * @param {{ timeoutMs?: number }} [runOpts] */ async function run(argv, runOpts) { resetBuffers() ctx.exitCode = 0 ctx.shellStdin = typeof runOpts?.shellStdin === 'string' ? runOpts.shellStdin : '' const t0 = performance.now() let thrown = null try { await runBinCommand(ctx, argv, { timeoutMs: runOpts?.timeoutMs ?? timeoutMs }) } catch (e) { thrown = e instanceof Error ? e : new Error(String(e)) } const durationMs = performance.now() - t0 return { exitCode: ctx.exitCode ?? 0, stdout: combinedStdout(), stderr: errs.join('\n'), durationMs, thrown } } return { repoRoot, tmp, store, drive, personal, vfs, ctx, kernelBinDir, resetBuffers, loadKernelBinNames, seedKernelBins, seedLayout, applyFixture, vfsSnapshot, cleanup, run, timeoutMs } }