Updates
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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\" \\)"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user