Updates
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# bare-os-bare-libs
|
||||
|
||||
Builds trusted IIFE bundles from [`bare-module-manifest.json`](../bare-os-booter/lib/bare-module-manifest.json) entries marked `"bundle": true`. Output:
|
||||
Builds trusted IIFE bundles from [`bare-module-manifest.json`](../bare-os-booter/lib/bare-module-manifest.json) entries marked `"bundle": true`. **`npm run build`** runs **`prebuild`**: **`../../scripts/generate-bare-module-manifest-data.mjs`**, which refreshes **[`bare-module-manifest.data.mjs`](../bare-os-booter/lib/bare-module-manifest.data.mjs)** for Pear before esbuild. Output:
|
||||
|
||||
- `kernel/lib/bare/bundles/<ctxKey>.js`
|
||||
- `kernel/lib/bare/manifest.json` (paths for the booter drive merge)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"description": "Builds IIFE bundles under kernel/lib/bare/bundles for ctx.bare drive merge",
|
||||
"scripts": {
|
||||
"prebuild": "node ../../scripts/generate-bare-module-manifest-data.mjs",
|
||||
"build": "node build.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -33,7 +33,7 @@ node index.js
|
||||
- **`bare-os-http-policy.js`** — Optional HTTP allow/deny lists for wrapped `ctx.httpFetch`
|
||||
- **`bare-os-ctx-api.js` / `bare-os-ctx.d.ts`** — Semantic version of the `ctx` contract + optional TypeScript shapes
|
||||
- **`bare-os-runtime-caps.js`** — Frozen caps: pipeline limits, `quotas`, pseudo paths, `features` flags
|
||||
- **`bare-os-ctx-bare.js` / `bare-module-manifest.json`** — **`ctx.bare`** host imports + optional **`/lib/bare/`** drive bundle merge
|
||||
- **`bare-os-ctx-bare.js` / `bare-module-manifest.json` / `bare-module-manifest.data.mjs`** — **`ctx.bare`** host imports + optional **`/lib/bare/`** drive bundle merge; Pear loads the manifest from generated **`bare-module-manifest.data.mjs`** (see [PEAR-RUN.md](../../PEAR-RUN.md), [Developer guide ch.12](../../developer-guide/12-bare-modules-and-pear-ecosystem.md))
|
||||
- **`identity-session.js` / `identity-account.js`** — Guest vs unlocked user, `/.bare/account`, vault
|
||||
- **`hdms-manager.js`** — Extra Hyperdrives, mounts under `/mnt`, Autopass pair/invite
|
||||
- **`bare-initd.js`** — Service registry, unit drop-ins, **`units.d`** fragments, socket-activation, **`IdleSec=`** idle stop, **`ConditionPathExists=`** / **`AssertPathExists=`**, **ReadinessPath** / **`exec:`** polling, DAG snapshot **`/proc/bare_os/initd_graph.json`**, mobile suspend/resume order, `startBareInitd`, **kernel-logger**
|
||||
@@ -66,11 +66,12 @@ npm test -w bare-os-booter
|
||||
```
|
||||
|
||||
- `brittle-bare test.identity.js` — crypto account codec (Bare)
|
||||
- `brittle-node test.js` — Hyperdrive, VFS, shell, kernel runner, cron matchers, etc.
|
||||
- `brittle-node test.js` — Hyperdrive, VFS, shell, kernel runner, cron matchers, etc.; includes parity of **`bare-module-manifest.json`** vs **`bare-module-manifest.data.mjs`**
|
||||
- Root **`npm run verify:manifest-data`** — ensures JSON and **`.data.mjs`** stay in sync (runs in **`pretest`**)
|
||||
|
||||
## Pear staging
|
||||
|
||||
`pear.stage` includes hoisted `../../node_modules`. Run **`scripts/ensure-pear-node-modules.mjs`** from the repo root before `pear run` so the app package sees symlinked deps (see root README).
|
||||
`pear.stage` includes hoisted `../../node_modules`. Run **`scripts/ensure-pear-node-modules.mjs`** from the repo root before `pear run` so the app package sees symlinked deps (see root README). After changing **`bare-module-manifest.json`** or running **`sync:bare-manifest`**, confirm **`npm run verify:manifest-data`** passes, then **`pear stage` / `pear release`** so Pear ships an up-to-date **`bare-module-manifest.data.mjs`**.
|
||||
|
||||
## See also
|
||||
|
||||
|
||||
@@ -6689,6 +6689,10 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
async bareOsVfsBatchWrite(puts) {
|
||||
await bareOsVfsBatchPut(disk.drive, puts)
|
||||
if (!vfs || !Array.isArray(puts)) return
|
||||
/* Drive path `lib/bare/bare-module-manifest.json` updates warm-cache bundle rows.
|
||||
* Pear hosts load `ctx.bare` from embedded `bare-module-manifest.data.mjs` (booter
|
||||
* release); changing only the drive copy does not alter the Pear module graph until a
|
||||
* new booter is staged. */
|
||||
let hasManifestPut = false
|
||||
/** @type {Uint8Array | ArrayBuffer | ArrayBufferView | null | undefined} */
|
||||
let manifestBuf = null
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,10 @@
|
||||
* plus optional merge from trusted IIFE bundles on the system drive.
|
||||
*/
|
||||
|
||||
import { readFileSync, statSync } from '#host-fs'
|
||||
import { dirname, join, resolve } from '#host-path'
|
||||
import { readFileSync } from '#host-fs'
|
||||
import { dirname, join } from '#host-path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import bareModuleManifestEmbedded from './bare-module-manifest.data.mjs'
|
||||
import { bareOsHostBooterWarn } from './bare-os-host-booter-log.js'
|
||||
|
||||
/**
|
||||
@@ -61,117 +62,54 @@ export const BARE_OS_STDLIB_GLOBAL = '__bare_os_stdlib__'
|
||||
|
||||
const MANIFEST_NAME = 'bare-module-manifest.json'
|
||||
|
||||
/** @param {string} root */
|
||||
function libDirIfManifestAtRoot(root) {
|
||||
if (!root || root === '/' || root === '') return null
|
||||
const tries = [
|
||||
join(root, 'lib', MANIFEST_NAME),
|
||||
join(root, 'packages', 'bare-os-booter', 'lib', MANIFEST_NAME)
|
||||
]
|
||||
for (const f of tries) {
|
||||
try {
|
||||
if (statSync(f).isFile()) return dirname(f)
|
||||
} catch {
|
||||
/* continue */
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* `pear://app/lib/foo.js` → try **`join(mount, 'lib')`** for each bundle mount (manifest must exist).
|
||||
* @param {string} href
|
||||
* @param {string[]} mounts
|
||||
* @param {unknown} m
|
||||
* @returns {typeof bareModuleManifestEmbedded}
|
||||
*/
|
||||
function pearUrlLibDir(href, mounts) {
|
||||
if (!href.startsWith('pear:')) return null
|
||||
let u
|
||||
try {
|
||||
u = new URL(href)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
const segs = u.pathname.split('/').filter(Boolean)
|
||||
if (segs.length < 2) return null
|
||||
segs.pop()
|
||||
const rel = segs.join('/')
|
||||
if (!rel) return null
|
||||
for (const m of mounts) {
|
||||
if (!m || m === '/') continue
|
||||
const candidate = resolve(join(String(m), rel))
|
||||
try {
|
||||
if (statSync(join(candidate, MANIFEST_NAME)).isFile()) return candidate
|
||||
} catch {
|
||||
/* continue */
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory containing `bare-module-manifest.json`. Resolved lazily so Pear **`cwd`** / mounts are valid.
|
||||
* Never fall back to **`/lib`** (happens when **`join('/', 'lib')`** after a bogus **`/`** candidate).
|
||||
*/
|
||||
function resolveBareCtxLibDir() {
|
||||
const href = String(import.meta.url)
|
||||
if (href.startsWith('file:')) {
|
||||
return dirname(fileURLToPath(href))
|
||||
}
|
||||
|
||||
const rti = globalThis.Pear?.constructor?.RTI?.mount
|
||||
const swap = globalThis.Pear?.config?.swapDir
|
||||
const cwdRaw =
|
||||
typeof globalThis.process?.cwd === 'function'
|
||||
? globalThis.process.cwd()
|
||||
: ''
|
||||
const cwd = cwdRaw ? resolve(String(cwdRaw)) : resolve('.')
|
||||
|
||||
let d = cwd
|
||||
for (let i = 0; i < 48; i++) {
|
||||
const found = libDirIfManifestAtRoot(d)
|
||||
if (found) return found
|
||||
const parent = dirname(d)
|
||||
if (parent === d) break
|
||||
d = parent
|
||||
}
|
||||
|
||||
const mounts = []
|
||||
const seenM = new Set()
|
||||
for (const c of [rti, swap].filter(Boolean)) {
|
||||
const p = resolve(String(c))
|
||||
if (p === '/' || !p || seenM.has(p)) continue
|
||||
seenM.add(p)
|
||||
mounts.push(p)
|
||||
}
|
||||
const fromPear = pearUrlLibDir(href, mounts)
|
||||
if (fromPear) return fromPear
|
||||
|
||||
const seen = new Set()
|
||||
for (const c of [cwd, ...mounts].filter(Boolean)) {
|
||||
const p = resolve(String(c))
|
||||
if (p === '/' || !p || seen.has(p)) continue
|
||||
seen.add(p)
|
||||
const found = libDirIfManifestAtRoot(p)
|
||||
if (found) return found
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`[bare-os-booter] Cannot find ${MANIFEST_NAME}. ` +
|
||||
`cwd=${JSON.stringify(cwdRaw)} pearMount=${JSON.stringify(rti ?? null)} import.meta.url=${JSON.stringify(href.slice(0, 120))}`
|
||||
function cloneBareModuleManifest(m) {
|
||||
return /** @type {typeof bareModuleManifestEmbedded} */ (
|
||||
JSON.parse(JSON.stringify(m))
|
||||
)
|
||||
}
|
||||
|
||||
/** @type {{ version: number, entries: Array<{ ctxKey: string, package: string, export?: string, bundle?: boolean, optional?: boolean, tier?: string }> }} */
|
||||
/** @type {{ version: number, entries: Array<{ ctxKey: string, package: string, export?: string, bundle?: boolean, optional?: boolean, tier?: string }> } | null} */
|
||||
let _cachedManifest = null
|
||||
|
||||
/**
|
||||
* Embedded copy of **`bare-module-manifest.json`** for Pear (`pear://` **import.meta.url**).
|
||||
* `bare-fs` coerces **`URL`** arguments through **`bare-url.fileURLToPath`**, which rejects
|
||||
* non-`file:` schemes, so **`readFileSync(new URL('pear:…'))`** cannot load the JSON from the
|
||||
* bundle. Static import of **`bare-module-manifest.data.mjs`** (regenerated by
|
||||
* **`npm run sync:bare-manifest`**) keeps the manifest in the Pear module graph.
|
||||
*
|
||||
* On **`file:`** trees, the JSON on disk is preferred so edits apply without regenerating `.data.mjs`.
|
||||
*/
|
||||
export function loadBareModuleManifest() {
|
||||
if (_cachedManifest) return _cachedManifest
|
||||
const libDir = resolveBareCtxLibDir()
|
||||
const raw = readFileSync(join(libDir, MANIFEST_NAME), 'utf8')
|
||||
_cachedManifest = JSON.parse(raw)
|
||||
const href = String(import.meta.url)
|
||||
if (href.startsWith('pear:')) {
|
||||
_cachedManifest = cloneBareModuleManifest(bareModuleManifestEmbedded)
|
||||
return _cachedManifest
|
||||
}
|
||||
if (href.startsWith('file:')) {
|
||||
try {
|
||||
const libDir = dirname(fileURLToPath(href))
|
||||
const raw = readFileSync(join(libDir, MANIFEST_NAME), 'utf8')
|
||||
_cachedManifest = JSON.parse(raw)
|
||||
} catch {
|
||||
_cachedManifest = cloneBareModuleManifest(bareModuleManifestEmbedded)
|
||||
}
|
||||
return _cachedManifest
|
||||
}
|
||||
_cachedManifest = cloneBareModuleManifest(bareModuleManifestEmbedded)
|
||||
return _cachedManifest
|
||||
}
|
||||
|
||||
/** Exported for tests: default export object from **`bare-module-manifest.data.mjs`**. */
|
||||
export function bareOsBareModuleManifestEmbeddedRef() {
|
||||
return bareModuleManifestEmbedded
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, string>} shellEnv
|
||||
*/
|
||||
@@ -351,8 +289,9 @@ export async function maybeMergeBareFromDrive(shellEnv, vfs, target) {
|
||||
const sym = BARE_OS_STDLIB_GLOBAL
|
||||
g[sym] = g[sym] && typeof g[sym] === 'object' ? g[sym] : {}
|
||||
|
||||
const concRaw = String(shellEnv?.BARE_OS_BARE_STDLIB_RESOLVE_CONCURRENCY ?? '')
|
||||
.trim()
|
||||
const concRaw = String(
|
||||
shellEnv?.BARE_OS_BARE_STDLIB_RESOLVE_CONCURRENCY ?? ''
|
||||
).trim()
|
||||
const concParsed = Number.parseInt(concRaw, 10)
|
||||
const readConc =
|
||||
Number.isFinite(concParsed) && concParsed >= 1
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/**
|
||||
* Session identity: guest vs unlocked keypair; env + vfs; vault save.
|
||||
* User-facing **`ctx.console.log`** lines on register / unlock / logout / vault save are intentional session feedback (not debug).
|
||||
* @see [Handbook — Identity and vault](../../../handbook/05-identity-vault-and-hdms.md)
|
||||
* @see [Developer guide — Context object](../../../developer-guide/02-the-context-object.md)
|
||||
*/
|
||||
|
||||
import b4a from 'b4a'
|
||||
@@ -325,8 +328,8 @@ export async function unlockIdentity(ctx, passphrase) {
|
||||
const buf = await ctx.personalDrive.get(ACCOUNT_PATH)
|
||||
if (!buf) {
|
||||
const e = new Error('No account — use: login --new <passphrase>')
|
||||
/** @type {Error & { code?: string }} */ (e).code =
|
||||
'BARE_OS_IDENTITY_NO_ACCOUNT'
|
||||
/** @type {Error & { code?: string }} */
|
||||
e.code = 'BARE_OS_IDENTITY_NO_ACCOUNT'
|
||||
throw e
|
||||
}
|
||||
let publicKey
|
||||
@@ -337,9 +340,9 @@ export async function unlockIdentity(ctx, passphrase) {
|
||||
const e = new Error(
|
||||
'Passphrase does not unlock this account (wrong passphrase or unreadable account file).'
|
||||
)
|
||||
const ee = /** @type {Error & { code?: string, cause?: unknown }} */ (e)
|
||||
ee.code = 'BARE_OS_IDENTITY_PASSPHRASE_REJECTED'
|
||||
ee.cause = err
|
||||
/** @type {Error & { code?: string, cause?: unknown }} */
|
||||
e.code = 'BARE_OS_IDENTITY_PASSPHRASE_REJECTED'
|
||||
e.cause = err
|
||||
throw e
|
||||
}
|
||||
wipeSecret(ctx)
|
||||
|
||||
@@ -4,8 +4,14 @@ import { fileURLToPath } from 'url'
|
||||
import os from 'bare-os'
|
||||
|
||||
function cwd() {
|
||||
if (typeof globalThis.process?.cwd === 'function')
|
||||
return globalThis.process.cwd()
|
||||
if (typeof globalThis.process?.cwd === 'function') {
|
||||
try {
|
||||
const p = globalThis.process.cwd()
|
||||
if (p) return p
|
||||
} catch {
|
||||
/* Pear/Bare sometimes exposes cwd() but returns "" */
|
||||
}
|
||||
}
|
||||
return os.cwd()
|
||||
}
|
||||
|
||||
@@ -36,9 +42,11 @@ export function packageRootDir(metaUrl) {
|
||||
}
|
||||
const rti = globalThis.Pear?.constructor?.RTI?.mount
|
||||
const swap = globalThis.Pear?.config?.swapDir
|
||||
const pearAppDir = globalThis.Pear?.app?.dir ?? globalThis.Pear?.config?.dir
|
||||
const rtiDir = globalThis.Pear?.constructor?.RTI?.dir
|
||||
const seen = new Set()
|
||||
const candidates = []
|
||||
for (const c of [rti, swap, cwd()].filter(Boolean)) {
|
||||
for (const c of [rti, swap, pearAppDir, rtiDir, cwd()].filter(Boolean)) {
|
||||
const p = path.resolve(String(c))
|
||||
if (!seen.has(p)) {
|
||||
seen.add(p)
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"pear": {
|
||||
"name": "bare-os-booter",
|
||||
"stage": {
|
||||
"includes": [
|
||||
"include": [
|
||||
"../../node_modules"
|
||||
],
|
||||
"ignore": [
|
||||
|
||||
@@ -60,6 +60,7 @@ import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
|
||||
import { buildBareOsRuntimeCaps } from './lib/bare-os-runtime-caps.js'
|
||||
import {
|
||||
bareOsBareModulesEnabled,
|
||||
bareOsBareModuleManifestEmbeddedRef,
|
||||
buildBareCtxObjectFromHost,
|
||||
loadBareModuleManifest,
|
||||
maybeMergeBareFromDrive
|
||||
@@ -2711,6 +2712,29 @@ test('loadBareModuleManifest has entries', async (t) => {
|
||||
t.ok(m.entries.some((e) => e.ctxKey === 'b4a'))
|
||||
})
|
||||
|
||||
function stableStringifyManifest(obj) {
|
||||
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj)
|
||||
if (Array.isArray(obj)) {
|
||||
return '[' + obj.map((x) => stableStringifyManifest(x)).join(',') + ']'
|
||||
}
|
||||
const keys = Object.keys(obj).sort()
|
||||
return (
|
||||
'{' +
|
||||
keys
|
||||
.map((k) => JSON.stringify(k) + ':' + stableStringifyManifest(obj[k]))
|
||||
.join(',') +
|
||||
'}'
|
||||
)
|
||||
}
|
||||
|
||||
test('bare-module-manifest.data.mjs matches bare-module-manifest.json', async (t) => {
|
||||
const dir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const jsonPath = path.join(dir, 'lib', 'bare-module-manifest.json')
|
||||
const fromJson = JSON.parse(await readFile(jsonPath, 'utf8'))
|
||||
const embedded = bareOsBareModuleManifestEmbeddedRef()
|
||||
t.is(stableStringifyManifest(fromJson), stableStringifyManifest(embedded))
|
||||
})
|
||||
|
||||
test('buildBareCtxObjectFromHost loads core keys on Node', async (t) => {
|
||||
const target = {}
|
||||
await buildBareCtxObjectFromHost({}, target)
|
||||
|
||||
@@ -55,7 +55,11 @@ async function run(ctx, argv) {
|
||||
if (modeOpt != null) mkOpts.mode = modeOpt
|
||||
await ctx.vfs.mkdir(p, mkOpts)
|
||||
} catch (e) {
|
||||
ctx.console.error('mkdir: ' + p + ': ' + ((e && e.message) || e))
|
||||
const code =
|
||||
e && typeof e === 'object' && typeof e.code === 'string' ? e.code : ''
|
||||
const base = (e && e.message) || String(e)
|
||||
const tail = code ? `${code}: ${base}` : base
|
||||
ctx.console.error(`mkdir: ${p}: ${tail}`)
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import Hyperswarm from 'hyperswarm'
|
||||
import Protomux from 'protomux'
|
||||
import AppDrive from 'pear-appdrive'
|
||||
import b4a from 'b4a'
|
||||
import safetyCatch from 'safety-catch'
|
||||
import Hyperdrive from 'hyperdrive'
|
||||
import Corestore from 'corestore'
|
||||
import { readdir, readFile, stat } from '#host-fs-promises'
|
||||
import path from '#host-path'
|
||||
import { stat } from '#host-fs-promises'
|
||||
import {
|
||||
topicKey,
|
||||
buildMbr,
|
||||
@@ -18,7 +18,14 @@ import {
|
||||
defaultKernelRoot,
|
||||
defaultSeedCorestorePath
|
||||
} from './lib/paths.js'
|
||||
import { logPearMultisigKernelHint } from './lib/pear-multisig-hint.js'
|
||||
import {
|
||||
logPearMultisigKernelHint,
|
||||
logPearMultisigKernelHintFromPearAppDrive
|
||||
} from './lib/pear-multisig-hint.js'
|
||||
import {
|
||||
stageKernelTreeFromFs,
|
||||
stageKernelTreeFromPearAppDrive
|
||||
} from './lib/stage-kernel-tree.js'
|
||||
import { buildSeederSnapshotHintsJson } from './lib/build-seeder-snapshot-hints-json.js'
|
||||
import { seedLog } from './lib/host-logger.mjs'
|
||||
|
||||
@@ -28,55 +35,6 @@ function corestorePath() {
|
||||
return defaultSeedCorestorePath(_pkg, import.meta.url)
|
||||
}
|
||||
|
||||
/** @param {import('hyperdrive').default} drive */
|
||||
async function stageKernelTree(drive, kernelRoot) {
|
||||
/** @type {string[]} */
|
||||
const manifestPaths = []
|
||||
async function walk(rel) {
|
||||
const abs = path.join(kernelRoot, rel)
|
||||
const entries = await readdir(abs, { withFileTypes: true })
|
||||
for (const ent of entries) {
|
||||
const name = ent.name
|
||||
const subRel = rel ? path.join(rel, name) : name
|
||||
const subAbs = path.join(kernelRoot, subRel)
|
||||
if (ent.isDirectory()) {
|
||||
await walk(subRel)
|
||||
} else {
|
||||
const raw = await readFile(subAbs)
|
||||
let drivePath
|
||||
if (subRel === 'init.js') {
|
||||
drivePath = '/boot/init.js'
|
||||
} else if (subRel.startsWith('bin' + path.sep)) {
|
||||
drivePath = '/bin/' + subRel.slice(4).split(path.sep).join('/')
|
||||
} else if (subRel.startsWith('etc' + path.sep)) {
|
||||
drivePath = '/etc/' + subRel.slice(4).split(path.sep).join('/')
|
||||
} else {
|
||||
drivePath = '/' + subRel.split(path.sep).join('/')
|
||||
}
|
||||
if (drivePath === '/README.md') {
|
||||
seedLog(
|
||||
'info',
|
||||
'[skip] /README.md (kernel tree doc only; not installed on image root)'
|
||||
)
|
||||
continue
|
||||
}
|
||||
await drive.put(drivePath, b4a.from(raw))
|
||||
manifestPaths.push(drivePath)
|
||||
seedLog('info', `[+] ${drivePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const st = await stat(kernelRoot).catch(() => null)
|
||||
if (!st || !st.isDirectory()) {
|
||||
throw new Error('kernel directory missing: ' + kernelRoot)
|
||||
}
|
||||
seedLog('info', `Staging kernel from ${kernelRoot}`)
|
||||
await walk('')
|
||||
manifestPaths.sort()
|
||||
return manifestPaths
|
||||
}
|
||||
|
||||
async function maybeBuildCoreutilsFromSource() {
|
||||
if (!import.meta.url.startsWith('file:')) {
|
||||
seedLog(
|
||||
@@ -115,6 +73,7 @@ async function main() {
|
||||
await maybeBuildBareLibsFromSource()
|
||||
|
||||
const kernelRoot = defaultKernelRoot(_pkg, import.meta.url)
|
||||
const metaHref = String(import.meta.url)
|
||||
|
||||
const store = new Corestore(corestorePath())
|
||||
const drive = new Hyperdrive(store)
|
||||
@@ -123,8 +82,28 @@ async function main() {
|
||||
driveId: b4a.toString(drive.id, 'hex')
|
||||
})
|
||||
|
||||
const manifestPaths = await stageKernelTree(drive, kernelRoot)
|
||||
await logPearMultisigKernelHint(kernelRoot)
|
||||
const kernelOnDisk = await stat(kernelRoot).then(
|
||||
(s) => s.isDirectory(),
|
||||
() => false
|
||||
)
|
||||
|
||||
/** @type {string[]} */
|
||||
let manifestPaths
|
||||
if (kernelOnDisk) {
|
||||
manifestPaths = await stageKernelTreeFromFs(drive, kernelRoot)
|
||||
await logPearMultisigKernelHint(kernelRoot)
|
||||
} else if (!metaHref.startsWith('file:')) {
|
||||
const appDrive = new AppDrive()
|
||||
await appDrive.ready()
|
||||
try {
|
||||
manifestPaths = await stageKernelTreeFromPearAppDrive(appDrive, drive)
|
||||
await logPearMultisigKernelHintFromPearAppDrive(appDrive)
|
||||
} finally {
|
||||
await appDrive.close().catch(() => {})
|
||||
}
|
||||
} else {
|
||||
throw new Error('kernel directory missing: ' + kernelRoot)
|
||||
}
|
||||
|
||||
const localRAM = new Map()
|
||||
const mbr = buildMbr(drive.key)
|
||||
|
||||
@@ -144,7 +144,11 @@ async function run(ctx, argv) {
|
||||
if (modeOpt != null) mkOpts.mode = modeOpt
|
||||
await ctx.vfs.mkdir(p, mkOpts)
|
||||
} catch (e) {
|
||||
ctx.console.error('mkdir: ' + p + ': ' + ((e && e.message) || e))
|
||||
const code =
|
||||
e && typeof e === 'object' && typeof e.code === 'string' ? e.code : ''
|
||||
const base = (e && e.message) || String(e)
|
||||
const tail = code ? `${code}: ${base}` : base
|
||||
ctx.console.error(`mkdir: ${p}: ${tail}`)
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ Self-contained **`ctx.bare` support** on the **system** Hyperdrive: Holepunch **
|
||||
|
||||
## What gets staged
|
||||
|
||||
- **`bare-module-manifest.json`** — Copy of the booter manifest (same keys and packages as host resolution). Tells the runtime which logical module names exist.
|
||||
- **`bare-module-manifest.json`** — Copy of the booter manifest (same keys and packages as host resolution). Tells the runtime which logical module names exist on the **drive**. The Pear **booter** package also ships a generated sibling **`bare-module-manifest.data.mjs`** (repo: **`packages/bare-os-booter/lib/`**) used when **`import.meta.url`** is **`pear://…`** so **`ctx.bare`** host imports do not rely on **`readFile`** of **`pear:`** URLs; see [PEAR-RUN.md](../../../PEAR-RUN.md).
|
||||
- **`manifest.json`** — Drive loader index: **`bundles`** lists IIFE paths that assign into **`globalThis.__bare_os_stdlib__`**; **`bundleStats`** counts attempted bundles; **`bundleDiagnostics`** lists each bundle’s byte size (same data as **`docs/audit/bundle-health.json`**).
|
||||
- **`bundles/*.js`** — One esbuild IIFE per catalog entry. The bare-libs build is **fail-fast** (esbuild errors abort; no stub placeholders). Stale **`*.js`** left from older tiered builds are **pruned** on each successful build. Regenerate with **`npm run build -w bare-os-bare-libs`** (updates **`docs/audit/bundle-health.json`**).
|
||||
|
||||
@@ -52,8 +52,8 @@ The authoritative list is in the [environment appendix](../../../docs/reference/
|
||||
|
||||
## Regenerating bundles
|
||||
|
||||
1. Edit **`packages/bare-os-booter/lib/bare-module-manifest.json`** or bundle sources under **`packages/bare-os-bare-libs/`** as needed.
|
||||
2. Run **`npm run build -w bare-os-bare-libs`** — output lands in **`kernel/lib/bare/`** (and CI expects **`packages/bare-os-seeder/kernel/`** to match **`kernel/`** byte-for-byte afterward).
|
||||
1. Edit **`packages/bare-os-booter/lib/bare-module-manifest.json`** or bundle sources under **`packages/bare-os-bare-libs/`** as needed. After catalog merges, **`npm run sync:bare-manifest`** refreshes **`optionalDependencies`** and regenerates **`bare-module-manifest.data.mjs`** for Pear.
|
||||
2. Run **`npm run build -w bare-os-bare-libs`** — **`prebuild`** runs **`generate-bare-module-manifest-data.mjs`**; output lands in **`kernel/lib/bare/`** (and CI expects **`packages/bare-os-seeder/kernel/`** to match **`kernel/`** byte-for-byte afterward).
|
||||
3. Mirror **`kernel/`** into **`packages/bare-os-seeder/kernel/`** (same tree) so **`scripts/verify-kernel-seeder-parity.mjs`** passes — typically `rsync -a --delete kernel/ packages/bare-os-seeder/kernel/` from the repo root after init/bundle changes.
|
||||
4. Re-run the seeder so peers replicate the updated system drive.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"atMs": 1775373309737,
|
||||
"atMs": 1775376099873,
|
||||
"commands": [
|
||||
"arch",
|
||||
"awk",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
||||
import path from '#host-path'
|
||||
|
||||
/**
|
||||
* Map a path relative to the host `kernel/` tree to the POSIX path on the system image.
|
||||
* @param {string} subRel native-relative path (e.g. `bin/cat`, `init.js`)
|
||||
* @returns {string} image path starting with `/`
|
||||
*/
|
||||
export function kernelHostRelToImagePosix(subRel) {
|
||||
if (subRel === 'init.js') return '/boot/init.js'
|
||||
if (subRel.startsWith('bin' + path.sep)) {
|
||||
return '/bin/' + subRel.slice(4).split(path.sep).join('/')
|
||||
}
|
||||
if (subRel.startsWith('etc' + path.sep)) {
|
||||
return '/etc/' + subRel.slice(4).split(path.sep).join('/')
|
||||
}
|
||||
return '/' + subRel.split(path.sep).join('/')
|
||||
}
|
||||
|
||||
export function skipKernelImagePath(drivePath) {
|
||||
return drivePath === '/README.md'
|
||||
}
|
||||
|
||||
/**
|
||||
* `/kernel/bin/cat` → host-relative `bin/cat` (native separators).
|
||||
* @param {string} fullKey hyperdrive key
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export function bundleKernelKeyToHostRel(fullKey) {
|
||||
const k = String(fullKey).replace(/^\/+/, '')
|
||||
if (!k.startsWith('kernel/')) return null
|
||||
const rest = k.slice('kernel/'.length)
|
||||
if (!rest) return null
|
||||
return rest.split('/').join(path.sep)
|
||||
}
|
||||
@@ -4,8 +4,14 @@ import { fileURLToPath } from 'url'
|
||||
import os from 'bare-os'
|
||||
|
||||
function cwd() {
|
||||
if (typeof globalThis.process?.cwd === 'function')
|
||||
return globalThis.process.cwd()
|
||||
if (typeof globalThis.process?.cwd === 'function') {
|
||||
try {
|
||||
const p = globalThis.process.cwd()
|
||||
if (p) return p
|
||||
} catch {
|
||||
/* Pear/Bare sometimes exposes cwd() but returns "" */
|
||||
}
|
||||
}
|
||||
return os.cwd()
|
||||
}
|
||||
|
||||
@@ -40,9 +46,11 @@ export function packageRootDir(metaUrl) {
|
||||
}
|
||||
const rti = globalThis.Pear?.constructor?.RTI?.mount
|
||||
const swap = globalThis.Pear?.config?.swapDir
|
||||
const pearAppDir = globalThis.Pear?.app?.dir ?? globalThis.Pear?.config?.dir
|
||||
const rtiDir = globalThis.Pear?.constructor?.RTI?.dir
|
||||
const seen = new Set()
|
||||
const candidates = []
|
||||
for (const c of [rti, swap, cwd()].filter(Boolean)) {
|
||||
for (const c of [rti, swap, pearAppDir, rtiDir, cwd()].filter(Boolean)) {
|
||||
const p = path.resolve(String(c))
|
||||
if (!seen.has(p)) {
|
||||
seen.add(p)
|
||||
|
||||
@@ -101,3 +101,38 @@ export async function logPearMultisigKernelHint(kernelRoot) {
|
||||
console.warn('[seeder] pear.multisig.json:', e?.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same shape checks as {@link logPearMultisigKernelHint}, reading **`/kernel/pear.multisig.json`**
|
||||
* via Pear app drive IPC (**`pear-appdrive`**). Subprocess **`hyper-multisig verify`** is skipped.
|
||||
* @param {{ get: (key: string) => Promise<Uint8Array | Buffer | null> }} appDrive
|
||||
*/
|
||||
export async function logPearMultisigKernelHintFromPearAppDrive(appDrive) {
|
||||
const envOut = globalThis.process?.env
|
||||
let raw
|
||||
try {
|
||||
const buf = await appDrive.get('/kernel/pear.multisig.json')
|
||||
if (!buf) return
|
||||
raw = b4a.toString(buf, 'utf8')
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const j = JSON.parse(raw)
|
||||
if (!pearMultisigShapeOk(j)) {
|
||||
console.warn(
|
||||
'[seeder] pear.multisig.json: expected { signers: string[], quorum: number } with 1 ≤ quorum ≤ signers.length'
|
||||
)
|
||||
if (envOut) envOut.BARE_OS_SEEDER_MULTISIG_VERIFY_RESULT = 'skipped'
|
||||
return
|
||||
}
|
||||
const signers = j.signers
|
||||
const quorum = j.quorum
|
||||
console.log(
|
||||
`[seeder] pear.multisig.json OK (${signers.length} signers, quorum ${quorum}) [from Pear bundle]`
|
||||
)
|
||||
if (envOut) envOut.BARE_OS_SEEDER_MULTISIG_VERIFY_RESULT = 'skipped'
|
||||
} catch (e) {
|
||||
console.warn('[seeder] pear.multisig.json (bundle):', e?.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import path from '#host-path'
|
||||
import { readdir, readFile, stat } from '#host-fs-promises'
|
||||
import b4a from 'b4a'
|
||||
import {
|
||||
bundleKernelKeyToHostRel,
|
||||
kernelHostRelToImagePosix,
|
||||
skipKernelImagePath
|
||||
} from './kernel-layout.js'
|
||||
import { seedLog } from './host-logger.mjs'
|
||||
|
||||
/**
|
||||
* @param {import('hyperdrive').default} drive system image (writable)
|
||||
* @param {string} kernelRoot host directory
|
||||
*/
|
||||
export async function stageKernelTreeFromFs(drive, kernelRoot) {
|
||||
const st = await stat(kernelRoot).catch(() => null)
|
||||
if (!st || !st.isDirectory()) {
|
||||
throw new Error('kernel directory missing: ' + kernelRoot)
|
||||
}
|
||||
|
||||
seedLog('info', `Staging kernel from ${kernelRoot}`)
|
||||
/** @type {string[]} */
|
||||
const manifestPaths = []
|
||||
|
||||
async function walk(rel) {
|
||||
const abs = path.join(kernelRoot, rel)
|
||||
const entries = await readdir(abs, { withFileTypes: true })
|
||||
for (const ent of entries) {
|
||||
const name = ent.name
|
||||
const subRel = rel ? path.join(rel, name) : name
|
||||
const subAbs = path.join(kernelRoot, subRel)
|
||||
if (ent.isDirectory()) {
|
||||
await walk(subRel)
|
||||
} else {
|
||||
const raw = await readFile(subAbs)
|
||||
const drivePath = kernelHostRelToImagePosix(subRel)
|
||||
if (skipKernelImagePath(drivePath)) {
|
||||
seedLog(
|
||||
'info',
|
||||
'[skip] /README.md (kernel tree doc only; not installed on image root)'
|
||||
)
|
||||
continue
|
||||
}
|
||||
await drive.put(drivePath, b4a.from(raw))
|
||||
manifestPaths.push(drivePath)
|
||||
seedLog('info', `[+] ${drivePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk('')
|
||||
manifestPaths.sort()
|
||||
return manifestPaths
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy `/kernel/**` from the Pear app bundle onto the system image drive.
|
||||
* Uses **`pear-appdrive`** (sidecar IPC) — do not open the platform Corestore from the worker
|
||||
* (RocksDB lock conflict with the sidecar).
|
||||
*
|
||||
* @param {{ list: (key: string, opts?: object) => AsyncIterable<{ key?: string }>, get: (key: string) => Promise<Uint8Array | Buffer | null> }} appDrive
|
||||
* @param {import('hyperdrive').default} imageDrive system image (write)
|
||||
*/
|
||||
export async function stageKernelTreeFromPearAppDrive(appDrive, imageDrive) {
|
||||
const probe = await appDrive.get('/kernel/init.js')
|
||||
if (probe == null) {
|
||||
throw new Error(
|
||||
'Pear app drive has no /kernel/init.js (bundle missing kernel; re-stage with pear.stage.include listing kernel)'
|
||||
)
|
||||
}
|
||||
|
||||
seedLog('info', 'Staging kernel from Pear app drive via IPC (/kernel)')
|
||||
/** @type {string[]} */
|
||||
const manifestPaths = []
|
||||
|
||||
for await (const { key } of appDrive.list('/kernel')) {
|
||||
if (!key) continue
|
||||
const subRel = bundleKernelKeyToHostRel(key)
|
||||
if (!subRel) continue
|
||||
|
||||
const raw = await appDrive.get(key)
|
||||
if (raw == null) continue
|
||||
|
||||
const drivePath = kernelHostRelToImagePosix(subRel)
|
||||
if (skipKernelImagePath(drivePath)) {
|
||||
seedLog(
|
||||
'info',
|
||||
'[skip] /README.md (kernel tree doc only; not installed on image root)'
|
||||
)
|
||||
continue
|
||||
}
|
||||
await imageDrive.put(drivePath, b4a.from(raw))
|
||||
manifestPaths.push(drivePath)
|
||||
seedLog('info', `[+] ${drivePath}`)
|
||||
}
|
||||
|
||||
manifestPaths.sort()
|
||||
return manifestPaths
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
"corestore": "^7.2.1",
|
||||
"hyperdrive": "^13.3.2",
|
||||
"hyperswarm": "^4.17.0",
|
||||
"pear-appdrive": "^1.1.2",
|
||||
"protomux": "^3.10.1",
|
||||
"safety-catch": "^1.0.2"
|
||||
},
|
||||
@@ -30,8 +31,9 @@
|
||||
"pear": {
|
||||
"name": "bare-os-seeder",
|
||||
"stage": {
|
||||
"includes": [
|
||||
"../../node_modules"
|
||||
"include": [
|
||||
"../../node_modules",
|
||||
"kernel"
|
||||
],
|
||||
"ignore": [
|
||||
".git",
|
||||
|
||||
Reference in New Issue
Block a user