Skip to content

Bootstrap

The initialization sequence from Unix() builder to running system.

Overview

The bootstrap has three distinct phases:

  1. BuildUnix().use(...).build() produces a frozen UnixImage
  2. RuntimenodeRuntime(), browserRuntime(), or testRuntime() provides platform capabilities
  3. Bootruntime.boot(image, opts) assembles the namespace, creates the kernel, starts PID 1
Unix() -- configure
.use(stdSystem())
.use(myTools())
.build() -- freeze into image
nodeRuntime() -- platform capabilities
.boot(image, { tty }) -- assemble namespace, create kernel, start PID 1

Build Phase

function Unix(): UnixBuilder
interface UnixBuilder {
use(ext: Extension): UnixBuilder // primary composition
mount(path, server): UnixBuilder // sugar over use()
bin(name, fn): UnixBuilder
env(key, value): UnixBuilder
file(path, content): UnixBuilder
service(def): UnixBuilder
run(command): UnixBuilder // execute shell command during build
catalog(name, factory): UnixBuilder // register offline package
build(): Promise<UnixImage> // freeze into image
}

The builder accumulates Extensions and produces a frozen image. Each method returns a new builder (immutable). .build() merges all Extensions, writes exec nodes to a MemoryFS, executes .run() steps (each producing a frozen overlay layer), and returns the image.

Decision: builder is immutable. Each .use() returns a new builder. Forking is safe — base configs can branch into specialized variants without interference.

Image

An image is a frozen, immutable system configuration:

interface UnixImage {
createBootContext(): BootContext // data contract for Runtime
extend(): UnixBuilder // derive a new builder from this image
}

The image holds:

  • Frozen rootFS — stacked overlay layers from .build() and .run() steps
  • Merged config — bins, env, mounts, services, catalog entries

createBootContext() returns a read-only view of the image’s contents for the Runtime to consume. extend() creates a new builder pre-loaded with the image’s layers.

Runtime Phase

The Runtime sits between Image and Instance. It knows what platform it’s on and provides platform-appropriate capabilities. See runtime architecture for full details.

import { nodeRuntime } from '@fishnet/core/node'
import { browserRuntime } from '@fishnet/core/browser'
import { testRuntime } from '@fishnet/core/test'
// Each returns a Runtime with platform-specific capabilities
const rt = nodeRuntime() // node:fs asset loading, Buffer-based ESM eval
const rt = browserRuntime() // fetch-based loading, Blob URL ESM eval
const rt = testRuntime() // minimal caps, throws on asset loading

Boot Phase

runtime.boot(image, opts) assembles the running system:

runtime.boot(image, opts):
1. Read BootContext from image (rootFs, mounts, bins, env, services, catalogs)
2. Create writable upper layer (memoryFS)
3. Create overlay root: overlayFS(upper, rootFs)
4. Mount devFS at /dev
5. Create processTable + KernelLog
6. Mount procFS at /proc (with signal + getNs callbacks)
7. Mount consFS at /dev/cons (if tty provided) via mountConsTerminal helper
8. Mount userFS at /dev/user
9. Mount volumes from BootOpts
10. Mount srvFS at /srv
11. Write catalog metadata to /pkg/.catalog/
12. Build environment (DEFAULT_ENV + image env)
13. Build catalogInstall callback
14. Create kernel with mount table + callbacks
15. Determine PID 1 (init with services, or shell)
16. Construct and return UnixInstance

Decision: procFS signal callback captures kernel via late-binding closure. The kernel is created in step 14 but referenced in step 6’s procFS callback. This is resolved via let kernel with late assignment — the callback reads it lazily, only after the kernel exists.

PID 1

If services are declared, PID 1 is makeInit(services) — a generated init that starts services in dependency order, then starts a shell, then supervises.

If no services, PID 1 is the shell directly. Zero overhead for the common case.

When PID 1 exits:

  1. All child processes receive SIGTERM
  2. After 5s grace period, survivors receive SIGKILL
  3. instance.wait() resolves with PID 1’s exit code
  4. kernelLog.dispose() unblocks any /proc/log readers

BootOpts

Per-instance configuration provided at boot time:

interface BootOpts {
tty?: TtyConfig // terminal streams
cwd?: string // initial working directory
volumes?: Record<string, Fileserver> // direct mounts (bypass overlay)
}

Complete Example

import { Unix, stdSystem } from '@fishnet/core'
import { nodeRuntime } from '@fishnet/core/node'
import { nodeStdio } from '@fishnet/core/node'
// Build image
const image = await Unix()
.use(stdSystem())
.env('EDITOR', 'vim')
.build()
// Create runtime
const rt = nodeRuntime()
// Boot with terminal
const io = await nodeStdio({ prompt: '$ ' })
const instance = await rt.boot(image, { tty: io.tty })
// Wait for shell to exit
const exitCode = await instance.boot()

Lifecycle

Unix().use(...).build() -- freeze into image
|
v
runtime.boot(image, opts) -- assemble namespace, create kernel
|
v
instance.boot() -- start PID 1 (shell or init)
|
v
Running (processes executing)
|
v
instance.shutdown() -- SIGTERM -> 5s -> SIGKILL -> cleanup
|
v
Disposed