Keyboard shortcuts

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

Getting Started

Recipes and build profiles are written in Nickel. Basic familiarity with the language will help you follow the examples below.

Download the recipes

In your working directory, download the bobr-recipes release matching your installed Bobr:

BOBR_VERSION="$(bobr --version | awk '{print $2}')"
curl -fLO "https://github.com/aleksey-makarov/bobr-recipes/archive/refs/tags/v${BOBR_VERSION}.tar.gz"
tar -xf "v${BOBR_VERSION}.tar.gz"
mv "bobr-recipes-${BOBR_VERSION}" bobr-recipes

Build and boot a QEMU system

Create bobr.ncl beside bobr-recipes with this content:

let bobrpkgs = import "bobr-recipes/bobrpkgs.ncl" in
let pkgs = bobrpkgs [] in
(import "bobr-recipes/profiles/bobr-user.ncl") & {
  include pkgs,
  goals = [pkgs.qemu_image, pkgs.initrd, pkgs.linux_bzimage],
  secondaries.providers = [
    import "bobr-recipes/profiles/provider-public.ncl",
  ],
}

bobr.ncl is your build profile. bobr-build.sh evaluates it with Nickel and generates a JSON request for Bobr. This profile imports the package set and user defaults, selects three boot artifacts as goals, and enables a repository of prebuilt objects to speed up the build.

mkdir -p bobr-store
bobr-recipes/bin/bobr-build.sh

This produces the EROFS root filesystem image, initramfs, and kernel. You can find them through the qemu-image, initrd, and linux-bzimage links in bobr-store/object-refs/. Allow roughly 1–2 minutes to download the prebuilt objects, depending on your connection.

To run the script, qemu-system-x86_64 and mkfs.ext4 must be on PATH, and your user must have access to /dev/kvm on an x86-64 Linux host:

bobr-recipes/tools/bobr-run-qemu.sh

The script finds the boot artifacts in the adjacent bobr-store and creates bobr-home.img beside it on first launch: a persistent 1 GiB ext4 disk for /home. It reuses that disk on subsequent launches.

The terminal becomes the guest’s root console, with automatic login. Shut down with poweroff; press Ctrl-A, X to exit QEMU directly. Changes under /home persist in bobr-home.img; changes elsewhere are discarded on reboot.

Build and boot a GNOME system

Using the same profile, build the GNOME image:

bobr-recipes/bin/bobr-build.sh --target qemu_gnome_image

Alternatively, change the profile’s goals to [pkgs.qemu_gnome_image] and run without --target.

Your host QEMU must support SDL/OpenGL and virtio GPU with Venus; the host also needs a working Vulkan driver. See QEMU graphics requirements.

Launch GNOME:

bobr-recipes/tools/bobr-run-qemu-gnome.sh

The desktop opens in a window. The script reuses bobr-home.img; do not run both guests simultaneously.

The systems booted above are still proof-of-concept demos, with much left to improve. For now, Bobr’s primary focus is the build system rather than a complete OS.

Boot QEMU from a host bundle

You can also run programs built by Bobr on your host using a host bundle, which packages a program with its runtime dependencies. Host bundles are still a proof of concept, but host_bundle_qemu includes QEMU and the boot images, so no host QEMU installation is needed. Access to /dev/kvm is still required.

Build and run directly from the store:

bobr-recipes/bin/bobr-build.sh --target host_bundle_qemu
bobr-store/object-refs/host-bundle-qemu/bin/bobr-run-qemu

The runner creates a persistent home.img in the current directory. Use --help for options.

host_bundle_qemu_gnome is also available, but it is a large download and requires a working Vulkan driver on the host.

Build a specific target

List the available targets:

bobr-recipes/bin/bobr-list-pkgs.sh

Build gzip using your existing profile:

bobr-recipes/bin/bobr-build.sh --target gzip

Inspect its filesystem-tree manifest:

cat bobr-store/object-refs/gzip-1.14

The list shows the attribute used by --target and the recipe name used in object-refs/.

The manifest lists paths, file metadata, and content hashes. Regular file contents are stored separately in bobr-store/fs-files/, shared between trees with identical files. The manifest itself does not contain those bytes.

For details, see Filesystem trees.

Reuse results from another local store

After building gzip, create reuse.ncl beside bobr.ncl:

