This commit is contained in:
Raven Scott
2026-04-03 19:49:21 -04:00
parent 76dc3ecb7c
commit 4dfee8398b
19 changed files with 504 additions and 250 deletions
@@ -2,4 +2,4 @@
* Semantic version of the booter `ctx` contract for custom kernels.
* Bump when adding/removing/renaming documented `ctx` fields or changing behavior.
*/
export const BARE_OS_CTX_API_VERSION = '1.2.0'
export const BARE_OS_CTX_API_VERSION = '1.3.0'
@@ -67,7 +67,10 @@ export function buildBareOsRuntimeCaps(shellEnv) {
simulatedPipelines: true,
bareInitd: true,
vfsTwoDrive: true,
identityVault: true
identityVault: true,
httpDelegate: true,
gitDelegate: true,
systemctlDelegate: true
})
})
}
+183 -31
View File
@@ -115,6 +115,113 @@ test('runKernelFromSource invokes start(ctx)', async (t) => {
t.is(calls[0], 'ok')
})
test('stock kernel init.js onboot runs multiple BARE_OS_ONBOOT lines when SKIP_REPL', async (t) => {
const src = await readFile(
path.join(__dirname, '../../kernel/init.js'),
'utf8'
)
const execLines = []
const drive = {
async get() {
return null
},
async *readdir() {
/* empty */
}
}
const ctx = {
bareOsSkipRepl: true,
env: { BARE_OS_ONBOOT: 'echo one\n#c\n echo two' },
drive,
b4a,
console: { log() {}, error() {} },
readLine: async () => null,
async execLine(line) {
execLines.push(String(line).trim())
return 'ok'
}
}
await runKernelFromSource(src, ctx)
t.alike(execLines, ['echo one', 'echo two'])
})
test('stock kernel init.js runs rc.local before onboot', async (t) => {
const src = await readFile(
path.join(__dirname, '../../kernel/init.js'),
'utf8'
)
const execLines = []
const drive = {
async get(p) {
if (p === '/etc/bare-os/rc.local') return b4a.from('echo rc-local')
return null
},
async *readdir() {
/* empty */
}
}
const ctx = {
bareOsSkipRepl: true,
env: { BARE_OS_ONBOOT: 'echo onboot-line' },
drive,
b4a,
console: { log() {}, error() {} },
readLine: async () => null,
async execLine(line) {
execLines.push(String(line).trim())
return 'ok'
}
}
await runKernelFromSource(src, ctx)
t.is(execLines[0], 'echo rc-local')
t.is(execLines[1], 'echo onboot-line')
})
test('stock kernel init.js BARE_OS_BOOT_TRACE=json emits phase JSON on stderr', async (t) => {
const src = await readFile(
path.join(__dirname, '../../kernel/init.js'),
'utf8'
)
const stderr = []
const drive = {
async get() {
return null
},
async *readdir() {
/* empty */
}
}
const ctx = {
bareOsSkipRepl: false,
env: { BARE_OS_BOOT_TRACE: 'json' },
drive,
b4a,
console: {
log() {},
error(...a) {
stderr.push(a.join(' '))
}
},
readLine: async () => null,
async execLine() {
return 'ok'
}
}
await runKernelFromSource(src, ctx)
const phases = stderr
.map((l) => {
try {
return JSON.parse(l)
} catch {
return null
}
})
.filter(Boolean)
t.ok(phases.length >= 3)
t.ok(phases.some((o) => o.phase === 'rc.local'))
t.ok(phases.every((o) => typeof o.ms === 'number'))
})
test('runBinCommand runs /bin helper', async (t) => {
const dir = testCorestoreDir('bin')
const store = new Corestore(dir)
@@ -221,7 +328,10 @@ test('ls hides dotfiles unless -a', async (t) => {
await drive.ready()
await personal.ready()
await drive.put('/bin/ls', b4a.from(lsSrc))
await personal.put(personalHomeBacking('/home/user', 'shown.txt'), b4a.from(''))
await personal.put(
personalHomeBacking('/home/user', 'shown.txt'),
b4a.from('')
)
await personal.put(personalHomeBacking('/home/user', '.hidden'), b4a.from(''))
const lines = []
const ctx = testCtx(drive, personal)
@@ -402,7 +512,9 @@ test('vfs expands ~ and ~/ to $HOME (not read-only /~)', async (t) => {
await vfs.chdir('~')
t.is(vfs.getcwd(), '/home/zuser')
await vfs.writeFile('~/tilde.txt', b4a.from('ok'))
const buf = await personal.get(personalHomeBacking('/home/zuser', 'tilde.txt'))
const buf = await personal.get(
personalHomeBacking('/home/zuser', 'tilde.txt')
)
t.ok(buf)
t.is(b4a.toString(buf), 'ok')
await store.close()
@@ -431,9 +543,7 @@ test('vfs lists virtual /home at root; /home shows only active session dir', asy
t.alike(await vfsU.readdir('/home'), ['eeb18de988e9'])
t.is(await vfsU.stat('/home/guest'), null)
await vfsU.writeFile('/home/eeb18de988e9/x', b4a.from('1'))
const buf = await personal.get(
personalHomeBacking('/home/eeb18de988e9', 'x')
)
const buf = await personal.get(personalHomeBacking('/home/eeb18de988e9', 'x'))
t.ok(buf)
await store.close()
rmSync(dir, { recursive: true, force: true })
@@ -481,9 +591,7 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
const ver = b4a.toString(await vfs.readFile('/proc/version'))
t.ok(ver.includes('Bare OS'))
t.ok(ver.includes('bare_os_ctx_api_version=1.2.3-test'))
const sysVer = b4a.toString(
await vfs.readFile('/sys/fs/bare_os/version')
)
const sysVer = b4a.toString(await vfs.readFile('/sys/fs/bare_os/version'))
t.ok(sysVer.includes('1.2.3-test'))
const cmd = b4a.toString(await vfs.readFile('/proc/self/cmdline'))
t.ok(cmd.includes('unit-test'))
@@ -559,10 +667,7 @@ test('vfs /tmp maps to personal drive per HOME basename', async (t) => {
PATH: '/bin'
})
await vfsGuest.writeFile('/tmp/session.dat', b4a.from('g'))
t.is(
b4a.toString(await personal.get('/.bare-os/tmp/guest/session.dat')),
'g'
)
t.is(b4a.toString(await personal.get('/.bare-os/tmp/guest/session.dat')), 'g')
const vfsUser = createVfs(sys, personal, {
HOME: '/home/hexuserdead',
PWD: '/home/hexuserdead',
@@ -574,10 +679,7 @@ test('vfs /tmp maps to personal drive per HOME basename', async (t) => {
b4a.toString(await personal.get('/.bare-os/tmp/hexuserdead/session.dat')),
'u'
)
t.is(
b4a.toString(await personal.get('/.bare-os/tmp/guest/session.dat')),
'g'
)
t.is(b4a.toString(await personal.get('/.bare-os/tmp/guest/session.dat')), 'g')
await store.close()
rmSync(dir, { recursive: true, force: true })
})
@@ -618,9 +720,7 @@ test('vfs isolates home and /var/log per HOME basename on personal drive', async
'user-data'
)
t.is(
b4a.toString(
await personal.get('/.bare-os/var/log/guest/bare-os/g.log')
),
b4a.toString(await personal.get('/.bare-os/var/log/guest/bare-os/g.log')),
'glog'
)
await vfsUser.writeFile('/var/log/bare-os/u.log', b4a.from('ulog'))
@@ -676,9 +776,7 @@ test('vfs /var in root readdir; /var/log empty lstat; writes map to personal', a
t.is(stLog.type, 'directory')
await vfs.mkdir('/var/log/bare-os', { recursive: true })
await vfs.writeFile('/var/log/bare-os/test-vfs.log', b4a.from('hello'))
const buf = await personal.get(
'/.bare-os/var/log/u/bare-os/test-vfs.log'
)
const buf = await personal.get('/.bare-os/var/log/u/bare-os/test-vfs.log')
t.ok(buf)
t.is(b4a.toString(buf), 'hello')
const sysTry = await sys.get('/.bare-os/var/log/u/bare-os/test-vfs.log')
@@ -779,6 +877,9 @@ test('buildBareOsRuntimeCaps matches ctx API version and pipeline env', async (t
t.ok(Array.isArray(caps.pseudoFsPaths))
t.ok(caps.pseudoFsPaths.includes('/proc/version'))
t.is(caps.features.simulatedPipelines, true)
t.is(caps.features.httpDelegate, true)
t.is(caps.features.gitDelegate, true)
t.is(caps.features.systemctlDelegate, true)
})
test('expandArgvAliases expands first word and keeps trailing argv', async (t) => {
@@ -888,7 +989,9 @@ async function run(ctx) {
error: (...a) => logs.push(['err', ...a])
}
await execShellLine(ctx, 'emit 2>&1 | cat')
const piped = logs.filter((x) => x[0] === 'out').map((x) => x.slice(1).join(' '))
const piped = logs
.filter((x) => x[0] === 'out')
.map((x) => x.slice(1).join(' '))
t.ok(piped.some((l) => l.includes('OUT') && l.includes('ERR')))
await store.close()
rmSync(dir, { recursive: true, force: true })
@@ -994,10 +1097,7 @@ async function run(ctx, argv) {
test('tokenize && || ; and split helpers', async (t) => {
const toks = tokenize('a&&b||c;d')
t.is(
toks.map((x) => x.value).join(' '),
'a && b || c ; d'
)
t.is(toks.map((x) => x.value).join(' '), 'a && b || c ; d')
const lists = splitTokensBySemicolon(toks)
t.is(lists.length, 2)
const { segments, ops } = splitTokensByAndOr(lists[0])
@@ -1408,6 +1508,45 @@ test('tier-1 grep from system drive', async (t) => {
rmSync(dir, { recursive: true, force: true })
})
test('tier-1 find -mindepth skips shallow paths', async (t) => {
const dir = testCorestoreDir('find-mindepth')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pfindmd'))
await drive.ready()
await personal.ready()
await drive.put('/bin/find', b4a.from(await readBuiltBin('find')))
const lines = []
const ctx = testCtx(drive, personal)
ctx.exitCode = 0
ctx.console = {
log(s) {
lines.push(String(s))
},
error() {}
}
await ctx.vfs.mkdir('nest', { recursive: true })
await ctx.vfs.mkdir('nest/sub', { recursive: true })
await ctx.vfs.writeFile('nest/a.txt', b4a.from('a'))
await ctx.vfs.writeFile('nest/sub/b.txt', b4a.from('b'))
await runBinCommand(ctx, ['find', '.', '-mindepth', '2', '-type', 'f'])
t.is(ctx.exitCode, 0)
const norm = lines.map((p) => p.replace(/\\/g, '/')).sort()
t.ok(norm.some((p) => p.endsWith('/nest/a.txt')))
t.ok(norm.some((p) => p.endsWith('/nest/sub/b.txt')))
lines.length = 0
await runBinCommand(ctx, ['find', 'nest', '-mindepth', '2', '-type', 'f'])
t.is(ctx.exitCode, 0)
t.ok(
lines.some((p) => String(p).replace(/\\/g, '/').endsWith('/nest/sub/b.txt'))
)
t.ok(
!lines.some((p) => String(p).replace(/\\/g, '/').endsWith('/nest/a.txt'))
)
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('tier-1 jq from system drive', async (t) => {
const dir = testCorestoreDir('jq')
const store = new Corestore(dir)
@@ -1455,11 +1594,14 @@ test('tier-1 getconf and xargs from system drive', async (t) => {
await drive.ready()
await personal.ready()
await drive.put('/bin/getconf', b4a.from(await readBuiltBin('getconf')))
await drive.put('/bin/echolog', b4a.from(`
await drive.put(
'/bin/echolog',
b4a.from(`
async function run(ctx, argv) {
ctx.console.log(argv.slice(1).join(' '))
}
`))
`)
)
await drive.put('/bin/xargs', b4a.from(await readBuiltBin('xargs')))
await drive.put('/bin/false', b4a.from(await readBuiltBin('false')))
await drive.put('/bin/printf', b4a.from(await readBuiltBin('printf')))
@@ -1655,7 +1797,10 @@ test('curl delegated from booter with stub fetch', async (t) => {
lines.length = 0
ctx.exitCode = 0
ctx.httpFetch = async () =>
new Response('body-o', { status: 200, headers: { 'Content-Type': 'text/plain' } })
new Response('body-o', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
await runBinCommand(ctx, [
'curl',
'-sO',
@@ -2030,7 +2175,14 @@ test('git clean -fd removes untracked files only', async (t) => {
await runGitCli(ctx, ['git', 'init', '-C', '/home/user/clrepo'])
await ctx.vfs.writeFile('/home/user/clrepo/a.txt', b4a.from('a'))
await runGitCli(ctx, ['git', '-C', '/home/user/clrepo', 'add', 'a.txt'])
await runGitCli(ctx, ['git', '-C', '/home/user/clrepo', 'commit', '-m', 'init'])
await runGitCli(ctx, [
'git',
'-C',
'/home/user/clrepo',
'commit',
'-m',
'init'
])
await ctx.vfs.writeFile('/home/user/clrepo/junk.txt', b4a.from('j'))
await runGitCli(ctx, ['git', '-C', '/home/user/clrepo', 'clean', '-fd'])
t.is(await ctx.vfs.readFile('/home/user/clrepo/junk.txt'), null)
+7 -10
View File
@@ -2,17 +2,10 @@
"name": "find",
"section": 1,
"title": "find files",
"synopsis": [
"find [PATH...] [EXPRESSION]"
],
"description": "Walks directories and applies expressions (-name, -type, -print, -maxdepth, logical -and/-or/-not).",
"synopsis": ["find [PATH...] [EXPRESSION]"],
"description": "Walks directories and applies expressions (-name, -type, -print, -maxdepth, -mindepth, logical -and/-or/-not).",
"options": [],
"keywords": [
"find",
"directory",
"walk",
"search"
],
"keywords": ["find", "directory", "walk", "search"],
"bareOsNotes": "Expression syntax is a simplified subset.",
"examples": [
{
@@ -27,6 +20,10 @@
"caption": "max depth",
"code": "find . -maxdepth 2 -type f"
},
{
"caption": "skip top directory level (GNU-like -mindepth 2)",
"code": "find . -mindepth 2 -type f"
},
{
"caption": "OR names",
"code": "find . \\( -name \"*.c\" -o -name \"*.h\" \\)"
+23 -5
View File
@@ -1,4 +1,8 @@
async function walk(ctx, dir, nameRe, wantType, maxDepth, curDepth) {
/**
* curDepth is distance from the search root directory (0 at the initial path).
* Printed paths use gnu-like depth curDepth + 1 (immediate children of the root are depth 1).
*/
async function walk(ctx, dir, nameRe, wantType, maxDepth, minDepth, curDepth) {
if (maxDepth >= 0 && curDepth > maxDepth) return
let names
try {
@@ -16,15 +20,21 @@ async function walk(ctx, dir, nameRe, wantType, maxDepth, curDepth) {
continue
}
if (!st) continue
const gnuDepth = curDepth + 1
if (!nameRe || nameRe.test(n)) {
if (!wantType || st.type === wantType) ctx.console.log(path)
if (!wantType || st.type === wantType) {
if (gnuDepth >= minDepth) ctx.console.log(path)
}
}
if (st.type === 'directory') await walk(ctx, path, nameRe, wantType, maxDepth, curDepth + 1)
if (st.type === 'directory')
await walk(ctx, path, nameRe, wantType, maxDepth, minDepth, curDepth + 1)
}
}
async function run(ctx, argv) {
let maxDepth = -1
/** Minimum path depth below the search root (1 = default; same as GNU -mindepth 1 for tree walks). */
let minDepth = 1
/** @type {string | null} */
let nameGlob = null
/** @type {'file' | 'directory' | 'symlink' | null} */
@@ -36,6 +46,11 @@ async function run(ctx, argv) {
maxDepth = Number.parseInt(argv[++i], 10)
continue
}
if (a === '-mindepth' && argv[i + 1]) {
minDepth = Number.parseInt(argv[++i], 10)
if (!Number.isFinite(minDepth) || minDepth < 1) minDepth = 1
continue
}
if (a === '-name' && argv[i + 1]) {
nameGlob = argv[++i]
continue
@@ -61,9 +76,12 @@ async function run(ctx, argv) {
const root = rest[0] || '.'
let nameRe = null
if (nameGlob) {
const esc = nameGlob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.')
const esc = nameGlob
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*')
.replace(/\?/g, '.')
nameRe = new RegExp('^' + esc + '$')
}
const abs = ctx.vfs.resolveLogical(root)
await walk(ctx, abs, nameRe, wantType, maxDepth, 0)
await walk(ctx, abs, nameRe, wantType, maxDepth, minDepth, 0)
}
+9 -9
View File
@@ -4,22 +4,22 @@ Files in this directory are **read from disk by the seeder** (or copied into `pa
## Staging map (seeder)
| Source | Drive path |
| ----------------- | --------------------- |
| `init.js` | `/boot/init.js` |
| `bin/<name>` | `/bin/<name>` |
| `etc/...` | `/etc/...` |
| `share/man/...` | `/share/man/...` |
| Any other file | `/<relative path>` |
| Source | Drive path |
| --------------- | ------------------ |
| `init.js` | `/boot/init.js` |
| `bin/<name>` | `/bin/<name>` |
| `etc/...` | `/etc/...` |
| `share/man/...` | `/share/man/...` |
| Any other file | `/<relative path>` |
## Contents
- **`init.js`** — Kernel entry: must define `async function start(ctx)`. Boot order: **`/etc/os-release`** → **`/etc/motd`** → optional **`/etc/bare-os/rc.profile.<profile>`** (profile from **`BARE_OS_BOOT_PROFILE`** or first line of **`/etc/bare-os/profile`**) → **`/etc/bare-os/rc`** → **`/etc/bare-os/rc.d/*`** (sorted; only names starting with a digit, plus skips dotfiles, `*~`, `README*`, `*.md`) → banner → when **`BARE_OS_SKIP_REPL`**, optional one line from **`BARE_OS_ONBOOT`** or **`/etc/bare-os/onboot`** → **`readLine` / `execLine`** loop (boot snippet errors are logged, not fatal). Custom kernels may call **`ctx.registerKernelShutdownHook(fn)`** before initd disposers; use **`ctx.bareOsRuntimeCaps`** for limits and pseudo path lists ([`developer-guide/02-the-context-object.md`](../developer-guide/02-the-context-object.md)).
- **`init.js`** — Kernel entry: must define `async function start(ctx)`. Boot order: **`/etc/os-release`** → **`/etc/motd`** → optional **`/etc/bare-os/rc.profile.<profile>`** (profile from **`BARE_OS_BOOT_PROFILE`** or first line of **`/etc/bare-os/profile`**) → **`/etc/bare-os/rc`** → **`/etc/bare-os/rc.d/*`** (sorted; only names starting with a digit, plus skips dotfiles, `*~`, `README*`, `*.md`) → optional **`/etc/bare-os/rc.local`** → banner → when **`BARE_OS_SKIP_REPL`**, optional **onboot** lines from **`BARE_OS_ONBOOT`** (newline-separated) or **`/etc/bare-os/onboot`** (file order)**`readLine` / `execLine`** loop (boot snippet errors are logged, not fatal). Custom kernels may call **`ctx.registerKernelShutdownHook(fn)`** before initd disposers; use **`ctx.bareOsRuntimeCaps`** for limits, pseudo paths, and **`features`** ([`developer-guide/02-the-context-object.md`](../developer-guide/02-the-context-object.md)).
- **`bin/`** — **Tier-1 utilities** built by [bare-os-coreutils](../packages/bare-os-coreutils/README.md). Each file is **`runtime.js`** + optional **`lib/*-engine.js`** (**`sed`**, **`awk`**) or **`lib/man-render.js`** (**`man`**) + **`async function run(ctx, argv)`** (no ESM **`import`** in **`src/`**).
- **`share/man/man.json`** — Merged manual database for **`/bin/man`** (built by **`bare-os-coreutils`**; see [handbook ch.10](../handbook/10-manpages-and-online-help.md)).
- **`etc/os-release`** — Static OS metadata (`NAME`, `VERSION`, …).
- **`etc/motd`** — Optional message printed after **`os-release`** (distributors can customize).
- **`etc/bare-os/banner`** or **`/etc/issue`** — If present on the system drive, the default kernel prints one of these instead of the built-in session hint (unless **`BARE_OS_SKIP_REPL`** shortens the banner). Set **`BARE_OS_BOOT_TRACE=1`** in the session environment to log boot phase timings on stderr.
- **`etc/bare-os/banner`** or **`/etc/issue`** — If present on the system drive, the default kernel prints one of these instead of the built-in session hint (unless **`BARE_OS_SKIP_REPL`** shortens the banner). Set **`BARE_OS_BOOT_TRACE=1`** or **`true`** for **`[boot] phase: Nms`** lines on stderr, or **`json`** for **`{"phase":"…","ms":n}`** per phase.
- **`etc/bare-os/rc`** — Optional boot snippet: one **`execLine`** per non-comment line (trusted).
- **`etc/bare-os/rc.d/`** — Optional extra snippets (basename must start with a digit), same line rules, run after **`rc`** in filename order. Human-oriented notes live in **`.README`** (a dotfile so legacy **`init.js`** never executes it).
+23 -5
View File
@@ -60,7 +60,11 @@ function barePosixBlocks(size) {
return Math.ceil(Number(size) / 512) || 0
}
async function walk(ctx, dir, nameRe, wantType, maxDepth, curDepth) {
/**
* curDepth is distance from the search root directory (0 at the initial path).
* Printed paths use gnu-like depth curDepth + 1 (immediate children of the root are depth 1).
*/
async function walk(ctx, dir, nameRe, wantType, maxDepth, minDepth, curDepth) {
if (maxDepth >= 0 && curDepth > maxDepth) return
let names
try {
@@ -78,15 +82,21 @@ async function walk(ctx, dir, nameRe, wantType, maxDepth, curDepth) {
continue
}
if (!st) continue
const gnuDepth = curDepth + 1
if (!nameRe || nameRe.test(n)) {
if (!wantType || st.type === wantType) ctx.console.log(path)
if (!wantType || st.type === wantType) {
if (gnuDepth >= minDepth) ctx.console.log(path)
}
}
if (st.type === 'directory') await walk(ctx, path, nameRe, wantType, maxDepth, curDepth + 1)
if (st.type === 'directory')
await walk(ctx, path, nameRe, wantType, maxDepth, minDepth, curDepth + 1)
}
}
async function run(ctx, argv) {
let maxDepth = -1
/** Minimum path depth below the search root (1 = default; same as GNU -mindepth 1 for tree walks). */
let minDepth = 1
/** @type {string | null} */
let nameGlob = null
/** @type {'file' | 'directory' | 'symlink' | null} */
@@ -98,6 +108,11 @@ async function run(ctx, argv) {
maxDepth = Number.parseInt(argv[++i], 10)
continue
}
if (a === '-mindepth' && argv[i + 1]) {
minDepth = Number.parseInt(argv[++i], 10)
if (!Number.isFinite(minDepth) || minDepth < 1) minDepth = 1
continue
}
if (a === '-name' && argv[i + 1]) {
nameGlob = argv[++i]
continue
@@ -123,9 +138,12 @@ async function run(ctx, argv) {
const root = rest[0] || '.'
let nameRe = null
if (nameGlob) {
const esc = nameGlob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.')
const esc = nameGlob
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*')
.replace(/\?/g, '.')
nameRe = new RegExp('^' + esc + '$')
}
const abs = ctx.vfs.resolveLogical(root)
await walk(ctx, abs, nameRe, wantType, maxDepth, 0)
await walk(ctx, abs, nameRe, wantType, maxDepth, minDepth, 0)
}
+41 -18
View File
@@ -3,14 +3,17 @@
* Loaded by the booter with an injected ctx object (trusted replication source).
*
* Boot order: /etc/os-release → /etc/motd → optional profile rc → /etc/bare-os/rc →
* /etc/bare-os/rc.d/* (sorted; digit-prefixed snippet names) → session banner
* optional oneshot onboot when BARE_OS_SKIP_REPL → interactive loop.
* /etc/bare-os/rc.d/* (sorted; digit-prefixed snippet names) → /etc/bare-os/rc.local
* session banner → optional onboot lines when BARE_OS_SKIP_REPL → interactive loop.
*
* Profile: first non-empty line of /etc/bare-os/profile, overridden by BARE_OS_BOOT_PROFILE.
* When set, runs /etc/bare-os/rc.profile.<name> if present (trusted execLine, before main rc).
*
* Non-interactive onboot: when ctx.bareOsSkipRepl, runs one line from BARE_OS_ONBOOT env
* or first non-empty, non-# line from /etc/bare-os/onboot (trusted), then readLine yields EOF.
* Non-interactive onboot: when ctx.bareOsSkipRepl, runs each non-empty, non-# line from
* BARE_OS_ONBOOT (newline-separated) or, if unset, every such line from /etc/bare-os/onboot
* in file order (trusted). Then readLine yields EOF.
*
* BARE_OS_BOOT_TRACE=json logs one JSON object per phase on stderr: {"phase":"…","ms":n}.
*
* Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers.
*/
@@ -20,7 +23,14 @@
*/
function wantBootTrace(ctx) {
const v = ctx.env && ctx.env.BARE_OS_BOOT_TRACE
return v === '1' || v === 'true'
return v === '1' || v === 'true' || v === 'json'
}
/**
* @param {Record<string, unknown>} ctx
*/
function isBootTraceJson(ctx) {
return ctx.env && ctx.env.BARE_OS_BOOT_TRACE === 'json'
}
/**
@@ -32,7 +42,12 @@ async function bootTimed(ctx, label, fn) {
const t0 = Date.now()
await fn()
if (wantBootTrace(ctx)) {
ctx.console.error(`[boot] ${label}: ${Date.now() - t0}ms`)
const ms = Date.now() - t0
if (isBootTraceJson(ctx)) {
ctx.console.error(JSON.stringify({ phase: label, ms }))
} else {
ctx.console.error(`[boot] ${label}: ${ms}ms`)
}
}
}
@@ -120,16 +135,21 @@ async function runProfileRc(ctx, profileName) {
}
/**
* When stdin is non-interactive, run a single trusted boot command (automation).
* When stdin is non-interactive, run trusted boot commands (automation).
* @param {Record<string, unknown>} ctx
*/
async function runOnbootOnce(ctx) {
async function runOnboot(ctx) {
if (!ctx.bareOsSkipRepl) return
const { execLine, console, drive, b4a, env } = ctx
let line = ''
/** @type {string[]} */
const lines = []
const fromEnv = env && env.BARE_OS_ONBOOT
if (fromEnv != null && String(fromEnv).trim()) {
line = String(fromEnv).trim()
for (const raw of String(fromEnv).split(/\r?\n/)) {
const t = raw.trim()
if (!t || t.startsWith('#')) continue
lines.push(t)
}
} else {
try {
const buf = await drive.get('/etc/bare-os/onboot')
@@ -137,19 +157,19 @@ async function runOnbootOnce(ctx) {
for (const raw of b4a.toString(buf).split(/\r?\n/)) {
const t = raw.trim()
if (!t || t.startsWith('#')) continue
line = t
break
lines.push(t)
}
}
} catch (e) {
console.error('onboot: ' + ((e && e.message) || String(e)))
}
}
if (!line) return
try {
await execLine(line)
} catch (e) {
console.error((e && e.message) || String(e))
for (const line of lines) {
try {
await execLine(line)
} catch (e) {
console.error((e && e.message) || String(e))
}
}
}
@@ -247,8 +267,11 @@ async function start(ctx) {
await bootTimed(ctx, 'rc.profile', () => runProfileRc(ctx, profileName))
await bootTimed(ctx, 'rc', () => runRcFileAt(ctx, '/etc/bare-os/rc', 'rc'))
await bootTimed(ctx, 'rc.d', () => runBareOsRcDir(ctx))
await bootTimed(ctx, 'rc.local', () =>
runRcFileAt(ctx, '/etc/bare-os/rc.local', 'rc.local')
)
await printSessionBanner(ctx)
await bootTimed(ctx, 'onboot', () => runOnbootOnce(ctx))
await bootTimed(ctx, 'onboot', () => runOnboot(ctx))
while (true) {
const line = await readLine('')
if (line == null) break
File diff suppressed because one or more lines are too long