Document not found (404)
+This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +diff --git a/book/book.toml b/book/book.toml new file mode 100644 index 0000000..0e0cfee --- /dev/null +++ b/book/book.toml @@ -0,0 +1,5 @@ +[book] +authors = ["Till Wegmueller"] +language = "en" +src = "src" +title = "Refraction Forger" diff --git a/book/book/.nojekyll b/book/book/.nojekyll new file mode 100644 index 0000000..f173110 --- /dev/null +++ b/book/book/.nojekyll @@ -0,0 +1 @@ +This file makes sure that Github Pages doesn't process mdBook's output. diff --git a/book/book/404.html b/book/book/404.html new file mode 100644 index 0000000..f7ead9d --- /dev/null +++ b/book/book/404.html @@ -0,0 +1,221 @@ + + +
+ + +Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+When the build host doesn't match the target OS, Forger delegates to an ephemeral builder VM. This chapter explains the internals.
+1. Image Resolution
+ ├── OCI reference → pull from registry
+ ├── URL → download
+ └── Local path → use directly
+
+2. Cloud-Init Generation
+ ├── Generate ephemeral Ed25519 SSH keypair
+ ├── Create user-data: builder user, SSH key, passwordless sudo
+ └── Create cloud-init ISO
+
+3. VM Creation
+ ├── Create VmSpec (CPU, memory, disk, network)
+ ├── Network: user-mode (SLIRP) — no root required
+ ├── Disk: overlay on builder image (20GB default)
+ └── Hypervisor: auto-detect via vm-manager
+
+4. Boot & Connect
+ ├── Start VM via hypervisor
+ └── SSH retry loop (up to 120 seconds)
+
+5. Transfer
+ ├── Upload forger binary via SCP
+ ├── Upload spec files via SCP
+ └── Upload overlay files via SCP
+
+6. Build
+ └── Execute forger build command via SSH
+
+7. Download
+ └── Retrieve artifacts via SCP
+
+8. Teardown (guaranteed, even on failure)
+ └── Destroy VM via hypervisor
+
+Builder VMs use QEMU's user-mode networking (SLIRP). This means:
+DNS is automatically configured if needed.
+Builder images are QCOW2 files with cloud-init support. To create your own:
+cloud-init is installed and enabledpkg, qemu-img, zfs, etc.)qemu-img (for QCOW2 conversion)zfs/zpool for ZFS, parted/mkfs.ext4 for ext4)The builder VM uses a disk overlay (copy-on-write layer) on top of the builder image. This means:
+Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Forger is a Cargo workspace with five crates, each with a clear responsibility.
+crates/
+├── forger/ CLI entry point
+├── spec-parser/ KDL parsing and resolution
+├── forge-engine/ Build execution (Phase 1 + Phase 2)
+├── forge-builder/ Remote VM builds
+└── forge-oci/ OCI image and registry operations
+
+The binary crate. Defines five subcommands via clap:
+| Command | Description |
|---|---|
build | Build image from spec |
validate | Parse and check a spec |
inspect | Show resolved, profile-filtered spec |
push | Push artifact to OCI registry |
targets | List targets in a spec |
This crate is thin — it parses CLI arguments and delegates to the library crates.
+Handles everything related to KDL spec files:
+knuffel crate to deserialize KDL into Rust structs (ImageSpec, Target, Package, etc.)base and include references, merging specs while detecting circular dependenciesKey types:
+ImageSpec — Root spec structureDistroFamily — Enum discriminating OmniOS vs UbuntuTarget / TargetKind — Output target definitionsOverlays — File operations (file, ensure-dir, ensure-symlink, shadow, devfsadm, remove-files)The build engine implementing both phases:
+phase1/mod.rs): Distro-specific rootfs assembly
+phase2/mod.rs): Target production
+qcow2_zfs.rs: ZFS pool creation, BE managementqcow2_ext4.rs: Partitioning, ext4 formattingKey abstraction: ToolRunner trait — wraps all external tool execution (pkg, apt, qemu-img, zfs, parted, etc.) through a single interface. The real implementation (SystemToolRunner) uses tokio::process::Command. This trait enables testing without root access.
Manages remote builds via ephemeral VMs:
+vm-manager crate (user-mode networking)Uses the external vm-manager crate for hypervisor abstraction (RouterHypervisor auto-detects QEMU/bhyve).
OCI-specific operations:
+forger
+ ├── spec-parser
+ ├── forge-engine
+ │ └── spec-parser
+ ├── forge-builder
+ │ ├── spec-parser
+ │ └── vm-manager (external)
+ └── forge-oci
+
+All crates are async (tokio) and use miette for error diagnostics.
+| Category | Crate | Purpose |
|---|---|---|
| KDL parsing | knuffel 3.2 | Spec deserialization |
| CLI | clap 4.5 | Argument parsing |
| Async | tokio 1 | Runtime, process, fs |
| OCI | oci-spec, oci-client | Image spec, registry client |
| SSH | ssh2 | Remote builder communication |
| Errors | miette, thiserror | Rich diagnostics |
| VM | vm-manager (path dep) | Hypervisor abstraction |
Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Refraction Forger is designed around three principles: declarative specs, two-phase execution, and distro abstraction.
+ ┌─────────────┐
+ │ KDL Spec │
+ └──────┬──────┘
+ │
+ ┌──────▼──────┐
+ │ spec-parser │ Parse → Resolve → Filter
+ └──────┬──────┘
+ │
+ ┌──────▼──────┐
+ │ forger │ CLI routing
+ └──────┬──────┘
+ │
+ ┌────────────┼────────────┐
+ │ │
+ ┌──────▼──────┐ ┌──────▼──────┐
+ │ Local Build │ │forge-builder│
+ └──────┬──────┘ └──────┬──────┘
+ │ │
+ │ Ephemeral VM
+ │ ┌─────────────┐
+ │ │ SSH + SCP │
+ │ └──────┬──────┘
+ │ │
+ ┌──────▼─────────────────────────▼──────┐
+ │ forge-engine │
+ │ ┌──────────┐ ┌─────────────────┐ │
+ │ │ Phase 1 │────▶│ Phase 2 │ │
+ │ │ Rootfs │ │ QCOW2/OCI/Tar │ │
+ │ └──────────┘ └────────┬────────┘ │
+ └────────────────────────────┼──────────┘
+ │
+ ┌──────▼──────┐
+ │ forge-oci │ Registry push
+ └─────────────┘
+
+The old tools used shell scripts to orchestrate builds — ordering mattered, error handling was manual, and reuse required copy-paste. Forger uses a declarative spec where you describe what the image should contain, and the engine handles how.
+Packer boots an ISO, types keystrokes into a virtual console, and waits for an installer to finish. Forger calls the package manager directly to assemble a rootfs, skipping the installer entirely. This is faster and more reliable.
+The old omnios-image-builder required an illumos host with ZFS and pfexec. Forger can build from any platform by spinning up an ephemeral builder VM. The build environment is part of the spec, not a prerequisite.
Instead of custom upload scripts for each cloud provider, Forger uses OCI registries as a universal distribution mechanism. VM images, container images, and tar artifacts all flow through the same registry infrastructure.
+Forger uses miette for rich error diagnostics. When something fails, you get:
+This is deliberate — image building involves many external tools and system operations, and clear error messages are essential for debugging.
+ +Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Every Forger build follows a two-phase architecture. Understanding this split is key to building efficient images and debugging build failures.
+Phase 1 creates a populated filesystem tree in a staging directory. The work is entirely distro-specific:
+pkg image-create — Initialize an IPS package imagepkg set-publisher — Configure each publisher (name + origin URL)pkg change-variant — Set zone variant (global or nonglobal)pkg approve-ca-cert — Trust CA certificates for signed packagespkg install — Install all specified packagesdebootstrap — Bootstrap a minimal Debian/Ubuntu rootfs/etc/apt/sources.list from repository configurationapt update — Refresh package listsapt install — Install all specified packagesPhase 1 produces a staging directory containing a complete rootfs. This directory is consumed by Phase 2.
+Phase 2 takes the rootfs from Phase 1 and packages it into the requested output format. This logic is shared across all distros.
+Create raw disk file (specified size)
+ → Attach as loopback device
+ → ZFS: create pool → create BE dataset → mount
+ → ext4: partition → format → mount
+ → Copy rootfs into mounted filesystem
+ → Install bootloader (UEFI/GRUB)
+ → ZFS: set bootfs → unmount → export pool
+ → ext4: unmount
+ → Detach loopback
+ → qemu-img convert raw → qcow2
+
+The ZFS path creates a unique pool name during build (e.g., forgebuild_12345) and renames to rpool after export. This prevents conflicts with existing pools on the build host.
Compress rootfs → tar.gz layer
+ → Compute SHA256 digest
+ → Build OCI config JSON (entrypoint, env)
+ → Build OCI manifest JSON
+ → Write OCI Image Layout directory
+
+Create tar archive from rootfs
+
+The split serves several purposes:
+If Phase 2 fails (e.g., disk too small, bootloader installation error), Forger cleans up:
+Cleanup runs even on failure, preventing resource leaks on the build host.
+ +Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+The base directive establishes a parent-child relationship between specs. This is Forger's primary caching mechanism: a parent spec produces an artifact, and a child spec consumes it.
omnios-base.kdl (parent)
+ → builds artifact (tar archive cached on disk or in registry)
+ → omnios-disk.kdl (child, consumes the artifact)
+ → builds QCOW2
+
+The parent handles the expensive, slow operations (OS installation, base package setup). The child adds customization and produces the final image. When the parent's output is cached, rebuilding the child skips the entire base installation.
+In the child spec:
+base "omnios-base.kdl"
+
+// Child-specific additions
+packages {
+ package "/system/cloud-init"
+ package "/driver/virtio/vioif"
+}
+
+target "vm" kind="qcow2" {
+ disk-size "2G"
+ bootloader "uefi"
+ filesystem "zfs"
+}
+
+The parent spec (omnios-base.kdl) defines the foundation:
metadata name="omnios-base" version="1.0.0" description="Base OmniOS configuration"
+
+repositories {
+ publisher name="omnios" origin="https://pkg.omnios.org/bloody/core/"
+ publisher name="extra.omnios" origin="https://pkg.omnios.org/bloody/extra/"
+}
+
+incorporation "entire"
+
+certificates {
+ ca publisher="omnios" certfile="omniosce-ca.cert.pem"
+}
+
+packages {
+ package "/editor/vim"
+ package "/network/openssh-server"
+ package "/network/rsync"
+}
+
+When a child references a parent via base:
The key insight is that base creates a build stage boundary. The parent's output (typically an artifact/tar) is the cache unit:
This mirrors the strap→image→archive pipeline from the old omnios-image-builder, but expressed declaratively.
Base specs can themselves have bases, creating a chain:
+distro-base.kdl
+ → platform-base.kdl
+ → application-image.kdl
+
+Each level adds or refines what the previous level established. Circular references are detected and rejected.
+| Base | Include | |
|---|---|---|
| Relationship | Parent → child (produces artifact for child to consume) | Sibling (shared steps imported) |
| Caching | Yes — parent output is the cache boundary | No — just DRY for config |
| Merging | Full merge with child overrides | Steps imported as-is |
| Targets | Child's targets used | No target interaction |
Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+The include directive imports shared configuration steps from another spec file into the current spec. Unlike base, includes don't create a build-stage boundary — they simply pull in common definitions for reuse.
include "common.kdl"
+include "devfs.kdl"
+
+An include file is a regular .kdl spec containing overlays, packages, customizations, or other configuration. When included, its contents are merged into the including spec as if they were written inline.
common.kdloverlays {
+ ensure-symlink "/etc/svc/profile/generic.xml" target="generic_limited_net.xml"
+ ensure-symlink "/etc/svc/profile/inetd_services.xml" target="inetd_generic.xml"
+ ensure-symlink "/etc/svc/profile/platform.xml" target="platform_none.xml"
+ ensure-symlink "/etc/svc/profile/name_service.xml" target="ns_dns.xml"
+
+ file destination="/etc/inet/hosts" source="files/etc/hosts"
+ file destination="/etc/nodename" source="files/etc/nodename"
+ file destination="/etc/resolv.conf" source="files/etc/resolv.conf"
+ ensure-symlink "/etc/nsswitch.conf" target="/etc/nsswitch.dns"
+}
+
+devfs.kdloverlays {
+ devfsadm
+
+ remove-files "/dev/dsk" "/dev/rdsk" "/dev/cfg" "/dev/usb"
+
+ ensure-dir "/dev/cfg" owner="root" group="root" mode="755"
+ ensure-dir "/dev/dsk" owner="root" group="root" mode="755"
+ ensure-dir "/dev/rdsk" owner="root" group="root" mode="755"
+ ensure-dir "/dev/usb" owner="root" group="root" mode="755"
+
+ ensure-symlink "/dev/msglog" target="../devices/pseudo/log@0:msglog"
+}
+
+A disk image spec can import these shared steps:
+base "omnios-base.kdl"
+include "devfs.kdl"
+include "common.kdl"
+
+packages {
+ package "/system/cloud-init"
+}
+
+overlays {
+ file destination="/boot/conf.d/console" source="files/boot_console.115200"
+ shadow username="root" password="$5$..."
+}
+
+target "vm" kind="qcow2" {
+ disk-size "2G"
+ bootloader "uefi"
+ filesystem "zfs"
+}
+
+This gives you control over layering: device filesystem setup (devfs.kdl) before network configuration (common.kdl) before image-specific overlays.
Includes are lightweight — they don't trigger a separate build phase or produce intermediate artifacts.
+ +Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Forger's composability model enables multi-stage build pipelines where each stage's output feeds as input to the next. This is the key to fast, cacheable image builds.
+A typical pipeline for a bootable OmniOS VM image looks like this:
+Stage 1: Base (expensive, cached)
+ omnios-base.kdl
+ → Initializes IPS
+ → Sets publishers
+ → Installs core packages
+ → Produces: artifact (tar archive)
+
+Stage 2: Image (fast, incremental)
+ omnios-disk.kdl (base: omnios-base.kdl)
+ → Consumes base artifact
+ → Adds cloud-init, drivers
+ → Applies overlays (config files, device nodes)
+ → Produces: QCOW2 image
+
+The first build runs both stages. Subsequent builds skip Stage 1 if the base hasn't changed, jumping straight to Stage 2.
+The most expensive operation in image building is package installation — downloading and extracting hundreds of packages from a repository. Put this in the base spec:
+// omnios-base.kdl — the slow part, cached
+metadata name="omnios-base" version="1.0.0"
+
+repositories {
+ publisher name="omnios" origin="https://pkg.omnios.org/bloody/core/"
+ publisher name="extra.omnios" origin="https://pkg.omnios.org/bloody/extra/"
+}
+
+incorporation "entire"
+
+certificates {
+ ca publisher="omnios" certfile="omniosce-ca.cert.pem"
+}
+
+variants {
+ set name="opensolaris.zone" value="global"
+}
+
+packages {
+ package "/editor/vim"
+ package "/network/openssh-server"
+ package "/network/rsync"
+ package "/service/network/ntpsec"
+ package "/web/curl"
+ package "/web/wget"
+}
+
+Then create derivative specs that add target-specific configuration:
+// omnios-disk.kdl — fast, builds on cached base
+base "omnios-base.kdl"
+include "devfs.kdl"
+include "common.kdl"
+
+packages {
+ package "/system/cloud-init"
+ package "/driver/virtio/vioif"
+ package "/driver/virtio/vioblk"
+}
+
+overlays {
+ file destination="/boot/conf.d/console" source="files/boot_console.115200"
+ shadow username="root" password="$5$rounds=10000$..."
+}
+
+target "vm" kind="qcow2" {
+ disk-size "2G"
+ bootloader "uefi"
+ filesystem "zfs"
+ pool {
+ property name="ashift" value="12"
+ }
+}
+
+omnios-base.kdl
+ ├── omnios-disk.kdl → QCOW2 VM image
+ ├── omnios-rust-ci.kdl → Rust CI image (adds rust, git, build tools)
+ ├── omnios-container.kdl → OCI container
+ └── omnios-aws.kdl → AWS-specific VM image
+
+Each derivative shares the same cached base, so building all four images only runs the expensive Stage 1 once.
+The old omnios-image-builder achieved the same pattern with ZFS snapshots:
01-strap.json → pkg install entire → ZFS snapshot "strap"
+02-image.json → pkg install extras → ZFS snapshot "image"
+03-archive.json → pack_tar → omnios-bloody.tar.gz
+aws.json → unpack_tar + make_bootable → raw disk
+
+Each ZFS snapshot was a cache point. The -f flag forced a full rebuild.
Forger replaces this with the base directive. No ZFS on the build host required. No manual snapshot management. The caching is implicit in the spec relationship.
Packer has no native multi-stage caching. Each build starts from an ISO installation, runs provisioner scripts, and captures the result. There's no way to say "skip the OS install and start from here."
+Forger's pipeline model is fundamentally more efficient for iterative development.
+ +Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Profiles let you create multiple image variants from a single spec. Blocks tagged with if="profile-name" are only included when that profile is active.
Tag any packages, overlays, or customization block with an if property:
// Always included (no condition)
+packages {
+ package "/editor/vim"
+ package "/network/openssh-server"
+}
+
+// Only when --profile build is active
+packages if="build" {
+ package "/developer/build-essential"
+ package "/ooce/developer/omnios-build-tools"
+}
+
+// Only when --profile ci is active
+customization if="ci" {
+ user "ci"
+}
+
+overlays if="debug" {
+ file destination="/etc/system" source="files/etc/system.debug"
+}
+
+Use the --profile flag (repeatable) when building or inspecting:
# No profiles — just the base packages
+forger build --spec my-image.kdl
+
+# With build tools
+forger build --spec my-image.kdl --profile build
+
+# With build tools AND CI user
+forger build --spec my-image.kdl --profile build --profile ci
+
+# Inspect with profiles to see what would be included
+forger inspect --spec my-image.kdl --profile build --profile ci
+
+Profile filtering happens after spec resolution (base + include merging) but before the build starts:
+if condition → always keptif value matches an active profile → keptif value doesn't match any active profile → removedpackages {
+ package "/network/openssh-server"
+}
+
+packages if="dev" {
+ package "/developer/build-essential"
+ package "/diagnostic/top"
+}
+
+overlays if="dev" {
+ shadow username="root" password="$5$..."
+}
+
+packages if="rust-ci" {
+ package "/ooce/developer/rust"
+ package "/developer/build/gnu-make"
+}
+
+packages if="go-ci" {
+ package "/ooce/developer/go"
+}
+
+overlays if="aws" {
+ file destination="/boot/conf.d/console" source="files/boot_console.aws"
+ file destination="/etc/dhcp/dhcpagent" source="files/dhcpagent.aws"
+}
+
+overlays if="digitalocean" {
+ file destination="/boot/conf.d/console" source="files/boot_console.do"
+}
+
+Profiles work across the full composition chain. Conditional blocks in base specs and includes are filtered together with the current spec's blocks:
+// omnios-base.kdl
+packages if="build" {
+ package "/developer/build-essential"
+}
+
+// my-image.kdl
+base "omnios-base.kdl"
+
+packages if="build" {
+ package "/ooce/developer/rust"
+}
+
+Building with --profile build activates both blocks from both files.
Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Forger's distro support is built around the DistroFamily abstraction. Adding a new distribution means extending this abstraction at the spec-parsing and build-engine levels.
The distro system has three touch points:
+spec-parser: Maps distro strings to DistroFamily enumforge-engine Phase 1: Distro-specific rootfs assembly logicforge-builder: Default builder image selectionIn crates/spec-parser/src/lib.rs, add your distro to the enum:
+#![allow(unused)] +fn main() { +pub enum DistroFamily { + OmniOS, + Ubuntu, + Fedora, // New +} +}
Update the detection logic that maps the distro string to a family:
+#![allow(unused)] +fn main() { +fn detect_family(distro: &str) -> DistroFamily { + if distro.starts_with("ubuntu") { + DistroFamily::Ubuntu + } else if distro.starts_with("fedora") { + DistroFamily::Fedora + } else { + DistroFamily::OmniOS + } +} +}
If your distro uses a different repository format, add it to the repositories parsing in the spec:
repositories {
+ // Existing
+ publisher name="..." origin="..." // IPS
+ apt-mirror "..." suite="..." components="..." // APT
+
+ // New: DNF/YUM
+ dnf-repo name="fedora" baseurl="https://..." gpgkey="..."
+}
+
+In crates/forge-engine/src/phase1/mod.rs, add the rootfs assembly path for your distro. This is the core work — each distro has its own bootstrap process:
pkg image-create → set publishers → installdebootstrap → write sources.list → apt installdnf --installroot → write repo files → dnf installThe key operations:
+In crates/forge-builder/src/lib.rs, add a default builder image:
+#![allow(unused)] +fn main() { +match distro_family { + DistroFamily::OmniOS => "oci://ghcr.io/cloudnebulaproject/omnios-builder:latest", + DistroFamily::Ubuntu => "oci://ghcr.io/cloudnebulaproject/ubuntu-builder:latest", + DistroFamily::Fedora => "oci://ghcr.io/cloudnebulaproject/fedora-builder:latest", +} +}
You'll also need to build and publish the builder image itself.
+Create example specs in the images/ directory showing common patterns for the new distro.
Add a chapter to this book under Distro Guide covering:
+When adding a distro, keep these principles in mind:
+ToolRunner trait wraps all external tool execution. Use it for any new package manager commands — this enables testing without root access.Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Forger's primary focus is the illumos ecosystem. This chapter provides essential background for image developers working with illumos distributions.
+illumos is a Unix operating system kernel derived from OpenSolaris. It powers several distributions including OmniOS, OpenIndiana, and SmartOS. Key technologies that distinguish illumos from Linux:
+ZFS is the default and recommended filesystem for illumos. It provides:
+Forger creates ZFS pools natively for QCOW2 targets on illumos.
+IPS is illumos's package manager. Key concepts:
+omnios, extra.omnios)entire)opensolaris.zone=global)/network/openssh-server)Forger wraps IPS operations directly — no shell scripting needed.
+SMF is illumos's service manager (similar to systemd on Linux, but predates it). Services are defined by XML manifests and managed via profiles:
+Forger configures SMF profiles through symlink overlays:
+overlays {
+ ensure-symlink "/etc/svc/profile/generic.xml" target="generic_limited_net.xml"
+ ensure-symlink "/etc/svc/profile/name_service.xml" target="ns_dns.xml"
+}
+
+illumos zones are lightweight OS-level containers (similar to LXC/Docker, but predating both). The opensolaris.zone variant controls whether packages include global-zone-only components:
global: Full system including kernel modules, boot components, and hardware driversnonglobal: Zone-only packages (no kernel or hardware support)For VM images, always use global:
variants {
+ set name="opensolaris.zone" value="global"
+}
+
+illumos manages device nodes through devfsadm, which discovers hardware and creates entries in /dev. For image building, this means:
devfsadm to create correct entries for the target hardware/dev/dsk, /dev/rdsk, /dev/cfg, /dev/usb)Forger handles this through the devfsadm overlay and the conventional devfs.kdl include.
| Distribution | Focus | Publisher URL |
|---|---|---|
| OmniOS | Server/cloud, minimal, stable | pkg.omnios.org |
| OpenIndiana | Desktop/general-purpose, broader package set | pkg.openindiana.org |
| SmartOS | Hypervisor/container host (Joyent) | Not IPS-based |
Forger currently supports OmniOS. OpenIndiana support follows the same IPS path.
+ +Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+OmniOS is Forger's primary target distribution. It's a server-focused illumos distribution maintained by the OmniOS Community Edition (OmniOSce) project.
+| Branch | URL Path | Use Case |
|---|---|---|
| bloody | /bloody/core/ | Rolling development, latest packages |
| stable (e.g., r151050) | /r151050/core/ | Production, LTS releases |
metadata name="omnios-base" version="1.0.0"
+
+repositories {
+ publisher name="omnios" origin="https://pkg.omnios.org/bloody/core/"
+ publisher name="extra.omnios" origin="https://pkg.omnios.org/bloody/extra/"
+}
+
+incorporation "entire"
+
+certificates {
+ ca publisher="omnios" certfile="omniosce-ca.cert.pem"
+}
+
+variants {
+ set name="opensolaris.zone" value="global"
+}
+
+packages {
+ package "/editor/vim"
+ package "/network/openssh-server"
+}
+
+incorporation "entire": Pins all packages to a consistent version set. Without this, you may get incompatible package versions.certificates: OmniOS packages are signed. The CA certificate file (omniosce-ca.cert.pem) must be available relative to the spec.variants with opensolaris.zone=global: Required for bootable VM images. Omitting this may exclude kernel modules.packages {
+ package "/editor/vim"
+ package "/network/openssh-server"
+ package "/network/rsync"
+ package "/service/network/ntpsec"
+ package "/web/curl"
+ package "/web/wget"
+}
+
+packages {
+ package "/system/cloud-init"
+ package "/driver/virtio/vioif" // Virtio network
+ package "/driver/virtio/vioblk" // Virtio block storage
+ package "/driver/virtio/vio9p" // 9p filesystem sharing
+ package "/driver/virtio/vioscsi" // Virtio SCSI
+ package "/driver/virtio/viorand" // Virtio RNG
+}
+
+packages if="build" {
+ package "/developer/build-essential"
+ package "/ooce/developer/omnios-build-tools"
+ package "/developer/build/gnu-make"
+}
+
+packages if="rust" {
+ package "/ooce/developer/rust"
+ package "/developer/versioning/git"
+}
+
+OmniOS VM images need console and boot configuration through overlays:
+overlays {
+ file destination="/boot/conf.d/console" source="files/boot_console.115200"
+ file destination="/etc/ttydefs" source="files/ttydefs.115200"
+ file destination="/etc/default/init" source="files/default_init.utc"
+}
+
+The boot console file configures which console device is used:
+ttya: Serial console (standard for cloud VMs)text: Framebuffer (for interactive debugging)overlays {
+ ensure-symlink "/etc/svc/profile/generic.xml" target="generic_limited_net.xml"
+ ensure-symlink "/etc/svc/profile/inetd_services.xml" target="inetd_generic.xml"
+ ensure-symlink "/etc/svc/profile/platform.xml" target="platform_none.xml"
+ ensure-symlink "/etc/svc/profile/name_service.xml" target="ns_dns.xml"
+}
+
+For OmniOS, always use ZFS with UEFI:
+target "vm" kind="qcow2" {
+ disk-size "2G"
+ bootloader "uefi"
+ filesystem "zfs"
+ pool {
+ property name="ashift" value="12"
+ }
+}
+
+ashift=12: 4K sector alignment, correct for modern disks and virtual storageSee images/omnios-bloody-disk.kdl in the repository for a full bootable OmniOS image spec, or the Example Specs chapter.
The extra.omnios publisher provides community-maintained packages not in the core repository, including:
/ooce/developer/rust)/ooce/developer/go)/ooce/runtime/python-*)/ooce/runtime/node-*)/ooce/developer/omnios-build-tools)Always add this publisher if you need development tools or language runtimes.
+ +Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Ubuntu is Forger's secondary target, providing Linux support for teams that need both illumos and Linux images from the same tooling.
+| Aspect | OmniOS | Ubuntu |
|---|---|---|
| Bootstrap | pkg image-create | debootstrap |
| Package manager | IPS (pkg) | APT (apt) |
| Repository config | Publishers | sources.list |
| Default filesystem | ZFS | ext4 |
| Bootloader | UEFI (illumos) | grub-efi-amd64-bin |
| Init system | SMF | systemd |
metadata name="ubuntu-base" version="1.0.0" description="Ubuntu 22.04 base"
+
+distro "ubuntu-22.04"
+
+repositories {
+ apt-mirror "http://archive.ubuntu.com/ubuntu" suite="jammy" components="main universe"
+}
+
+packages {
+ package "openssh-server"
+ package "curl"
+}
+
+target "vm" kind="qcow2" {
+ disk-size "8G"
+ bootloader "grub-efi-amd64-bin"
+ filesystem "ext4"
+}
+
+distro "ubuntu-22.04" is required — without it, Forger assumes OmniOSgrub-efi-amd64-bin — the Ubuntu GRUB EFI packageext4 — ZFS is possible but not the Ubuntu defaultUbuntu uses APT mirrors with suite and component selection:
+repositories {
+ apt-mirror "http://archive.ubuntu.com/ubuntu" suite="jammy" components="main universe"
+ apt-mirror "http://archive.ubuntu.com/ubuntu" suite="jammy-updates" components="main universe"
+ apt-mirror "http://archive.ubuntu.com/ubuntu" suite="jammy-security" components="main universe"
+}
+
+For most server images, main universe covers all needed packages.
packages {
+ package "openssh-server"
+ package "curl"
+ package "git"
+ package "cloud-init"
+ package "linux-image-generic"
+}
+
+++Important: Include
+linux-image-genericfor bootable VM images. Without a kernel, the image won't boot.
packages if="build" {
+ package "build-essential"
+ package "pkg-config"
+ package "libssl-dev"
+}
+
+packages if="rust" {
+ package "rustc"
+ package "cargo"
+ package "build-essential"
+ package "libssl-dev"
+ package "pkg-config"
+}
+
+Ubuntu builds need an Ubuntu builder VM. Specify it explicitly or let Forger use the default:
+builder {
+ image "oci://ghcr.io/cloudnebulaproject/ubuntu-builder:latest"
+ vcpus 4
+ memory 4096
+ disk 20
+}
+
+metadata name="ubuntu-rust-ci" version="1.0.0" description="Ubuntu Rust CI image"
+
+distro "ubuntu-22.04"
+
+repositories {
+ apt-mirror "http://archive.ubuntu.com/ubuntu" suite="jammy" components="main universe"
+}
+
+packages {
+ package "build-essential"
+ package "rustc"
+ package "cargo"
+ package "git"
+ package "curl"
+ package "openssh-server"
+ package "cloud-init"
+ package "linux-image-generic"
+ package "grub-efi-amd64-bin"
+ package "libssl-dev"
+ package "pkg-config"
+}
+
+customization {
+ user "ci"
+}
+
+builder {
+ image "oci://ghcr.io/cloudnebulaproject/ubuntu-builder:latest"
+ vcpus 4
+ memory 4096
+ disk 20
+}
+
+target "vm" kind="qcow2" {
+ disk-size "8G"
+ bootloader "grub-efi-amd64-bin"
+ filesystem "ext4"
+ push-to "ghcr.io/cloudnebulaproject/ubuntu-rust:latest"
+}
+
+A long-term goal of the Forger project is to bring IPS to Linux via a Rust implementation. This would allow Linux images to use the same publisher-based, signed-package, incorporation-constrained model that makes illumos packaging robust. When this is available, Ubuntu and other Linux distros will gain IPS as an alternative package manager option within Forger.
+ +Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Artifact targets produce a tar archive of the assembled rootfs. This is the simplest output format and serves as the building block for multi-stage pipelines.
+target "archive" kind="artifact" {
+}
+
+The primary use of artifacts is as intermediate outputs in a multi-stage pipeline. A parent spec produces an artifact that a child spec consumes:
+// base.kdl — produces artifact
+target "base-archive" kind="artifact" {
+}
+
+// disk.kdl — consumes artifact from base
+base "base.kdl"
+
+target "vm" kind="qcow2" {
+ disk-size "8G"
+ bootloader "uefi"
+}
+
+Artifacts can be consumed by external tools for further processing:
+Build an artifact to inspect what the rootfs looks like without committing to a disk image:
+forger build --spec my-image.kdl --target archive
+tar -tzf output/archive.tar.gz | head -50
+
+The artifact is written to the output directory as a tar archive (typically gzip-compressed). The archive contains the full rootfs tree starting from /.
Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+OCI targets produce container images compatible with Docker, Podman, and any OCI-compliant runtime.
+target "container" kind="oci" {
+ entrypoint command="/bin/sh"
+ environment {
+ set "PATH" "/usr/bin:/bin:/usr/sbin:/sbin"
+ set "TZ" "UTC"
+ }
+}
+
+Phase 2 for an OCI target:
+output/container/
+├── oci-layout # {"imageLayoutVersion": "1.0.0"}
+├── index.json # Points to manifest
+└── blobs/
+ └── sha256/
+ ├── <manifest> # OCI manifest JSON
+ ├── <config> # OCI config JSON
+ └── <layer> # Compressed rootfs layer
+
+The command to run when the container starts:
+entrypoint command="/usr/sbin/sshd"
+
+If omitted, no entrypoint is set and the container runtime's default applies.
+environment {
+ set "PATH" "/usr/bin:/bin:/usr/sbin:/sbin"
+ set "LANG" "C.UTF-8"
+ set "TZ" "UTC"
+}
+
+# From OCI Image Layout directory
+docker load < output/container/
+
+# Or use skopeo
+skopeo copy oci:output/container docker-daemon:myimage:latest
+
+podman load < output/container/
+
+Use the push command or push-to in the target:
forger push --image output/container/ --reference ghcr.io/myorg/myimage:latest
+
+Or configure auto-push in the spec (see OCI Registry Push).
+OmniOS in a container is useful for CI builds and lightweight services:
+metadata name="omnios-container" version="1.0.0"
+
+repositories {
+ publisher name="omnios" origin="https://pkg.omnios.org/bloody/core/"
+}
+
+incorporation "entire"
+
+variants {
+ set name="opensolaris.zone" value="nonglobal"
+}
+
+packages {
+ package "/editor/vim"
+ package "/web/curl"
+}
+
+target "container" kind="oci" {
+ entrypoint command="/bin/bash"
+ environment {
+ set "PATH" "/usr/bin:/bin:/usr/sbin:/sbin"
+ }
+}
+
+Note: For containers, use opensolaris.zone=nonglobal to exclude kernel modules and hardware drivers that aren't needed in a container context.
Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+QCOW2 (QEMU Copy-On-Write v2) is the primary output format for bootable virtual machine images.
+Phase 2 for a QCOW2 target follows this sequence:
+qemu-img convertZFS is the default and recommended filesystem for OmniOS images:
+target "vm" kind="qcow2" {
+ disk-size "2G"
+ bootloader "uefi"
+ filesystem "zfs"
+ pool {
+ property name="ashift" value="12"
+ }
+}
+
+During build, Forger creates a uniquely-named ZFS pool (e.g., forgebuild_12345) to avoid conflicts with existing pools on the build host. After export, the pool is named rpool in the final image.
pool {
+ property name="ashift" value="12"
+}
+
+ashift=12: 4KB sector alignment. Use this for modern storage and virtual disks.ZFS images use boot environments (BEs), a core illumos concept. The image contains a single BE that becomes the default boot target. On first boot, the system can create new BEs for upgrades, allowing rollback to previous states.
+ext4 is the default filesystem for Ubuntu images:
+target "vm" kind="qcow2" {
+ disk-size "8G"
+ bootloader "grub-efi-amd64-bin"
+ filesystem "ext4"
+}
+
+The disk is partitioned with an EFI System Partition and a root partition formatted as ext4.
+Specify the disk size as a string with a unit suffix:
+disk-size "2G" // 2 gigabytes
+disk-size "2000M" // 2000 megabytes
+disk-size "8G" // 8 gigabytes
+
+Choose a size that accommodates your installed packages plus reasonable free space. Typical sizes:
+| Value | Platform | Description |
|---|---|---|
uefi | illumos | Native UEFI boot (recommended for OmniOS) |
grub | illumos | Legacy GRUB (BIOS boot) |
grub-efi-amd64-bin | Ubuntu | GRUB EFI for x86_64 Linux |
QCOW2 images can be automatically pushed to an OCI registry as artifacts:
+target "vm" kind="qcow2" {
+ disk-size "8G"
+ bootloader "uefi"
+ filesystem "zfs"
+ push-to "ghcr.io/myorg/omnios-image:latest"
+}
+
+The QCOW2 file is wrapped as an OCI artifact with custom media types (application/vnd.cloudnebula.qcow2.*) and pushed to the registry. This allows distributing VM images through container registries.
QCOW2 images work with:
+qemu-system-x86_64 -drive file=image.qcow2,format=qcow2)To convert to raw (for AWS, DigitalOcean, etc.):
+qemu-img convert -f qcow2 -O raw image.qcow2 image.raw
+
+
+ Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Forger can push built images directly to OCI-compliant registries, including GitHub Container Registry (GHCR), Docker Hub, and self-hosted registries.
+Set push-to on a target to automatically push after a successful build:
target "vm" kind="qcow2" {
+ disk-size "8G"
+ bootloader "uefi"
+ push-to "ghcr.io/myorg/omnios-image:latest"
+}
+
+target "container" kind="oci" {
+ push-to "ghcr.io/myorg/omnios-container:latest"
+}
+
+Skip the push with:
+forger build --spec my-image.kdl --skip-push
+
+Push a previously built image:
+# Push OCI Image Layout
+forger push --image output/container/ --reference ghcr.io/myorg/myimage:latest
+
+# Push QCOW2 as OCI artifact
+forger push --image output/vm.qcow2 --reference ghcr.io/myorg/myvm:latest --artifact
+
+| Flag | Description |
|---|---|
--image <PATH> | Path to OCI Image Layout directory or QCOW2 file |
--reference <REF> | Registry reference (e.g., ghcr.io/org/image:tag) |
--artifact | Push QCOW2 as OCI artifact (custom media types) |
--auth-file <PATH> | JSON auth file for registry authentication |
Forger automatically uses the GITHUB_TOKEN environment variable when pushing to ghcr.io:
export GITHUB_TOKEN=ghp_...
+forger build --spec my-image.kdl
+
+In GitHub Actions, the token is available automatically:
+- name: Build and push
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: forger build --spec images/omnios-rust-ci.kdl
+
+For other registries, provide a JSON auth file:
+{
+ "username": "myuser",
+ "password": "mypassword"
+}
+
+Or with a token:
+{
+ "token": "my-registry-token"
+}
+
+forger push --image output/container/ \
+ --reference registry.example.com/myimage:latest \
+ --auth-file auth.json
+
+Local registries (localhost, 127.0.0.1) are accessed without authentication over HTTP (insecure mode).
+When pushing QCOW2 images with --artifact, Forger uses custom OCI media types:
application/vnd.cloudnebula.qcow2.config.v1+jsonapplication/vnd.cloudnebula.qcow2.layer.v1This allows distributing VM disk images through container registries alongside container images, using a unified registry infrastructure.
+QCOW2 artifacts pushed to a registry can be pulled back as builder images or for deployment:
+builder {
+ image "oci://ghcr.io/myorg/omnios-builder:latest"
+}
+
+Forger resolves oci:// references by pulling from the registry.
Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Forger supports two build modes: local and remote. The mode is selected automatically based on your environment, or you can force it with CLI flags.
+A local build runs directly on your host machine. This is the fastest option but requires:
+forger build --spec my-image.kdl --local
+
+Use --local to skip builder VM detection and force a local build.
When your host doesn't match the target OS — for example, building OmniOS images from a Linux workstation — Forger spins up an ephemeral builder VM:
+forger binary, spec file, and overlay files via SSHforger build --spec my-image.kdl --use-builder
+
+Use --use-builder to force a remote build even when local build is possible.
If no builder is specified in the spec or on the CLI, Forger uses sensible defaults:
+| Target Distro | Default Builder Image |
|---|---|
| OmniOS | oci://ghcr.io/cloudnebulaproject/omnios-builder:latest |
| Ubuntu | oci://ghcr.io/cloudnebulaproject/ubuntu-builder:latest |
From the CLI:
+forger build --spec my-image.kdl --builder-image oci://my-registry/my-builder:v1
+
+Or in the spec file (see Builder Configuration).
+When neither --local nor --use-builder is specified, Forger checks whether the current host can satisfy the build requirements (package manager availability, OS match). If it can, it builds locally. Otherwise, it falls back to a builder VM.
Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Let's build a minimal OmniOS VM image. Create a file called my-image.kdl:
metadata name="my-first-image" version="1.0.0" description="A minimal OmniOS image"
+
+repositories {
+ publisher name="omnios" origin="https://pkg.omnios.org/bloody/core/"
+ publisher name="extra.omnios" origin="https://pkg.omnios.org/bloody/extra/"
+}
+
+incorporation "entire"
+
+certificates {
+ ca publisher="omnios" certfile="omniosce-ca.cert.pem"
+}
+
+variants {
+ set name="opensolaris.zone" value="global"
+}
+
+packages {
+ package "/editor/vim"
+ package "/network/openssh-server"
+ package "/network/rsync"
+}
+
+target "vm" kind="qcow2" {
+ disk-size "2G"
+ bootloader "uefi"
+ filesystem "zfs"
+ pool {
+ property name="ashift" value="12"
+ }
+}
+
+Before building, validate the spec to catch syntax errors:
+forger validate --spec my-image.kdl
+
+See what the build will do after resolving includes and applying profiles:
+forger inspect --spec my-image.kdl
+
+Check what targets are defined:
+forger targets --spec my-image.kdl
+
+Output:
+vm (qcow2)
+
+forger build --spec my-image.kdl
+
+This will:
+The output lands in ./output/ by default.
If your spec defines multiple targets, build just one:
+forger build --spec my-image.kdl --target vm
+
+Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Forger is written in Rust and builds as a single static binary. You need:
+For local builds (building images directly on your machine), you also need:
+pkg on OmniOS, debootstrap + apt on Ubuntu)qemu-img for QCOW2 conversionFor remote builds (the default when your host doesn't match the target), you only need:
+git clone https://github.com/cloudnebulaproject/refraction-forger.git
+cd refraction-forger
+cargo build --release
+
+The binary is at target/release/forger.
If you're building on Linux for deployment on illumos:
+# Install the cross-compilation tool
+cargo install cross
+
+# Build for illumos
+cross build --release --target x86_64-unknown-illumos
+
+The project includes a Cross.toml with the illumos target preconfigured.
forger --help
+
+You should see the five subcommands: build, validate, inspect, push, and targets.
Press ← or → to navigate between chapters
+Press S or / to search in the book
+Press ? to show this help
+Press Esc to hide this help
+Refraction Forger is a declarative image building tool that creates optimized OS images from simple specification files. It is designed for infrastructure engineers, DevOps teams, and OS distribution maintainers who need reproducible, cacheable image builds across multiple platforms.
+Forger reads a .kdl specification file that declares what your image should contain — packages, files, users, boot configuration — and produces ready-to-deploy artifacts:
Built artifacts can be pushed directly to OCI registries like GHCR.
+Traditional image building tools fall into two camps:
+image-builder) that require a matching host OS, root privileges, and careful manual sequencing.Forger takes a different approach. It assembles images directly by calling package managers and filesystem tools, skipping the installer entirely. Builds that took 20+ minutes with Packer complete in a fraction of the time.
+When your host doesn't match the target OS, Forger automatically spins up an ephemeral builder VM, transfers the spec, builds inside it, and retrieves the artifacts — no manual VM management needed.
+Forger's primary focus is illumos distributions, particularly OmniOS. The illumos ecosystem uses IPS (Image Packaging System) and ZFS, both of which Forger understands natively.
+Linux support (starting with Ubuntu) is included as a secondary target. The long-term goal is to bring IPS to Linux via a Rust implementation, making Forger's packaging model available across operating systems. In the meantime, popular Linux distributions are supported to provide a broad userbase familiar with tools like Packer.
+omnios-image-builder or Packer.