let bobrpkgs = import "bobr-recipes/bobrpkgs.ncl" in
let pkgs = bobrpkgs [] in
(import "bobr-recipes/profiles/bobr-user.ncl") & {
  include pkgs,
  goals = [pkgs.gzip],
  store = "bobr-store.new",
  secondaries.providers = [
    {
      name = "previous",
      mappings = true,
      content = true,
      local = {
        store = "bobr-store",
        transfer = "hardlink",
      },
    },
  ],
}

Build into a new store using this profile:

mkdir bobr-store.new
bobr-recipes/bin/bobr-build.sh reuse.ncl

Bobr finds the build mapping in the previous store and imports the result using hardlinks, without downloading or rebuilding. Both stores must be on the same filesystem.

The result is available through bobr-store.new/object-refs/. Your original bobr.ncl remains unchanged.

Add a recipe with an overlay

An overlay adds or changes recipes without editing bobr-recipes, following the same model as Nix overlays. The package set is defined as a fixed point: recipes refer to the final set, including all overlay changes. Each overlay receives final, that final set, and prev, the set before this overlay, and returns the fields to add or replace.

Create overlay.ncl beside bobr.ncl:

fun final => fun prev => {
  greeting = {
    name = "greeting",
    tag = "Tree",
    config.tree.entries = [
      {
        type = "file",
        path = "etc/greeting.txt",
        text = "Hello from my overlay!\n",
        executable = false,
      },
    ],
    inputs = {},
  },
}

tag selects the builder; Tree in Request describes how this builder creates files and directories from inline data. The nested path makes the result a directory containing etc/greeting.txt.

In bobr.ncl, replace:

let pkgs = bobrpkgs [] in

with:

let pkgs = bobrpkgs [import "./overlay.ncl"] in

Build the new target and read the resulting file:

bobr-recipes/bin/bobr-build.sh --target greeting
cat bobr-store/object-refs/greeting/etc/greeting.txt

Change text in the overlay and build again to update the file.

Import a file into an fs-tree

Add another recipe inside the record returned by overlay.ncl, alongside greeting:

greeting = {
  # Keep the greeting recipe from the previous example.
},
greeting_tree = {
  name = "greeting-tree",
  tag = "FsTreeImport",
  config.install.rules = [
    {
      path = "**",
      attrs = {
        uid = 0,
        gid = 0,
        directory_mode = 0o755,
        regular_file_mode = 0o644,
        executable_file_mode = 0o755,
      },
    },
  ],
  inputs = { input = final.greeting },
},

FsTreeImport imports the directory into an fs-tree, assigning root ownership and file mode 0644. final.greeting refers to the recipe from the final overlaid set.

Build and inspect the manifest:

bobr-recipes/bin/bobr-build.sh --target greeting_tree
cat bobr-store/object-refs/greeting-tree

The manifest records the etc/greeting.txt path, ownership, mode, and fs-file hash. The file contents are stored separately in bobr-store/fs-files/.

The files in fs-files/ already carry their required ownership and permissions. Bobr can assemble a usable filesystem by hardlinking them into the layout described by an fs-tree manifest.

Compile a local program

Create hello-src/ beside bobr.ncl, containing these two files.

hello.c:

#include <stdio.h>

int main(void)
{
    puts("Hello from Bobr!");
    return 0;
}

Makefile:

.RECIPEPREFIX := >

hello: hello.c
>$(CC) $(CFLAGS) -o $@ $<

install: hello
>mkdir -p /usr/bin
>install -m 0755 hello /usr/bin/hello

.PHONY: install

Record the source directory’s absolute path and content hash:

realpath hello-src > hello-source.path
bobr-fsobj-hash hello-src > hello-source.fsobj-hash

Create hello.ncl beside bobr.ncl:

