Request
bobr reads a JSON document (described below) from standard input or from a
file named on the command line. The document is a request: it describes a
recipe DAG and its goals. bobr realizes every goal and prints the result: one
goal prints its ObjectHash; several goals print an ordered JSON array of
{ "node", "object_hash" } records. For the model behind requests — objects,
recipes, keys — see Concepts.
The request is a single JSON object:
{
"schema": "bobr-request-v6",
"store": "/abs/path/to/store",
"logs": "/abs/path/to/logs/260803120000",
"work": "/abs/path/to/work/260803120000",
"run_id": "260803120000",
"quiet": false,
"jobs": 8,
"progress": { "mode": "auto" },
"limits": {
"per_host_default": 6,
"max_connections": 64,
"max_local_jobs": 4
},
"secondaries": {
"repository_cache": "/abs/path/to/store/repository-cache",
"providers": []
},
"goals": ["root"],
"nodes": {
"root": { "...": "..." }
}
}
schema— format version; must be"bobr-request-v6".bobr --build-inforeports the schema this build accepts in machine-readable form, so a recipe layer can check compatibility before building rather than finding out from the parse error;bobr --versionshows the same schema together with human-readable build provenancestore— the store root for this request: an absolute path to an existing directory (see Store)logs— this run’s log directory: an absolute path to an existing directory, whichbobrfills with the run log and one subdirectory per subject (see Build logging)work— this run’s work directory: an absolute path to an existing directory, whichbobrfills with one scratch directory per subjectrun_id— what to call this run; recorded in the object records it writes and in each subject’smeta.json. It must start with an ASCII letter or digit and may contain only ASCII letters, digits,.,_, and-, up to 64 charactersquiet— optional bool; suppress the live progress logjobs— optional positive integer; limit on parallel builder executionprogress— optional terminal presentation policy; defaults to{ "mode": "auto" }. The live block always has fetch statistics, build statistics, and a run summary;summaryhides only its individual activity rows.fixedrequiresmax_lines >= 4and caps the complete live block. This field never affects build identity and is ignored for non-TTY outputlimits— optional shared Source/repository network and local-I/O limits, described belowsecondaries— optional ordered mapping and content providers, described belowgoals— ordered, non-empty array of distinct, existing node ids to realize; its order determines the order of multi-goal results, not execution ordernodes— the recipe DAG
bobr neither names the run nor creates its two directories: the caller does
both, which is also what keeps two runs from writing into one another. bobr
writes into what it is given, and refuses to share it: the run’s events.jsonl
is created exclusively, so a second run pointed at a log directory already in use
(or left by an earlier run) fails immediately instead of interleaving with it.
The work directory must be on the same filesystem as the store. Builders
stage their output there, and the store publishes it by renaming it into
objects/ and hardlinking its files into fs-files/; neither crosses a
filesystem boundary. bobr checks this before building rather than failing on
the first import. The log directory has no such constraint.
The recipe DAG is a JSON object: each member’s value is a recipe. Inputs refer
to other members by node id. Only nodes reachable from goals are planned;
duplicate build identities are shared.
Acquisition limits
All limits fields are optional; every numeric limit, including each
per_host value, must be positive. Omitted fields use bobr’s defaults. A typical
resolved configuration is:
{
"per_host_default": 6,
"per_host": { "gitlab.freedesktop.org": 2 },
"max_connections": 64,
"max_local_jobs": 4
}
per_host_default— maximum simultaneous Source and repository network operations on one host whenper_hosthas no entry for it; this includes HTTP/OCI downloads and repository metadata and content fetchesper_host— per-host overrides of that limitmax_connections— total maximum simultaneous Source and repository network operations; the default is one quarter of the process’s soft file-descriptor limit, clamped to1..64max_local_jobs— one shared bound for localPathsource materialization, Source import and hashing, local-repository content transfer and verification, downloaded repository fs-file publication, and builder input path preparation; it limits disk work independently of network connections and builder slots
Secondary providers
secondaries.providers is an ordered list of independent mapping and content
capabilities. The low-level request contains one capability per entry; a
profile-level repository that enables both is lowered into two consecutive
entries backed by the same source.
repository_cache is an absolute path shared by all remote backends. If it is
omitted, it defaults to <store>/repository-cache.
A local read-only store can provide mappings, content, or both:
{
"repository_cache": "/mnt/bobr/store/repository-cache",
"providers": [
{
"name": "previous",
"capability": "mappings",
"backend": {
"kind": "local",
"store": "/mnt/bobr/previous-store"
}
},
{
"name": "previous",
"capability": "content",
"backend": {
"kind": "local",
"store": "/mnt/bobr/previous-store",
"transfer": "hardlink"
}
}
]
}
capability = "mappings"permits authoritativeBuildKeyandReuseKeyanswers. It carries no content transfer policy.capability = "content"permits acquisition by an already-knownObjectHash. A local content backend requirestransfer: either"hardlink"or"copy"."hardlink"shares regular-file inodes; directory structure and symlinks are reproduced without changing their logical contents."copy"gives ordinary objects and every fs-file in an fs-tree closure independent working-store inodes while preserving the metadata that participates in their identities.
Mapping and content lookup remain separate: bobr may learn a candidate hash from one backend and obtain its bytes from another. Providers are tried in declared order within each capability.
Repository roots must exist and contain a complete bobr store layout. Paths are
canonicalized when opened: a repository cannot alias the working store, and the
same canonical repository root cannot be repeated within one capability.
Complementary entries may share it, and entries with the same name must do so.
hardlink requires the repository and working store objects/ directories to
share a filesystem, and likewise requires their fs-files/ directories to
share a filesystem. These pairs are checked separately before realization
starts because either directory may be a mount point. There is no copy fallback
after a hardlink-policy error. copy has no shared-filesystem requirement and
always creates independent regular-file inodes, even on the same filesystem;
it never silently optimizes the transfer into hardlinks. Both modes verify
content identity and publish only a complete ordinary object or fs-tree
closure.
A remote backend has this normalized shape:
{
"name": "public",
"capability": "mappings",
"backend": {
"kind": "remote",
"master_url": "https://repo.example/bobr/master",
"trusted_keys": ["/etc/bobr/repository-key.pem"],
"ca_bundle": "/etc/bobr/repository-ca.pem"
}
}
master_url must be an absolute HTTPS URL without credentials, query, or
fragment. trusted_keys is a non-empty array of absolute paths to pinned
Ed25519 public keys. ca_bundle is optional and adds private roots to the
normal HTTPS roots. Entries that repeat one URL must use identical trust and
TLS settings. Remote providers support both mapping lookup and verified content
downloads. They authenticate the signed repository master with the pinned keys
and fetch metadata lazily, caching it under repository_cache. Authenticating
the master does not enable either capability implicitly: each must be requested
explicitly.
Within each capability, provider names and physical backends are unique. The same name may occur once for mappings and once for content only when both entries use the same backend. This makes diagnostics stable and prevents an apparently combined provider from naming unrelated sources.
Repository configuration is required only while it is used. After successful import, complete content belongs to the working store; any mappings, records, and refs published for the selected result are working-store entries as well. A later offline request can omit the repository and reuse that content; removing the repository’s directory entries does not invalidate either copied files or hardlinked working-store names. Explicitly configured local stores and all provider settings are validated at startup; remote metadata is fetched only when that provider is needed.
A recipe for the Source builder has this shape:
{
"name": "linux-src",
"tag": "Source",
"object_hash": "0123…abcd",
"origin": {
"tag": "Http",
"url": ["https://example.invalid/linux.tar.xz"],
"unpack": true
}
}
name— a human-facing name for the resulttag— must be"Source"object_hash— theObjectHashthis source must produceorigin— how to obtain the object this recipe describes; defined below
A recipe for the Source builder may also omit origin. Then its object must
already be available from the working store or a configured content provider
under its object_hash; if it is not, the source fails.
An origin is the ordinary case in current bobr-recipes: unified bobr
acquires missing Source content lazily while realizing the same DAG. Omitting
it is useful for explicitly offline requests and requires the object to be
available from the working store or configured content providers.
A recipe for any other builder has this shape:
{
"name": "tar-1.35",
"tag": "Sandbox",
"config": {
"steps": [
{
"name": "build",
"run_as": "build-user",
"cwd": "@{build}",
"argv": ["@{script}", "build"]
}
]
},
"inputs": {
"_rootfs": "rootfs_1",
"script": "script_1",
"source": "src_0"
}
}
name— a human-facing name for the resulttag— the name of the builder that builds this recipeconfig— the builder’s configuration; its shape is defined by the builderinputs— dependencies keyed by input name; each value is the key of another member ofnodes— the recipe this one depends on
An input whose name begins with _ is materialized: its object must be a
filesystem tree and the builder receives the path to a real
directory of files. Any other input is passed as the object itself (for an
fs-tree, that is the manifest object). This rule is uniform across builders and
applies to both the slots a builder declares and any extra inputs it accepts.
Builders
Tree
Realizes an inline description of text files, symlinks, and directories as one object.
Inputs: none.
Config: a tree with a list of entries, each a file, dir, or
symlink:
{
"tree": {
"entries": [
{ "type": "dir", "path": "etc" },
{ "type": "symlink", "path": "bin", "target": "usr/bin" },
{ "type": "file", "path": "etc/hostname", "text": "bobr\n", "executable": false }
]
}
}
- a
fileentry carries UTF-8textand anexecutableflag - a
symlinkentry carries a literaltargetstring - a
direntry is an explicit directory
Behavior:
- parent directories for file entries are created automatically
- if the tree is exactly one top-level
fileentry, the result is a file object; otherwise it is an ordinary directory object installmetadata is not accepted here — useFsTreeImportto attach logical ownership and mode
Limitations: only UTF-8 text files, symlinks, and explicit directories; binary files and richer file-mode control are not yet supported.
Bundle
Collects an arbitrary number of file inputs into one directory object.
Inputs: one or more extra inputs. Each must be a regular file or a directory containing exactly one regular file at its top level. Other directory entries are ignored; symlinks cannot be passed as standalone inputs.
Config: none ({}).
Behavior:
- writes a plain directory object with one entry per input, a hardlink named by the input key (inputs are hardlinked, never copied); for a single-file directory input, the inner filename does not determine the output name
- the result is ordinary data with no ownership or mode identity — unlike the fs-tree builders; use it to hand a downstream build many inputs as a single directory (one bind mount) instead of one bind mount per input, e.g. to bundle vendored crate archives
FsTreeImport
Imports one ordinary directory object into an fs-tree, applying install rules that set logical ownership and mode.
Inputs: required input — one ordinary directory object. To import a file
object, first place it in a directory with Bundle.
Config: install.rules, evaluated in order; each rule is a glob path plus
attrs (any of uid, gid, directory_mode, regular_file_mode,
executable_file_mode):
{
"install": {
"rules": [
{
"path": "**",
"attrs": {
"uid": 0,
"gid": 0,
"directory_mode": 493,
"regular_file_mode": 420,
"executable_file_mode": 493
}
}
]
}
}
Behavior:
- imports the input contents into the store’s shared file storage and writes one fs-tree object
- later matching rules override earlier attributes field by field
- rules must provide
uid,gid, and the applicable mode for every entry, including the root directory (whose relative path is empty);"**"matches the root as well as its descendants - directory, regular-file, and executable-file modes come from install attributes; symlink mode is not represented
- runs as a namespace function, since importing needs namespace-root access to ownership metadata
FsTreeExport
The inverse of FsTreeImport: extracts selected entries out of an fs-tree into
an ordinary object. Its input is an fs-tree, passed as a plain object (so the
builder receives the manifest and pulls matched files from shared storage by
content hash — it does not materialize the whole tree).
Inputs: required input — one fs-tree object.
Config: copies, a non-empty ordered array of { from, to } commands:
{ "copies": [ { "from": "boot/bzImage", "to": "bzImage" },
{ "from": "usr/lib/*.so.1", "to": "libs" } ] }
fromis a glob (or literal path) matched against the fs-tree’s paths.tois the destination in the output object. For a literalfromnaming a single file or symlink,tois the exact output path (allowing rename). For a glob (or a literal directory),tois a directory and each match is placed under it preserving its path relative to the glob’s literal base (for a literal directory, relative to that directory). Directory entries never copy on their own — parent directories are created as needed.- A command that matches nothing, or two commands writing the same destination, is rejected.
Behavior:
- produces a plain-object directory: matched regular files (mode, including the
executable bit, preserved), matched symlinks recreated, all owned
0:0 - runs as a namespace function: reading arbitrary fs-files needs namespace-root (they carry their entries’ logical ownership and mode), and a plain object must be single-owner
TreeMerge
Merges two or more fs-trees into one, with strict conflict checking.
Inputs: two or more fs-tree inputs (as extra inputs), consumed in the standard input order (required inputs, then optional, then extra inputs in lexical name order).
Config: none ({}).
Behavior:
- reads the input manifests directly and does not materialize them
- overlapping directory paths are allowed only when
uid,gid, and mode match - duplicate file paths are allowed only when their
FsFileHashvalues match, including file bytes, ownership, and mode - duplicate symlink paths are allowed only when
uid,gid, and target match - file-vs-directory, symlink-vs-directory, and parent/child leaf conflicts are rejected
- writes one fs-tree object
TreeSubset
Produces an fs-tree containing only the paths that match its include patterns.
Inputs: required tree — one fs-tree.
Config: include, a non-empty list of glob patterns:
{
"include": [
"usr/lib64/libfoo.so*",
"usr/share/foo/**"
]
}
Behavior:
- reads the input manifest directly and does not materialize it
- matches
includeglobs against manifest paths; a matched file, symlink, or directory is included together with its parent directories - selecting a directory directly includes only that directory; recursive
selection needs a pattern such as
dir/** - individual patterns may match nothing, but the build fails if the final subset has no non-root paths
- rejects empty include lists, empty patterns, absolute patterns, and patterns
containing
.. - writes one fs-tree object
TreeMove
Re-roots an fs-tree at one of its subdirectories: the strip_prefix directory
becomes the new root and its leading path component is dropped from every nested
entry. A pure manifest operation — the same fs-files are referenced under shorter
paths.
Inputs: required tree — one fs-tree.
Config: strip_prefix, the subdirectory to promote to the root:
{ "strip_prefix": "stage" }
Behavior:
- reads the input manifest directly and does not materialize it
- promotes the
strip_prefixsubtree to the root — each entry keeps its content and metadata, with the leadingstrip_prefixcomponent removed — and writes one fs-tree object
HostBundle
Builds a verified, relocatable host-side application directory from an already materialized runtime root and a static launcher package. See HostBundle for the complete composition, verification, and runtime model.
Inputs:
- required
_root— the materialized payload fs-tree - required
_launcher— a materialized fs-tree containingusr/libexec/bobr-bundle-launcher - optional
overrides— an ordinary directory object copied under the bundle’soverrides/
Config:
{
"arch": "x86_64",
"policy": "strict",
"min_kernel": "4.19",
"library_dirs": ["usr/lib64", "usr/lib"],
"public_tools": {
"demo": {
"path": "usr/bin/demo",
"argv0": "demo",
"argument_prefix": [
{ "value": "--data-dir" },
{ "source": "payload", "path": "usr/share/demo" }
],
"environment": {
"DEMO_CONFIG": {
"operation": "replace",
"paths": [
{ "source": "payload", "path": "etc/demo/config.toml" }
]
}
}
}
},
"internal_tools": {
"demo-helper": { "path": "usr/libexec/demo/helper" }
},
"environment": {
"LOCALE_ARCHIVE": {
"operation": "replace",
"paths": [
{ "source": "payload", "path": "usr/lib/locale/locale-archive" }
]
}
}
}
Top-level fields:
- required
arch—"x86_64"or"aarch64" - optional
policy—"strict"(the default) or"integrated"; the current runtime records and reports this value, but both values use the same launch and verification rules - optional
min_kernel—MAJOR.MINORorMAJOR.MINOR.PATCH, default4.19 - required
library_dirs— ordered safe paths relative to_root; it may be empty only for a completely static configured startup closure - required non-empty
public_tools— commands exposed in top-levelbin/ - optional
internal_tools— commands exposed only through the managed child processPATH - optional
environment— rules shared by all tools
A tool declaration requires path, a safe path relative to _root resolving
to an executable regular file. Optional argv0 defaults to the tool name.
Optional argument_prefix inserts entries before caller arguments; each entry
is either a literal { "value": "..." } or a path selected from payload or
overrides. Optional per-tool environment is applied after the common rules.
Tool names are non-empty UTF-8 basenames: ., .., names containing / or
NUL, and the reserved bobr-bundle-launcher name are invalid. A name cannot be
both public and internal. argv0 must be non-empty and contain no NUL.
An environment rule has this shape:
{
"operation": "prepend",
"paths": [
{ "source": "payload", "path": "usr/share/example" }
],
"values": [],
"inherit": true,
"host_default": ["/fallback"]
}
paths, values, inherit, and host_default are optional and default to an
empty array or false. paths and values are mutually exclusive; multiple
entries are joined with :. Operations are:
replace— require configured paths or values and ignore the host;inheritandhost_defaultare invalidprepend/append— require configured paths or values; include the current value only withinherit = true, usinghost_defaultwhen that value is absentunset— remove the variable and accept none of the other fieldsdefault— preserve an existing variable, otherwise use configured paths or values, orhost_default;inheritis invalid
For prepend and append, host_default requires inherit = true. Variable
names must be non-empty and contain neither = nor NUL. PATH is reserved in
both common and per-tool rules: the builder always prepends
libexec/wrapped-bin to the inherited host PATH.
A typed path has source "payload" or "overrides" and a safe path relative
to that source. Paths must be non-empty and relative, with no NUL and no empty,
. or .. component. Referencing overrides requires the optional input to
be present. Paths are canonicalized and must remain inside the corresponding
completed bundle tree. The top-level config, tool declarations, literal
arguments, and environment rules reject unknown fields.
Behavior:
- independently copies the payload to
root/, the launcher tolibexec/, and optional overrides tooverrides/; it creates no hardlinks to input/store objects - writes a read-only ordinary directory object with public wrappers in
bin/and wrappers for every public/internal tool inlibexec/wrapped-bin/ - generates the versioned
bobr-host-bundle-v2bundle.tomland records the selected architecture as[platform].arch - verifies that the launcher, tools, dynamic loaders, and complete startup
DT_NEEDEDclosure are ELF64 objects for the declared architecture; scripts and their bundled shebang interpreters are verified recursively - resolves startup libraries only inside the payload, accepting safe
$ORIGINRPATH/RUNPATH entries and rejecting unsupported or escaping paths - removes write, setuid, and setgid bits before publication
- never executes target code while composing the bundle, so the builder itself is host-architecture-independent; the explicit config and input hashes carry all target-architecture differences into the build key
OciExtract
Extracts one OCI image layout into an fs-tree.
Inputs: required image — an OCI image layout object (for example, a
Source/OciRegistry result).
Config: none ({}).
Behavior:
- extracts the image root filesystem into one fs-tree object
- the result can be consumed as an fs-tree input by
TreeMerge,TreeMove,TreeSubset,Sandbox, orSandboxInstall
Sandbox
Runs an ordered plan of commands on a read-write overlay root — a set of Linux
namespaces with no network access, over the materialized _rootfs — and captures
the @{out} staging directory as the result: chowned to a single owner and
stored as a plain object. Changes the steps make to the root itself are
discarded; only @{out} is kept. To keep those changes instead, see
SandboxInstall.
Inputs:
- required
_rootfs— one fs-tree; materialized (its name begins with_) and used as the read-only lower layer of the build’s read-write overlay root - any number of extra inputs — each made available to the steps through its
interpolation name
@{name}(read-only). An input name must start with an ASCII letter or_, contain only ASCII letters, digits, or_, and must not bebuild,out, orconfig(reserved). An extra whose name begins with_is materialized into a filesystem tree (see above) and@{name}is that directory; otherwise@{name}is the object itself.
Config:
steps— a required, non-empty array of stepsscript_config— a config tree, available to the steps as@{config}(default{}, an empty config directory)
{
"script_config": {
"configure_args": ["--disable-nls"]
},
"steps": [
{
"name": "build",
"run_as": "build-user",
"cwd": "@{build}",
"argv": ["@{script}", "build"],
"env": { "CC": "gcc" }
}
]
}
Each step has:
name— non-empty (after trimming); used in reports and log namesrun_as—"build-user"or"root"cwd— non-empty; must resolve to an absolute pathargv— a non-empty array of non-empty stringsenv— optional object whose values are strings
Each step runs with a fixed default environment, which its env extends or
overrides: PATH, HOME (the build directory), TMPDIR (/tmp), USER
(bobr), LC_ALL and LANG (C), TZ (UTC), SOURCE_DATE_EPOCH (a fixed
epoch), and PYTHONHASHSEED (0) — locale, timezone, epoch, and hash seed are
pinned for reproducibility. The build, output, config, and inputs directories
are also exposed as BOBR_BUILD_DIR, BOBR_OUT_DIR, BOBR_CONFIG_DIR, and
BOBR_INPUTS_DIR, and the step’s name as BOBR_STEP_NAME. BOBR_BUILD_SEED
carries a deterministic per-build seed (64 lowercase hex chars) for steps that
need a reproducible “random” value, such as a filesystem UUID; it is derived
from the build’s reuse key, so identical inputs yield an identical seed.
cwd, each argv item, and each env value support @{…} interpolation
(name, run_as, env keys, and script_config do not):
@{build}— the writable build directory@{out}— the writable output directory; its contents become the result object (Sandboxonly)@{config}— the materializedscript_configdirectory@{<input>}— an extra input: the materialized directory if its name begins with_, otherwise the read-only object path
@@{name} escapes to the literal @{name}. Unknown variables and malformed
interpolation are invalid config.
script_config may be absent or {} (an empty config directory); otherwise
it is a recursive tree: objects become directories, arrays become directories
with zero-padded numeric entries (00000000, …) in order, and strings become
file contents. Keys must be non-empty, must not be . or .., and may contain
only ASCII letters, digits, ., _, and -.
SandboxInstall
The additive counterpart of Sandbox: the same overlay run — identical
_rootfs, extra inputs, steps, script_config, environment, and @{…}
interpolation — but instead of a separate @{out} it captures the changes the
steps make to the root as an additive fs-tree layer: the overlay’s upper
layer, an ownership-aware delta over _rootfs meant to be TreeMerged back onto
it.
Inputs: required _rootfs plus any number of extra inputs — as
Sandbox.
Config: steps and script_config — as Sandbox.
Behavior:
- runs the steps on the same read-write overlay root, then writes one fs-tree object containing only additions, pruning unchanged directory copy-ups
- overwriting existing files or symlinks, deleting lower-layer entries, changing existing directory metadata, or hiding existing directory contents fails the build; the result is not a general-purpose modification layer
- there is no
@{out}/BOBR_OUT_DIR; using@{out}is an unknown-variable error — the result is the delta, not a staging directory
Group
Aggregates several otherwise unrelated targets under one completion-marker target.
Inputs: one or more extra inputs (arbitrary).
Config: none ({}).
Behavior: does not merge or inspect its inputs; it stages a constant zero-byte marker once all inputs are realized, so its object is only a completion marker — the meaningful results are the input targets themselves.
Origins
A recipe for the Source builder obtains its object from an origin, one of:
Path—origin.pathis an absolute host path;origin.unpack(defaultfalse) treats it as a tar archive when true.Http—origin.urlis one HTTP(S) URL or an ordered list of fallbacks;origin.unpack(defaultfalse);origin.archive_formatmay override archive detection for unpacked sources:"tar-gz","tar-xz","tar-bz2", or"zip".OciRegistry—origin.imageis the registry image locator,origin.digestthe pinnedsha256:manifest or index digest, andorigin.platform({ "os": …, "architecture": … }) selects the platform to pull.bobrfetches the pinned manifest (selecting the platform from a manifest list or index), downloads and verifies every blob, and writes an OCI image layout whose canonical form is independent of the registry mirror named byorigin.image.