let hello_source = {
  name = "hello-source",
  tag = "Source",
  object_hash =
    std.string.trim (import "./hello-source.fsobj-hash" as 'Text),
  origin = {
    tag = "Path",
    path = std.string.trim (import "./hello-source.path" as 'Text),
  },
}
in
fun pkgs => {
  name = "hello",
  tag = "Makefile",
  deps = {
    build = [],
    runtime = [pkgs.c_runtime],
  },
  config = {},
  inputs.source = hello_source,
}

In bobr.ncl, replace the goals line with:

goals = [(import "./hello.ncl") pkgs],

Goals can refer directly to recipes outside the package set; no overlay is needed to build this recipe.

Build, inspect the stored sources, and read the resulting fs-tree manifest:

bobr-recipes/bin/bobr-build.sh
ls bobr-store/object-refs/hello-source/
cat bobr-store/object-refs/hello

hello-source points to the source directory stored by Bobr; hello points to the resulting fs-tree manifest.

The first compilation requires a substantial download of the build environment. The recipe runs make and make install, producing an fs-tree containing /usr/bin/hello.

After changing either source file, update the source hash before rebuilding:

bobr-fsobj-hash hello-src > hello-source.fsobj-hash

Add a library dependency

A recipe’s result contains only the files installed by that recipe. Bobr does not inspect compiled programs to infer their dependencies. deps.build adds tools, headers, and libraries to the build environment; deps.runtime declares what must accompany the result when it is included in a root filesystem.

Make hello use zlib by replacing hello.c with:

#include <stdio.h>
#include <zlib.h>

int main(void)
{
    printf("Hello from Bobr with zlib %s!\n", zlibVersion());
    return 0;
}

Add -lz to the link command in Makefile:

>$(CC) $(CFLAGS) -o $@ $< -lz

Then change deps in hello.ncl:

deps = {
  build = [pkgs.zlib],
  runtime = [pkgs.c_runtime, pkgs.zlib],
},

pkgs.zlib in build provides zlib.h and the library used by the linker. The entries in runtime cause the C runtime and zlib to accompany hello when the next section adds it to the QEMU root filesystem.

Update the source hash and rebuild:

bobr-fsobj-hash hello-src > hello-source.fsobj-hash
bobr-recipes/bin/bobr-build.sh

Add the program to a QEMU system

Add these fields to the record returned by overlay.ncl:

greeting = {
  # Keep the greeting recipe from the previous example.
},
greeting_tree = {
  # Keep the greeting_tree recipe from the previous example.
},
hello = (import "./hello.ncl") final,
qemu_rootfs = prev.qemu_rootfs & {
  inputs.hello = final.hello,
},

This adds hello and its runtime dependencies to the image’s root filesystem.

Rebuild the image and boot:

bobr-recipes/bin/bobr-build.sh --target qemu_image
bobr-recipes/tools/bobr-run-qemu.sh

Allow several minutes (about six in a test run): changing the rootfs requires downloading its constituent objects and repacking the EROFS image. In the guest, run:

hello

It prints Hello from Bobr with zlib 1.3.1!; the version is the one selected by the recipes.

Rebuild world using repository sources

Use a new working directory containing bobr-recipes, or remove the existing bobr-store and bobr.ncl. Removing the store deletes all previous build results.

Create bobr.ncl:

let bobrpkgs = import "bobr-recipes/bobrpkgs.ncl" in
let pkgs = bobrpkgs [] in
(import "bobr-recipes/profiles/bobr-user.ncl") & {
  include pkgs,
  goals = [pkgs.world],
  secondaries.providers = [
    (import "bobr-recipes/profiles/provider-public.ncl") & {
      mappings = false,
    },
  ],
}

The repository supplies content whose hash is already known, such as source archives. Build-result mappings are disabled, so Bobr builds from source rather than downloading prebuilt results. Missing sources are fetched from their configured origins.

This is useful if you trust the source hashes in the recipes but do not want to trust prebuilt Bobr results. The build still relies on a prebuilt bootstrap environment. You can independently verify those source hashes by downloading the sources from upstream and running bobr-fsobj-hash --mode=direct. For non-executable files, this produces the same hash as sha256sum.

mkdir bobr-store
bobr-recipes/bin/bobr-build.sh

Expect several gigabytes of downloads and many hours of compilation. world builds all shipped images and host bundles.

Build world without secondary stores

Again, use a new working directory containing bobr-recipes, or remove the existing bobr-store and bobr.ncl.

Create bobr.ncl:

let bobrpkgs = import "bobr-recipes/bobrpkgs.ncl" in
let pkgs = bobrpkgs [] in
(import "bobr-recipes/profiles/bobr-user.ncl") & {
  include pkgs,
  goals = [pkgs.world],
  secondaries.providers = [],
}

Sources come from upstream servers or local recipe files.

This bypasses the prebuilt repository. You can also build Bobr itself from source instead of using its prebuilt binaries; see Development.

mkdir bobr-store
bobr-recipes/bin/bobr-build.sh

Expect several gigabytes of downloads and many hours of compilation. Subsequent runs reuse results in this store.

Next steps

  • Concepts — content addressing, objects, keys, and recipes.
  • Request — the request format and the built-in builders.
  • Recipes in Nickel — authoring recipes.
  • HostBundle — relocatable host-side application bundles.
  • Store — how results are stored, named, and reused.