bldr
The core build API: artifacts, pods, images, scopes and targets.
The core build API: artifacts, pods, images, scopes and targets.
Import from bldr in a .bldr.ts. Generated from the module's TypeScript.
Contents
AllOfPipelineCondition | class | Conjunction of conditions. |
AnyOfPipelineCondition | class | Disjunction of conditions. |
Artifact | class | A content-addressed build artifact: a lazy, memoized computation of a CID. |
buildArchitecture | function | |
cacheVolume | function | |
CacheVolume | class | A named cache volume, mountable into a pod with Pod.cache. |
claim | function | |
Compression | variable | Constructors for the supported codecs. |
ContainerImage | class | A content-addressed build artifact: a lazy, memoized computation of a CID. |
Coverage | class | A content-addressed build artifact: a lazy, memoized computation of a CID. |
DebianContainerImage | class | A Debian/Ubuntu base image, with apt-get spelled once. |
DEFAULT_ARCHITECTURE_OPTION | variable | The workspace option the node fills in with its host architecture. |
defaultArchitecture | function | |
Deployment | class | A content-addressed build artifact: a lazy, memoized computation of a CID. |
DeploymentPipelineAction | class | Run a deployment into an environment. |
Diagnostics | class | A content-addressed build artifact: a lazy, memoized computation of a CID. |
Directory | class | A content-addressed build artifact: a lazy, memoized computation of a CID. |
encodeUtf8 | function | |
Environment | class | A deployment environment: a name plus free-form labels. |
exitedWith | function | |
File | class | A content-addressed build artifact: a lazy, memoized computation of a CID. |
fromBiomeJson | function | |
fromCargoJson | function | |
fromJestJson | function | |
fromLcov | function | |
fromLibtestJson | function | |
fromLlvmJson | function | |
httpGet | function | |
httpPost | function | |
httpPut | function | |
invoke | function | |
invokeJson | function | |
isArtifact | function | |
languageServer | function | |
languageServerLabels | function | |
link | function | |
locate | function | |
LSP_CLAIMS_LABEL | variable | Which files this server answers about, relative to a mounted tree, as |
LSP_PIPE_DIR | variable | Where a v1 language server reads and writes. |
LSP_PIPE_IN | variable | |
LSP_PIPE_OUT | variable | |
LSP_PRIORITY_LABEL | variable | Which server wins when two claim the same file, as an integer. Higher wins; |
LSP_RUNNABLE_LABEL | variable | The label the daemon looks for, and the contract version it names. |
LSP_RUNNABLE_VERSION | variable | |
LSP_SERVICE | variable | The service name a single-server runnable uses. |
memberScope | function | |
Metrics | class | A content-addressed build artifact: a lazy, memoized computation of a CID. |
metricsFromCoverage | function | |
metricsFromPodDag | function | |
metricsFromTestResults | function | |
metricsTransform | function | |
narrowSources | function | |
nestSources | function | |
nodeCapabilities | function | |
nodeSupports | function | |
NoopPipelineAction | class | No action: the step exists to sequence or gate others. What a step stores |
NotPipelineCondition | class | Negation of one condition. |
Pipeline | class | A content-addressed build artifact: a lazy, memoized computation of a CID. |
PipelineAction | class | What a step does. Abstract: the family is open — each concrete action |
PipelineCondition | class | A gate on a step. Abstract: the family is open — each concrete condition |
PipelineStep | class | One step of a pipeline. Constructed by Pipeline.addStep — the value |
Pod | class | |
registrations | function | |
Report | class | A content-addressed build artifact: a lazy, memoized computation of a CID. |
resetRegistrations | function | |
rootScope | function | |
runnable | function | |
Runnable | class | A runnable builder. Fluent: declare network(...)s and service(...)s, then |
SCCACHE_BIN_DIR | variable | Where the node mounts the sccache wrapper inside the pod. The node also |
SCCACHE_CAPABILITY | variable | The node capability a pod needs before it may ask for sccache. |
SCCACHE_SOCKET | variable | Where the node binds the pod's sccache socket ($SCCACHE_SERVER_UDS). |
sccacheCargoConfig | function | |
sccacheSupported | function | |
Scope | class | A named place in the build tree. |
setWorkspaceOptions | function | |
shquote | function | |
tailLines | function | |
TimeWindowPipelineCondition | class | A cron-style time window: holds when the current time matches every field. |
toYaml | function | |
unlink | function | |
Value | class | A lazily-decoded non-CID value read out of the graph — an image's |
workspaceOption | function | |
workspaceOptions | function | |
yamlDocuments | function |
Types
Each has its own page.
AptInstallOptions | interface | Knobs for DebianContainerImage.aptInstall. Every one defaults to |
ArtifactKind | type | Which kind of artifact — the phantom brand that makes the subclasses |
ArtifactSource | type | What an Artifact can be built from: a resolved CID, another lazy CID, |
BuildEntry | type | A member's build function: given its root Scope, declare what it |
CacheMode | type | How a volume is shared with concurrent holders. RwLock, essentially. |
CacheVolumeOptions | interface | Options for cacheVolume. |
Cid | type | A content-addressed block identifier. |
CidSource | type | Something that resolves to a content-addressed CID: a ContainerImage / Directory |
Command | type | The command to run: a shell string, an explicit argv, or a thunk of either |
CompositeBlock | type | The options of a composite deployment: exactly one of the block forms. |
CompositeCall | interface | A child deployment run by a composite: the function and its options. |
CompositeDag | interface | Named child deployments run one at a time in an order that respects deps. |
CompositeDagNode | interface | A node of the DAG block: a named child deployment and the names it waits for. |
CompositeParallel | interface | Steps that run concurrently; the block waits for all of them and fails if |
CompositeSequence | interface | Steps that run one after another; the first failure stops the block and is |
CompositeStep | type | One step of a block: a child deployment, or another block written inline. |
CompositeTry | interface | try runs; catch runs only when try failed; finally runs in every |
Compression | type | Constructors for the supported codecs. |
ConfigPatch | interface | A partial config to set (used by ContainerImage.mapConfig). Each |
ContainerImageMember | interface | A member whose content is an OCI image manifest. |
CoverageBucketRule | interface | A rule routing paths into the generated or external bucket. |
CoverageConvertOptions | interface | Options shared by every coverage.from-* conversion. |
CoverageMapping | interface | One path prefix and the member it belongs to. |
DagMember | interface | A member whose content is a DAG-CBOR object rather than files or an image. |
DagResources | interface | What a whole DAG is granted: the budget its pods share, not a per-pod limit. |
DeploymentSpec | interface | A deployment definition. Mirrors bldr_types::build_output::Deployment: |
DiagnosticsOptions | interface | How a converter should resolve the paths a report names. |
DiffResult | interface | The result of Directory.diff: paths added / removed / modified. |
DirectoryMember | interface | A member whose content is a filesystem tree. |
EnvironmentProps | interface | Props for Environment. |
HttpOptions | interface | |
HttpResponse | interface | |
ImageConfigInfo | interface | An image's effective config, as read by ContainerImage.config. |
LanguageServer | interface | A language server ready to register: the runnable that runs it, and what it |
LanguageServerOptions | interface | How to wrap one stdio language server as a v1 service. |
LayerInfo | interface | One layer of an image, as read by ContainerImage.layers. |
Member | type | A workspace member, as seen from build code. |
MemberKind | type | |
Metadata | interface | Metadata to set on a path (see Directory.setMetadata). |
MetricsTagOptions | interface | Extra tags stamped on every produced row. |
MetricsTransformOptions | interface | Row selection + aggregation, mirroring the GetBuildMetrics query. |
MountOptions | interface | Options for Pod.mount. |
OtherMember | interface | A member of a kind this API has no typed accessor for. cid still works. |
PipelineSpec | interface | A pipeline definition's own props. Steps are not part of the spec: they are |
PipelineStepProps | interface | Props for Pipeline.addStep. Optional fields default — see the |
RegisteredDeployment | interface | A registered deployment: a node function plus the options to invoke it with. |
Registrations | interface | Everything the members' build functions declared, keyed by target path. |
RunMountDef | interface | An Directory (build artifact) mounted into a service. |
RunOptions | interface | Knobs for ContainerImage.run. |
RunPort | interface | A host→service port forward. |
RunServiceDef | interface | A service (a long-lived pod) in a runnable. |
SccacheMode | type | How a pod may use the shared cache. |
SccacheOptions | interface | What Pod.sccache accepts. |
ScopeOptions | interface | Extra metadata for a scope, shown against its node in the build view. |
SourceOrigin | interface | One subtree of a composed tree, and where it came from. |
TimeWindowProps | interface | Props for TimeWindowPipelineCondition: cron field expressions. |
AllOfPipelineCondition
class
Conjunction of conditions.
.constructor(conditions: PipelineCondition[])
constructor
constructor(conditions: PipelineCondition[]): AllOfPipelineConditionSee PipelineCondition.
conditions: readonly PipelineCondition[]
property
See PipelineCondition.
.allOf(...conditions: PipelineCondition[])
static method
Holds when every one of conditions holds (none: always holds).
allOf(...conditions: PipelineCondition[]): AllOfPipelineConditionSee PipelineCondition.
.anyOf(...conditions: PipelineCondition[])
static method
Holds when at least one of conditions holds (none: never holds).
anyOf(...conditions: PipelineCondition[]): AnyOfPipelineConditionSee AnyOfPipelineCondition, PipelineCondition.
.noneOf(...conditions: PipelineCondition[])
static method
Holds when none of conditions hold — not(anyOf(…)), spelled the
way a reader thinks it.
noneOf(...conditions: PipelineCondition[]): NotPipelineConditionSee NotPipelineCondition, PipelineCondition.
.not(condition: PipelineCondition)
static method
Holds when condition does not.
not(condition: PipelineCondition): NotPipelineConditionSee NotPipelineCondition, PipelineCondition.
AnyOfPipelineCondition
class
Disjunction of conditions.
.constructor(conditions: PipelineCondition[])
constructor
constructor(conditions: PipelineCondition[]): AnyOfPipelineConditionSee PipelineCondition.
conditions: readonly PipelineCondition[]
property
See PipelineCondition.
.allOf(...conditions: PipelineCondition[])
static method
Holds when every one of conditions holds (none: always holds).
allOf(...conditions: PipelineCondition[]): AllOfPipelineConditionSee AllOfPipelineCondition, PipelineCondition.
.anyOf(...conditions: PipelineCondition[])
static method
Holds when at least one of conditions holds (none: never holds).
anyOf(...conditions: PipelineCondition[]): AnyOfPipelineConditionSee PipelineCondition.
.noneOf(...conditions: PipelineCondition[])
static method
Holds when none of conditions hold — not(anyOf(…)), spelled the
way a reader thinks it.
noneOf(...conditions: PipelineCondition[]): NotPipelineConditionSee NotPipelineCondition, PipelineCondition.
.not(condition: PipelineCondition)
static method
Holds when condition does not.
not(condition: PipelineCondition): NotPipelineConditionSee NotPipelineCondition, PipelineCondition.
Artifact
class
A content-addressed build artifact: a lazy, memoized computation of a CID.
Composition on the subclasses is synchronous and builds a graph; nothing runs
until the artifact is forced. Force with cid (or the inherited
get(), which it aliases).
.constructor(source: ArtifactSource, labelHint?: string)
constructor
constructor(source: ArtifactSource, labelHint?: string): ArtifactSee ArtifactSource.
nodeLabel
accessor
shape
accessor
valueScope
accessor
.cid(inherited?: TaskSlot)
method
Force and return this artifact's CID. Alias of the inherited get(),
kept because .cid() reads better at a build's call sites. inherited
is passed by a consuming composition step — see Lazy.get.
cid(inherited?: TaskSlot): Promise<string>.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<string>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.map(f: (…) => …)
method
Derive a new lazy value from this one without forcing it now. Inherits this value's scope, so composition stays in the subtree that owns it.
map(f: (…) => …): Lazy<U>.dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …)
static method
A dynamic artifact: one whose subgraph cannot be known until deps
have resolved, because deciding what to build requires reading them.
Called on the concrete class, which is what types the result:
const crates = Directory.dynamic("compile each crate", [src], async () =>
Directory.merge(...(await src.entries()).map(([n, e]) => compile(n, e))))
.in(scope);label is what this node is called in the build tree — the one thing a
dynamic node cannot derive, since it runs no node function of its own.
Where it goes is [in][Lazy.in]'s business, like every other value; a
dynamic artifact with no scope groups its expansion under whichever
consumer forces it.
This is the only asynchronous surface in the API: everything else composes synchronously. The graph has a hole inside a dynamic node, but every edge around it is static — the returned handle is an ordinary artifact, so downstream work is declared normally.
deps are forced before expand runs, and are the node's declared
dependency edges.
dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …): TSee ArtifactSource.
.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>buildArchitecture()
function
The architecture this build's own pods run on: the node's, as published in
the build's options, or amd64 under a daemon that does not fill it in.
What a tool defaults to when it must pick one platform of a
multi-architecture image to run: an image for another architecture does
not start (exec format error). A tool that produces something for
another architecture names it instead.
buildArchitecture(): stringcacheVolume(name: string, opts?: CacheVolumeOptions)
function
Declare a cache volume named name, namespaced by opts.scope if given.
Two calls with the same resulting name are the same volume — that is the point, and it is why the namespacing is explicit rather than automatic: a volume shared on purpose and one shared by accident look identical afterwards.
cacheVolume(name: string, opts?: CacheVolumeOptions): CacheVolumeSee CacheVolume, CacheVolumeOptions.
CacheVolume
class
A named cache volume, mountable into a pod with Pod.cache.
const downloads = cacheVolume("npm-cache"); // global
const target = cacheVolume("target", { scope, mode: "exclusive" }); // per-crate
pod.cache(downloads, "/tmp/.npm-cache").cache(target, "/work/target");.constructor(name: string, mode: CacheMode, perImage: boolean)
constructor
constructor(name: string, mode: CacheMode, perImage: boolean): CacheVolumeSee CacheMode.
mode: CacheMode
property
See CacheMode.
name: string
property
The volume's full name — what the daemon keys the workspace's cache on.
perImage: boolean
property
Whether each rootfs image gets its own copy — see CacheVolumeOptions.perImage.
exclusive
accessor
claim(path: string, kind: string)
function
Claim path for one kind of registration, or say who already has it.
Names are checked across every kind, not per-kind: bldr/daemon/docs as
both an output and a test would be two different things reachable by one
name, and which one a caller got would depend on which map was searched
first. One name, one thing.
claim(path: string, kind: string): voidCompression: { … }
variable
Constructors for the supported codecs.
ContainerImage
class
A content-addressed build artifact: a lazy, memoized computation of a CID.
Composition on the subclasses is synchronous and builds a graph; nothing runs
until the artifact is forced. Force with cid (or the inherited
get(), which it aliases).
.constructor(image: string | Lazy<string>, arch?: string, overrides?: Overrides)
constructor
image is a manifest CID (or a lazy one). arch selects the platform for
a multi-arch image and is fixed for this image and all images derived from
it. overrides is internal (carried through the chain).
constructor(image: string | Lazy<string>, arch?: string, overrides?: Overrides): ContainerImagearchitecture
accessor
nodeLabel
accessor
shape
accessor
valueScope
accessor
.add(src: Directory, dest: string)
method
ADD — like copy for a local tree. (URL sources and tar
auto-extraction are not supported; use copy + run tar.)
add(src: Directory, dest: string): ContainerImageSee Directory.
.arg(key: string, value?: string)
method
ARG — a build-time variable (available to mapConfig callbacks
via buildArgs; not persisted to the image).
arg(key: string, value?: string): ContainerImage.buildArgs()
method
The build-time args set so far (for use in derived logic).
buildArgs(): Record<string, string>.cid(inherited?: TaskSlot)
method
Force and return this artifact's CID. Alias of the inherited get(),
kept because .cid() reads better at a build's call sites. inherited
is passed by a consuming composition step — see Lazy.get.
cid(inherited?: TaskSlot): Promise<string>.cmd(argv: string[])
method
CMD (exec form).
cmd(argv: string[]): ContainerImage.config()
method
The effective config (base image config with this image's overrides merged).
Memoized and placed, which is the only combination that is honest. The
value is read once and shared by every consumer (pod, run, layers,
build code), so an unplaced one would be attributed to whichever of them
won the race to force it — the very mis-attribution
ContainerImage.fromMember places the image to avoid. Placing it
in the image's scope makes the winner irrelevant: a Lazy with a scope
of its own ignores the slot a consumer offers. An image with no scope
keeps the old behaviour, because there is nothing better to say.
config(): Value<ImageConfigInfo>See ImageConfigInfo, Value.
.copy(src: Directory, dest: string)
method
COPY — the tree src becomes a new layer at dest (its contents land
under dest). Lazy.
A content-level graft (fs.at), not a pod: the layer is src placed
under dest, with its modes and symlinks as they are. No process runs,
which is what lets an image for another architecture be assembled on
this one — a cp inside an arm64 rootfs would need an arm64 CPU.
copy(src: Directory, dest: string): ContainerImageSee Directory.
.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.entrypoint(argv: string[])
method
ENTRYPOINT (exec form).
entrypoint(argv: string[]): ContainerImage.env(key: string, value: string)
method
ENV KEY VALUE.
env(key: string, value: string): ContainerImage.envAll(vars: Record<string, string>)
method
ENV for several variables at once.
envAll(vars: Record<string, string>): ContainerImage.expose(...ports: string[])
method
EXPOSE — document a port ("8080" or "8080/tcp").
expose(...ports: string[]): ContainerImage.fromOci()
method
Convert to bldr's native layer form: every .tar blob layer is
untarred into an FsNode (cheap to build on / diff / hot-swap). The
inverse of toOci. (oci.image.from-oci.)
fromOci(): ContainerImage.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<string>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.label(key: string, value: string)
method
LABEL key=value.
label(key: string, value: string): ContainerImage.labels(map: Record<string, string>)
method
LABEL for several labels at once.
labels(map: Record<string, string>): ContainerImage.layers()
method
The ordered layer list (base-first).
layers(): Value<LayerInfo[]>.map(f: (…) => …)
method
Derive a new lazy value from this one without forcing it now. Inherits this value's scope, so composition stays in the subtree that owns it.
map(f: (…) => …): Lazy<U>.mapConfig(fn: (…) => …)
method
Read this image's effective config, transform it in the fn callback
(running lazily), and write the returned patch back as the image's config.
The new image carries the result baked in.
const bumped = img.mapConfig((cfg) => ({
labels: { ...cfg.labels, "build.n": String(Number(cfg.labels["build.n"] ?? 0) + 1) },
}));mapConfig(fn: (…) => …): ContainerImageSee ConfigPatch, ImageConfigInfo.
.mapLabels(fn: (…) => …)
method
Read all labels, transform the map in fn, and write the result back.
A convenience over mapConfig for the common label case.
const img2 = img.mapLabels((labels) => ({
...labels,
"org.opencontainers.image.revision": gitSha,
}));mapLabels(fn: (…) => …): ContainerImage.pod()
method
A Pod over this image's rootfs, pre-seeded with the image's
effective env (so PATH etc. apply) and its WORKDIR/USER/SHELL.
Set the command and take outputs as usual.
pod(): PodSee Pod.
.root()
method
The merged root filesystem of the image (layers). (Config does not affect the rootfs.) Placed in this image's scope, like every other derivation of it: materializing a shared base image's rootfs is that image's own work, not the work of the first pod to ask for it.
root(): DirectorySee Directory.
.run(command: Command, opts?: RunOptions)
method
RUN — run command over this image's rootfs and append the resulting
overlay diff as a new layer. A shell string runs through the image's
SHELL/WORKDIR/USER with its ENV applied; an argv runs directly
(exec form). Lazy.
The layer's pod has no network unless { internet: true } approves it —
see RunOptions.
run(command: Command, opts?: RunOptions): ContainerImageSee Command, RunOptions.
.shell(argv: string[])
method
SHELL — the shell used for run shell-form commands. Build-time.
shell(argv: string[]): ContainerImage.stopSignal(signal: string)
method
STOPSIGNAL.
stopSignal(signal: string): ContainerImage.toOci()
method
Convert to the OCI layer form: every native layer (an FsNode overlay
diff, as run/copy produce) is tarred + gzipped into a blob with real
content digests, and the config's rootfs.diff_ids are rebuilt — a
standard, registry-pushable image. (oci.image.to-oci.)
toOci(): ContainerImage.user(user: string)
method
USER — default user for run (shell form) and the image.
user(user: string): ContainerImage.volume(...paths: string[])
method
VOLUME — declare a mount point as a volume.
volume(...paths: string[]): ContainerImage.workdir(dir: string)
method
WORKDIR — default directory for run (shell form) and the image.
workdir(dir: string): ContainerImage.dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …)
static method
A dynamic artifact: one whose subgraph cannot be known until deps
have resolved, because deciding what to build requires reading them.
Called on the concrete class, which is what types the result:
const crates = Directory.dynamic("compile each crate", [src], async () =>
Directory.merge(...(await src.entries()).map(([n, e]) => compile(n, e))))
.in(scope);label is what this node is called in the build tree — the one thing a
dynamic node cannot derive, since it runs no node function of its own.
Where it goes is [in][Lazy.in]'s business, like every other value; a
dynamic artifact with no scope groups its expansion under whichever
consumer forces it.
This is the only asynchronous surface in the API: everything else composes synchronously. The graph has a hole inside a dynamic node, but every edge around it is static — the returned handle is an ordinary artifact, so downstream work is declared normally.
deps are forced before expand runs, and are the node's declared
dependency edges.
dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …): TSee Artifact, ArtifactSource.
.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.from(image: string | Lazy<string>, arch?: string)
static method
FROM — start from a base image manifest CID at architecture arch.
from(image: string | Lazy<string>, arch?: string): ContainerImage.fromMember(member: ImageMemberRef, arch?: string)
static method
FROM an image member — the form to prefer when the base is a
workspace member (import rust from "rust").
The image is placed in that member's own scope, so everything derived from
it (an apt-get, a copy, the pods those run) lands in the member's
subtree. A shared base image has no other honest home: placing it in a
consumer's scope would attribute it to whichever member forced it first,
and leaving it unplaced strands its work at the tree root.
fromMember(member: ImageMemberRef, arch?: string): ContainerImage.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>Coverage
class
A content-addressed build artifact: a lazy, memoized computation of a CID.
Composition on the subclasses is synchronous and builds a graph; nothing runs
until the artifact is forced. Force with cid (or the inherited
get(), which it aliases).
.constructor(source: ArtifactSource, labelHint?: string)
constructor
constructor(source: ArtifactSource, labelHint?: string): CoverageSee ArtifactSource.
nodeLabel
accessor
shape
accessor
valueScope
accessor
.cid(inherited?: TaskSlot)
method
Force and return this artifact's CID. Alias of the inherited get(),
kept because .cid() reads better at a build's call sites. inherited
is passed by a consuming composition step — see Lazy.get.
cid(inherited?: TaskSlot): Promise<string>.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<string>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.map(f: (…) => …)
method
Derive a new lazy value from this one without forcing it now. Inherits this value's scope, so composition stays in the subtree that owns it.
map(f: (…) => …): Lazy<U>.dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …)
static method
A dynamic artifact: one whose subgraph cannot be known until deps
have resolved, because deciding what to build requires reading them.
Called on the concrete class, which is what types the result:
const crates = Directory.dynamic("compile each crate", [src], async () =>
Directory.merge(...(await src.entries()).map(([n, e]) => compile(n, e))))
.in(scope);label is what this node is called in the build tree — the one thing a
dynamic node cannot derive, since it runs no node function of its own.
Where it goes is [in][Lazy.in]'s business, like every other value; a
dynamic artifact with no scope groups its expansion under whichever
consumer forces it.
This is the only asynchronous surface in the API: everything else composes synchronously. The graph has a hole inside a dynamic node, but every edge around it is static — the returned handle is an ordinary artifact, so downstream work is declared normally.
deps are forced before expand runs, and are the node's declared
dependency edges.
dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …): TSee Artifact, ArtifactSource.
.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.fromCid(cid: string)
static method
A Coverage from an existing DataType::CoverageReport CID.
fromCid(cid: string): Coverage.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>DebianContainerImage
class
A Debian/Ubuntu base image, with apt-get spelled once.
Every callsite of apt-get in a build has to remember the same three things:
refresh the lists first, refuse recommends, and delete the lists in the
same layer — a separate cleanup .run() writes a new layer over the old
one and the bytes stay in the image. Writing that by hand at each callsite is
how one of them ends up missing a piece.
new DebianContainerImage(ContainerImage.from(DEBIAN))
.aptInstall(["curl", "ca-certificates"])Nothing else about the image changes: this is ContainerImage with one extra
verb, so .run(), .copy(), .env() and the rest behave exactly as they do
on the base class.
.constructor(image: ContainerImage)
constructor
Wrap image — its baked manifest becomes this image's base, so the
config it carries (env, workdir, user, shell) comes along.
constructor(image: ContainerImage): DebianContainerImageSee ContainerImage.
architecture
accessor
nodeLabel
accessor
shape
accessor
valueScope
accessor
.add(src: Directory, dest: string)
method
ADD — like copy for a local tree. (URL sources and tar
auto-extraction are not supported; use copy + run tar.)
add(src: Directory, dest: string): ContainerImageSee ContainerImage, Directory.
.aptInstall(packageNames: string | string[], opts?: AptInstallOptions)
method
apt-get install as one layer: update, install, and drop the package
lists, joined with && so a failure anywhere fails the layer.
The layer's pod is approved for the internet: installing packages is fetching them.
Returns a DebianContainerImage, so installs chain.
aptInstall(packageNames: string | string[], opts?: AptInstallOptions): DebianContainerImageSee AptInstallOptions.
.arg(key: string, value?: string)
method
ARG — a build-time variable (available to mapConfig callbacks
via buildArgs; not persisted to the image).
arg(key: string, value?: string): ContainerImageSee ContainerImage.
.buildArgs()
method
The build-time args set so far (for use in derived logic).
buildArgs(): Record<string, string>.cid(inherited?: TaskSlot)
method
Force and return this artifact's CID. Alias of the inherited get(),
kept because .cid() reads better at a build's call sites. inherited
is passed by a consuming composition step — see Lazy.get.
cid(inherited?: TaskSlot): Promise<string>.cmd(argv: string[])
method
CMD (exec form).
cmd(argv: string[]): ContainerImageSee ContainerImage.
.config()
method
The effective config (base image config with this image's overrides merged).
Memoized and placed, which is the only combination that is honest. The
value is read once and shared by every consumer (pod, run, layers,
build code), so an unplaced one would be attributed to whichever of them
won the race to force it — the very mis-attribution
ContainerImage.fromMember places the image to avoid. Placing it
in the image's scope makes the winner irrelevant: a Lazy with a scope
of its own ignores the slot a consumer offers. An image with no scope
keeps the old behaviour, because there is nothing better to say.
config(): Value<ImageConfigInfo>See ImageConfigInfo, Value.
.copy(src: Directory, dest: string)
method
COPY — the tree src becomes a new layer at dest (its contents land
under dest). Lazy.
A content-level graft (fs.at), not a pod: the layer is src placed
under dest, with its modes and symlinks as they are. No process runs,
which is what lets an image for another architecture be assembled on
this one — a cp inside an arm64 rootfs would need an arm64 CPU.
copy(src: Directory, dest: string): ContainerImageSee ContainerImage, Directory.
.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.entrypoint(argv: string[])
method
ENTRYPOINT (exec form).
entrypoint(argv: string[]): ContainerImageSee ContainerImage.
.env(key: string, value: string)
method
ENV KEY VALUE.
env(key: string, value: string): ContainerImageSee ContainerImage.
.envAll(vars: Record<string, string>)
method
ENV for several variables at once.
envAll(vars: Record<string, string>): ContainerImageSee ContainerImage.
.expose(...ports: string[])
method
EXPOSE — document a port ("8080" or "8080/tcp").
expose(...ports: string[]): ContainerImageSee ContainerImage.
.fromOci()
method
Convert to bldr's native layer form: every .tar blob layer is
untarred into an FsNode (cheap to build on / diff / hot-swap). The
inverse of toOci. (oci.image.from-oci.)
fromOci(): ContainerImageSee ContainerImage.
.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<string>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.label(key: string, value: string)
method
LABEL key=value.
label(key: string, value: string): ContainerImageSee ContainerImage.
.labels(map: Record<string, string>)
method
LABEL for several labels at once.
labels(map: Record<string, string>): ContainerImageSee ContainerImage.
.layers()
method
The ordered layer list (base-first).
layers(): Value<LayerInfo[]>.map(f: (…) => …)
method
Derive a new lazy value from this one without forcing it now. Inherits this value's scope, so composition stays in the subtree that owns it.
map(f: (…) => …): Lazy<U>.mapConfig(fn: (…) => …)
method
Read this image's effective config, transform it in the fn callback
(running lazily), and write the returned patch back as the image's config.
The new image carries the result baked in.
const bumped = img.mapConfig((cfg) => ({
labels: { ...cfg.labels, "build.n": String(Number(cfg.labels["build.n"] ?? 0) + 1) },
}));mapConfig(fn: (…) => …): ContainerImageSee ConfigPatch, ContainerImage, ImageConfigInfo.
.mapLabels(fn: (…) => …)
method
Read all labels, transform the map in fn, and write the result back.
A convenience over mapConfig for the common label case.
const img2 = img.mapLabels((labels) => ({
...labels,
"org.opencontainers.image.revision": gitSha,
}));mapLabels(fn: (…) => …): ContainerImageSee ContainerImage.
.pod()
method
A Pod over this image's rootfs, pre-seeded with the image's
effective env (so PATH etc. apply) and its WORKDIR/USER/SHELL.
Set the command and take outputs as usual.
pod(): PodSee Pod.
.root()
method
The merged root filesystem of the image (layers). (Config does not affect the rootfs.) Placed in this image's scope, like every other derivation of it: materializing a shared base image's rootfs is that image's own work, not the work of the first pod to ask for it.
root(): DirectorySee Directory.
.run(command: Command, opts?: RunOptions)
method
RUN — run command over this image's rootfs and append the resulting
overlay diff as a new layer. A shell string runs through the image's
SHELL/WORKDIR/USER with its ENV applied; an argv runs directly
(exec form). Lazy.
The layer's pod has no network unless { internet: true } approves it —
see RunOptions.
run(command: Command, opts?: RunOptions): ContainerImageSee Command, ContainerImage, RunOptions.
.shell(argv: string[])
method
SHELL — the shell used for run shell-form commands. Build-time.
shell(argv: string[]): ContainerImageSee ContainerImage.
.stopSignal(signal: string)
method
STOPSIGNAL.
stopSignal(signal: string): ContainerImageSee ContainerImage.
.toOci()
method
Convert to the OCI layer form: every native layer (an FsNode overlay
diff, as run/copy produce) is tarred + gzipped into a blob with real
content digests, and the config's rootfs.diff_ids are rebuilt — a
standard, registry-pushable image. (oci.image.to-oci.)
toOci(): ContainerImageSee ContainerImage.
.user(user: string)
method
USER — default user for run (shell form) and the image.
user(user: string): ContainerImageSee ContainerImage.
.volume(...paths: string[])
method
VOLUME — declare a mount point as a volume.
volume(...paths: string[]): ContainerImageSee ContainerImage.
.workdir(dir: string)
method
WORKDIR — default directory for run (shell form) and the image.
workdir(dir: string): ContainerImageSee ContainerImage.
.aptInstallScript(packageNames: string | string[], opts?: AptInstallOptions)
static method
The same shell one-liner aptInstall runs, for the callsites that
cannot take it as a layer of its own.
Some images install packages in the middle of a longer script — after
writing an apt.conf.d fragment the install depends on, or with a
tolerated failure the surrounding steps recover from. Splitting those
into three layers to use the method would cost two extra pods and change
what is cached, so they compose the string instead and keep one .run()
— which has to pass { internet: true } itself, since the script fetches.
aptInstallScript(packageNames: string | string[], opts?: AptInstallOptions): stringSee AptInstallOptions.
.dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …)
static method
A dynamic artifact: one whose subgraph cannot be known until deps
have resolved, because deciding what to build requires reading them.
Called on the concrete class, which is what types the result:
const crates = Directory.dynamic("compile each crate", [src], async () =>
Directory.merge(...(await src.entries()).map(([n, e]) => compile(n, e))))
.in(scope);label is what this node is called in the build tree — the one thing a
dynamic node cannot derive, since it runs no node function of its own.
Where it goes is [in][Lazy.in]'s business, like every other value; a
dynamic artifact with no scope groups its expansion under whichever
consumer forces it.
This is the only asynchronous surface in the API: everything else composes synchronously. The graph has a hole inside a dynamic node, but every edge around it is static — the returned handle is an ordinary artifact, so downstream work is declared normally.
deps are forced before expand runs, and are the node's declared
dependency edges.
dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …): TSee Artifact, ArtifactSource.
.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.from(image: string | Lazy<string>, arch?: string)
static method
FROM — start from a base image manifest CID at architecture arch.
from(image: string | Lazy<string>, arch?: string): ContainerImageSee ContainerImage.
.fromMember(member: ImageMemberRef, arch?: string)
static method
FROM an image member — the form to prefer when the base is a
workspace member (import rust from "rust").
The image is placed in that member's own scope, so everything derived from
it (an apt-get, a copy, the pods those run) lands in the member's
subtree. A shared base image has no other honest home: placing it in a
consumer's scope would attribute it to whichever member forced it first,
and leaving it unplaced strands its work at the tree root.
fromMember(member: ImageMemberRef, arch?: string): ContainerImageSee ContainerImage.
.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>DEFAULT_ARCHITECTURE_OPTION: "bldr.default-architecture"
variable
The workspace option the node fills in with its host architecture.
defaultArchitecture(options: unknown)
function
The architecture this build is for, in OCI spelling ("amd64", "arm64").
const arch = defaultArchitecture(input.options);
const img = new ContainerImage(base, arch);undefined when the build runs against a daemon old enough not to fill the
option in — treat that as "unknown", and fall back to whatever the build
assumed before it could ask.
defaultArchitecture(options: unknown): stringDeployment
class
A content-addressed build artifact: a lazy, memoized computation of a CID.
Composition on the subclasses is synchronous and builds a graph; nothing runs
until the artifact is forced. Force with cid (or the inherited
get(), which it aliases).
.constructor(source: ArtifactSource | DeploymentSpec, labelHint?: string)
constructor
A deployment authored in place —
new Deployment({ function: "deployment/local.shell/v0.0.0", … }).
The ArtifactSource form is what fromCid and Artifact.dynamic use
to wrap an existing CID; build code writes the spec form.
constructor(source: ArtifactSource | DeploymentSpec, labelHint?: string): DeploymentSee ArtifactSource, DeploymentSpec.
COMPOSITE_FUNCTION: "deployment/composite/v0.0.0"
static property
The composite deployment function: runs other deployments as one.
nodeLabel
accessor
shape
accessor
valueScope
accessor
.cid(inherited?: TaskSlot)
method
Force and return this artifact's CID. Alias of the inherited get(),
kept because .cid() reads better at a build's call sites. inherited
is passed by a consuming composition step — see Lazy.get.
cid(inherited?: TaskSlot): Promise<string>.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<string>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.map(f: (…) => …)
method
Derive a new lazy value from this one without forcing it now. Inherits this value's scope, so composition stays in the subtree that owns it.
map(f: (…) => …): Lazy<U>.composite(block: CompositeBlock, labels?: Record<string, string>)
static method
A composite deployment authored in place: block as the options of
Deployment.COMPOSITE_FUNCTION.
composite(block: CompositeBlock, labels?: Record<string, string>): DeploymentSee CompositeBlock.
.dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …)
static method
A dynamic artifact: one whose subgraph cannot be known until deps
have resolved, because deciding what to build requires reading them.
Called on the concrete class, which is what types the result:
const crates = Directory.dynamic("compile each crate", [src], async () =>
Directory.merge(...(await src.entries()).map(([n, e]) => compile(n, e))))
.in(scope);label is what this node is called in the build tree — the one thing a
dynamic node cannot derive, since it runs no node function of its own.
Where it goes is [in][Lazy.in]'s business, like every other value; a
dynamic artifact with no scope groups its expansion under whichever
consumer forces it.
This is the only asynchronous surface in the API: everything else composes synchronously. The graph has a hole inside a dynamic node, but every edge around it is static — the returned handle is an ordinary artifact, so downstream work is declared normally.
deps are forced before expand runs, and are the node's declared
dependency edges.
dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …): TSee Artifact, ArtifactSource.
.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.fromCid(cid: string)
static method
A Deployment from an existing DataType::Deployment CID.
fromCid(cid: string): Deployment.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>.parallel(steps: CompositeStep[])
static method
A block whose steps run concurrently — see CompositeParallel.
parallel(steps: CompositeStep[]): CompositeParallelSee CompositeParallel, CompositeStep.
.sequence(steps: CompositeStep[])
static method
A block whose steps run one after another — see CompositeSequence.
sequence(steps: CompositeStep[]): CompositeSequenceSee CompositeSequence, CompositeStep.
.tryCatch(block: CompositeTry)
static method
A try/catch/finally block — see CompositeTry for the outcome
rules. An absent catch or finally is left out of the block, so the
stored options carry only the phases that exist.
tryCatch(block: CompositeTry): CompositeTrySee CompositeTry.
DeploymentPipelineAction
class
Run a deployment into an environment.
The deployment is stored as a CID link (Ref<Deployment> on the Rust
side): the pipeline references the deployment block, it does not embed it —
so several steps can share one deployment, and the block stays inspectable
on its own. Forcing the pipeline forces the deployment first, since a link
needs a CID to point at.
.constructor(environment: Environment, deployment: Deployment)
constructor
constructor(environment: Environment, deployment: Deployment): DeploymentPipelineActionSee Deployment, Environment.
deployment: Deployment
property
See Deployment.
environment: Environment
property
See Environment.
Diagnostics
class
A content-addressed build artifact: a lazy, memoized computation of a CID.
Composition on the subclasses is synchronous and builds a graph; nothing runs
until the artifact is forced. Force with cid (or the inherited
get(), which it aliases).
.constructor(source: ArtifactSource, labelHint?: string)
constructor
constructor(source: ArtifactSource, labelHint?: string): DiagnosticsSee ArtifactSource.
nodeLabel
accessor
shape
accessor
valueScope
accessor
.cid(inherited?: TaskSlot)
method
Force and return this artifact's CID. Alias of the inherited get(),
kept because .cid() reads better at a build's call sites. inherited
is passed by a consuming composition step — see Lazy.get.
cid(inherited?: TaskSlot): Promise<string>.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<string>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.map(f: (…) => …)
method
Derive a new lazy value from this one without forcing it now. Inherits this value's scope, so composition stays in the subtree that owns it.
map(f: (…) => …): Lazy<U>.dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …)
static method
A dynamic artifact: one whose subgraph cannot be known until deps
have resolved, because deciding what to build requires reading them.
Called on the concrete class, which is what types the result:
const crates = Directory.dynamic("compile each crate", [src], async () =>
Directory.merge(...(await src.entries()).map(([n, e]) => compile(n, e))))
.in(scope);label is what this node is called in the build tree — the one thing a
dynamic node cannot derive, since it runs no node function of its own.
Where it goes is [in][Lazy.in]'s business, like every other value; a
dynamic artifact with no scope groups its expansion under whichever
consumer forces it.
This is the only asynchronous surface in the API: everything else composes synchronously. The graph has a hole inside a dynamic node, but every edge around it is static — the returned handle is an ordinary artifact, so downstream work is declared normally.
deps are forced before expand runs, and are the node's declared
dependency edges.
dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …): TSee Artifact, ArtifactSource.
.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.fromCid(cid: string)
static method
A Diagnostics from an existing DataType::Diagnostics CID.
fromCid(cid: string): Diagnostics.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>Directory
class
A content-addressed build artifact: a lazy, memoized computation of a CID.
Composition on the subclasses is synchronous and builds a graph; nothing runs
until the artifact is forced. Force with cid (or the inherited
get(), which it aliases).
.constructor(source: ArtifactSource, labelHint?: string)
constructor
constructor(source: ArtifactSource, labelHint?: string): DirectorySee ArtifactSource.
nodeLabel
accessor
shape
accessor
valueScope
accessor
.at(path: string)
method
This tree placed under directory path (empty intermediate dirs created)
— the inverse of resolve. Pair with merge to graft a
subtree into another at a subpath, at the content level (no copies):
Directory.merge(client.at("src/gen"), project).
at(path: string): Directory.cid(inherited?: TaskSlot)
method
Force and return this artifact's CID. Alias of the inherited get(),
kept because .cid() reads better at a build's call sites. inherited
is passed by a consuming composition step — see Lazy.get.
cid(inherited?: TaskSlot): Promise<string>.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.diff(other: Directory)
method
Path-level diff against another tree.
diff(other: Directory): Promise<DiffResult>See DiffResult.
.entries()
method
The direct entries of this directory as [name, Directory], sorted by name.
Genuinely async: it reads the directory node. Use it to fan out — e.g.
trigger a child build per entry, then Directory.merge(...results).
entries(): Promise<[string, Directory][]>.file(path: string, opts?: { … })
method
The payload of the file at path, as a File (content not read).
file(path: string, opts?: { … }): FileSee File.
.filterGlob(...patterns: string[])
method
This tree filtered to the paths matching any of the glob patterns.
filterGlob(...patterns: string[]): Directory.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<string>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.map(f: (…) => …)
method
Derive a new lazy value from this one without forcing it now. Inherits this value's scope, so composition stays in the subtree that owns it.
map(f: (…) => …): Lazy<U>.resolve(path: string, opts?: { … })
method
The subtree at path (a directory or file node).
resolve(path: string, opts?: { … }): Directory.setMetadata(path: string, meta: Metadata)
method
This tree with metadata changed at path.
setMetadata(path: string, meta: Metadata): DirectorySee Metadata.
.sources()
method
How this tree was assembled, as (prefix → origin) entries.
A tool that ran over this tree reports paths in its coordinates; these
entries are what turns one back into a path inside the member it came
from. See locate.
sources(): SourceOrigin[]See SourceOrigin.
.tar(compression?: Compression)
method
This tree as a (optionally compressed) tar archive File.
tar(compression?: Compression): FileSee Compression, File.
.dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …)
static method
A dynamic artifact: one whose subgraph cannot be known until deps
have resolved, because deciding what to build requires reading them.
Called on the concrete class, which is what types the result:
const crates = Directory.dynamic("compile each crate", [src], async () =>
Directory.merge(...(await src.entries()).map(([n, e]) => compile(n, e))))
.in(scope);label is what this node is called in the build tree — the one thing a
dynamic node cannot derive, since it runs no node function of its own.
Where it goes is [in][Lazy.in]'s business, like every other value; a
dynamic artifact with no scope groups its expansion under whichever
consumer forces it.
This is the only asynchronous surface in the API: everything else composes synchronously. The graph has a hole inside a dynamic node, but every edge around it is static — the returned handle is an ordinary artifact, so downstream work is declared normally.
deps are forced before expand runs, and are the node's declared
dependency edges.
dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …): TSee Artifact, ArtifactSource.
.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.fromCid(cid: string)
static method
A Directory from an existing CID (e.g. a member's root).
fromCid(cid: string): Directory.merge(...layers: Directory[])
static method
Overlay-merge trees left→right (later layers win).
The source maps concatenate in the same order, so a lookup that finds two entries for one path prefers the later — the layer whose bytes actually survived the merge.
merge(...layers: Directory[]): Directory.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>.textFile(text: string, options?: { … })
static method
A single-file tree holding text — the root is the file, so it goes
straight to a function that reads one file (k8s.apply's root), or
under a name with at: Directory.textFile(yaml).at("job.yaml").
The text is stored as a blob and fs.file wraps it as a regular file
(mode defaults to 0o644): the node writes the FsNode in the store's
own encoding, typed fs-node, so bldr inspect and the console show a
file rather than a bare map. No process runs, and the CID is a function
of the text and the mode alone.
textFile(text: string, options?: { … }): DirectoryencodeUtf8(text: string)
function
The UTF-8 bytes of text. A lone surrogate encodes as U+FFFD, as
TextEncoder does.
encodeUtf8(text: string): number[]Environment
class
A deployment environment: a name plus free-form labels.
.constructor(props: EnvironmentProps)
constructor
constructor(props: EnvironmentProps): EnvironmentSee EnvironmentProps.
labels: Readonly<Record<string, string>>
property
name: string
property
exitedWith(what: string, code: number, log: string, maxLines?: number)
function
<what> exited with code <code>, followed by the end of its log.
The error a build shows for a failed command. The end of the log is where a
command says why it stopped — cargo's error: blocks, a test harness's
summary — and bounding it keeps a log of thousands of Compiling lines from
becoming an error of thousands of lines.
exitedWith(what: string, code: number, log: string, maxLines?: number): stringFile
class
A content-addressed build artifact: a lazy, memoized computation of a CID.
Composition on the subclasses is synchronous and builds a graph; nothing runs
until the artifact is forced. Force with cid (or the inherited
get(), which it aliases).
.constructor(source: ArtifactSource, labelHint?: string)
constructor
constructor(source: ArtifactSource, labelHint?: string): FileSee ArtifactSource.
nodeLabel
accessor
shape
accessor
valueScope
accessor
.bytes()
method
The full byte content.
bytes(): Promise<Uint8Array<ArrayBufferLike>>.cid(inherited?: TaskSlot)
method
Force and return this artifact's CID. Alias of the inherited get(),
kept because .cid() reads better at a build's call sites. inherited
is passed by a consuming composition step — see Lazy.get.
cid(inherited?: TaskSlot): Promise<string>.compress(compression?: Compression)
method
This file, compressed (default: gzip).
compress(compression?: Compression): FileSee Compression.
.decompress(compression: Compression)
method
This file, decompressed with the given codec.
decompress(compression: Compression): FileSee Compression.
.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<string>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.length()
method
The total byte length — reads only the file's part list, not its data.
length(): Promise<number>.map(f: (…) => …)
method
Derive a new lazy value from this one without forcing it now. Inherits this value's scope, so composition stays in the subtree that owns it.
map(f: (…) => …): Lazy<U>.read(from: number, until?: number)
method
The bytes of the range [from, until).
read(from: number, until?: number): Promise<Uint8Array<ArrayBufferLike>>.slice(from: number, until?: number)
method
A byte range [from, until) of this file (without reading content).
slice(from: number, until?: number): File.text()
method
The full content as UTF-8 text.
Here rather than at each call site because the build runtime is hardened:
TextDecoder is not among the globals it keeps, so decoding bytes in
build code fails at the point of use with a ReferenceError that says
nothing about why. The host already knows how to hand back text.
text(): Promise<string>.untar(compression?: Compression)
method
Extract this file as a tar archive into a directory tree.
untar(compression?: Compression): DirectorySee Compression, Directory.
.dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …)
static method
A dynamic artifact: one whose subgraph cannot be known until deps
have resolved, because deciding what to build requires reading them.
Called on the concrete class, which is what types the result:
const crates = Directory.dynamic("compile each crate", [src], async () =>
Directory.merge(...(await src.entries()).map(([n, e]) => compile(n, e))))
.in(scope);label is what this node is called in the build tree — the one thing a
dynamic node cannot derive, since it runs no node function of its own.
Where it goes is [in][Lazy.in]'s business, like every other value; a
dynamic artifact with no scope groups its expansion under whichever
consumer forces it.
This is the only asynchronous surface in the API: everything else composes synchronously. The graph has a hole inside a dynamic node, but every edge around it is static — the returned handle is an ordinary artifact, so downstream work is declared normally.
deps are forced before expand runs, and are the node's declared
dependency edges.
dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …): TSee Artifact, ArtifactSource.
.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.fromCid(cid: string)
static method
A File from an existing CID.
fromCid(cid: string): File.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>fromBiomeJson(source: Directory, paths: string | string[], opts?: DiagnosticsOptions)
function
Postprocess Biome's JSON report
(biome ci --reporter=json --reporter-file=…) into a Diagnostics.
paths are glob(s) matched against source (a tree holding the report
file(s)), so a project that lints in several passes is collected in one call.
Its output type is DataType::Diagnostics, which is what tells a consumer
there are findings to rank, group and jump to rather than a file to browse.
fromBiomeJson(source: Directory, paths: string | string[], opts?: DiagnosticsOptions): DiagnosticsSee Diagnostics, DiagnosticsOptions, Directory.
fromCargoJson(source: Directory, paths: string | string[], opts?: DiagnosticsOptions)
function
Postprocess cargo's newline-delimited JSON
(cargo check --message-format=json, and the same from clippy) into a
Diagnostics. paths are glob(s) matched against source.
The richest of the converters, because rustc is the most forthcoming tool:
the offending span with byte offsets and the source line it came from, the
secondary spans that explain it, the notes beneath, and the suggested
replacement with rustc's own word for how safe it is. Clippy's findings
arrive on the same stream and are told apart by their clippy:: code.
fromCargoJson(source: Directory, paths: string | string[], opts?: DiagnosticsOptions): DiagnosticsSee Diagnostics, DiagnosticsOptions, Directory.
fromJestJson(source: Directory, paths: string | string[])
function
Postprocess Jest/Vitest JSON (vitest run --reporter=json --outputFile=…, or
jest --json) into a Report. paths are glob(s) matched against
source (a tree holding the report file(s)). Its output type is
DataType::TestResults, which is what tells bldr test to inspect the cases.
fromJestJson(source: Directory, paths: string | string[]): ReportfromLcov(source: Directory, paths: string | string[], opts?: CoverageConvertOptions)
function
Convert LCOV (cargo llvm-cov --lcov, grcov, gcov's own output) into a
Coverage.
LCOV has no region counters, so a report converted from it has line and
function coverage and nothing finer. Same options as fromLlvmJson.
fromLcov(source: Directory, paths: string | string[], opts?: CoverageConvertOptions): CoverageSee Coverage, CoverageConvertOptions, Directory.
fromLibtestJson(source: Directory, paths: string | string[])
function
Postprocess cargo test's libtest JSON into a Report. paths are
glob(s) matched against source (a tree holding the report file(s)), so a
project with several test binaries — hence several .jsons — is collected and
merged in one call. Its output type is DataType::TestResults, which is what
tells bldr test to inspect the cases.
fromLibtestJson(source: Directory, paths: string | string[]): ReportfromLlvmJson(source: Directory, paths: string | string[], opts?: CoverageConvertOptions)
function
Convert LLVM's own JSON export — what cargo llvm-cov --json writes — into a
Coverage. paths are glob(s) matched against source, a tree holding
the report file(s).
This is the lossless one: it carries regions, which is the only counter that can tell two branches on one line apart. Prefer it to LCOV unless something downstream demands LCOV.
fromLlvmJson(source: Directory, paths: string | string[], opts?: CoverageConvertOptions): CoverageSee Coverage, CoverageConvertOptions, Directory.
httpGet(url: string, opts?: HttpOptions)
function
httpGet(url: string, opts?: HttpOptions): Promise<HttpResponse>See HttpOptions, HttpResponse.
httpPost(url: string, body?: File, opts?: HttpOptions)
function
httpPost(url: string, body?: File, opts?: HttpOptions): Promise<HttpResponse>See File, HttpOptions, HttpResponse.
httpPut(url: string, body?: File, opts?: HttpOptions)
function
httpPut(url: string, body?: File, opts?: HttpOptions): Promise<HttpResponse>See File, HttpOptions, HttpResponse.
invoke(fn: string, input: Record<string, unknown>, slot?: TaskSlot)
function
Run a node function with a structured input now; resolves to the output CID.
slot is the tree node the work appears under. For a truly-async read, not
for composition — that is op.
invoke(fn: string, input: Record<string, unknown>, slot?: TaskSlot): Promise<string>invokeJson(fn: string, input: Record<string, unknown>, slot?: TaskSlot)
function
Run a node function now and read its output block as JSON.
invokeJson(fn: string, input: Record<string, unknown>, slot?: TaskSlot): Promise<T>isArtifact(value: unknown)
function
Whether a value is a build Artifact (a Directory, File, ContainerImage or
Report).
isArtifact(value: unknown): value is ArtifactSee Artifact.
languageServer(opts: LanguageServerOptions)
function
Wrap a stdio language server as a v1 LSP runnable.
The command is not run directly. It is run with its standard input and output bound to the two FIFOs, which have to exist before it starts and have to be created inside the pod — a FIFO is a filesystem object, and mounting one from outside would put a host path in a content-addressed tree.
The shell line is doing three things worth naming:
mkfifo -m 600— the pipes are private to the pod's user. There is no other consumer inside, and the daemon reaches them through the pod, not the filesystem.exec— the shell replaces itself with the server, so signals reach the server rather than a shell that would have to forward them, and the pod's main process is the thing whose exit actually means something.< in > out— the redirections open both pipes. This blocks until the other end of each is opened, which is deliberate: a language server that started before anyone was listening would write its first frames into a pipe with no reader and getSIGPIPEfor it.
languageServer(opts: LanguageServerOptions): LanguageServerSee LanguageServer, LanguageServerOptions.
languageServerLabels(server: LanguageServer, extra?: Record<string, string>)
function
The labels that mark a runnable as a v1 language server and say what it
claims.
Everything the daemon needs that it cannot discover for itself. It can discover where each mount came from — content addressing makes a mounted tree's origin findable — so no path appears here. Recording one would be recording something already true elsewhere, and the two would drift.
languageServerLabels(server: LanguageServer, extra?: Record<string, string>): Record<string, string>See LanguageServer.
link(cid: string)
function
Wrap a CID string as an IPLD link.
link(cid: string): Linklocate(sources: readonly SourceOrigin[], path: string)
function
The origin a path belongs to: the longest matching prefix, and among equal prefixes the last entry.
Longest wins because a tree assembled from bldr-types and then patched at
bldr-types/src/gen has both as prefixes, and the more specific one is the
answer. Last wins on a tie because that is what fs.merge does — later
layers overwrite earlier ones, so the later origin is whose bytes survived.
Returns the entry and the path relative to it, which together are what makes
a location openable: bldr-types/src/cid.rs in a merged workspace is
src/cid.rs in bldr/types.
locate(sources: readonly SourceOrigin[], path: string): { … }See SourceOrigin.
LSP_CLAIMS_LABEL: "bldr.runnable.lsp.claims"
variable
Which files this server answers about, relative to a mounted tree, as
comma-separated globs — "**/*.rs,**/Cargo.toml".
The daemon can work out where a server's mounts came from (a mounted tree is content-addressed, so the member whose content it is can be found by looking), but not what it is for. rust-analyzer and a TypeScript server mounted on the same tree claim disjoint halves of it, and nothing about the mount says which half. Only the build knows, so the build says.
LSP_PIPE_DIR: "/run/lsp"
variable
Where a v1 language server reads and writes.
Named pipes rather than a socket: a language server built for an editor reads
LSP frames from stdin and writes them to stdout, and two FIFOs are the
smallest thing that turns that program into a service without patching it.
rust-analyzer needs no argument to speak this way; neither does any other
server worth wrapping.
Under /run rather than /dev. /dev in a container is a small tmpfs the
runtime populates with the device nodes it decided the pod should have, and
adding a directory of our own to it invites a collision with a runtime that
has its own opinion — including a future one. /run is the standard place for
exactly this: mutable, tmpfs-backed state belonging to a running service,
discarded when it stops. FIFOs are that, precisely.
LSP_PIPE_IN: "/run/lsp/in"
variable
LSP_PIPE_OUT: "/run/lsp/out"
variable
LSP_PRIORITY_LABEL: "bldr.runnable.lsp.priority"
variable
Which server wins when two claim the same file, as an integer. Higher wins; ties go to the more deeply-rooted mapping. Absent means zero.
LSP_RUNNABLE_LABEL: "bldr.runnable.lsp"
variable
The label the daemon looks for, and the contract version it names.
A version rather than a boolean because "how to talk to this" is a contract: where the pipes are, what is written on them. A daemon that meets a version it does not implement skips the runnable rather than guessing.
LSP_RUNNABLE_VERSION: "v1"
variable
LSP_SERVICE: "lsp"
variable
The service name a single-server runnable uses.
memberScope(name: string)
function
The root scope of the member named name — its subtree of the build tree.
Called by the generated per-member module to define its scope global; build
code uses that global, or names another member's directly.
memberScope(name: string): ScopeSee Scope.
Metrics
class
A content-addressed build artifact: a lazy, memoized computation of a CID.
Composition on the subclasses is synchronous and builds a graph; nothing runs
until the artifact is forced. Force with cid (or the inherited
get(), which it aliases).
.constructor(source: ArtifactSource, labelHint?: string)
constructor
constructor(source: ArtifactSource, labelHint?: string): MetricsSee ArtifactSource.
nodeLabel
accessor
shape
accessor
valueScope
accessor
.cid(inherited?: TaskSlot)
method
Force and return this artifact's CID. Alias of the inherited get(),
kept because .cid() reads better at a build's call sites. inherited
is passed by a consuming composition step — see Lazy.get.
cid(inherited?: TaskSlot): Promise<string>.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<string>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.map(f: (…) => …)
method
Derive a new lazy value from this one without forcing it now. Inherits this value's scope, so composition stays in the subtree that owns it.
map(f: (…) => …): Lazy<U>.dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …)
static method
A dynamic artifact: one whose subgraph cannot be known until deps
have resolved, because deciding what to build requires reading them.
Called on the concrete class, which is what types the result:
const crates = Directory.dynamic("compile each crate", [src], async () =>
Directory.merge(...(await src.entries()).map(([n, e]) => compile(n, e))))
.in(scope);label is what this node is called in the build tree — the one thing a
dynamic node cannot derive, since it runs no node function of its own.
Where it goes is [in][Lazy.in]'s business, like every other value; a
dynamic artifact with no scope groups its expansion under whichever
consumer forces it.
This is the only asynchronous surface in the API: everything else composes synchronously. The graph has a hole inside a dynamic node, but every edge around it is static — the returned handle is an ordinary artifact, so downstream work is declared normally.
deps are forced before expand runs, and are the node's declared
dependency edges.
dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …): TSee Artifact, ArtifactSource.
.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.fromCid(cid: string)
static method
A Metrics from an existing line-protocol blob CID.
fromCid(cid: string): Metrics.fromFile(file: Artifact)
static method
A Metrics from a file artifact whose content is line-protocol rows —
e.g. pod.output("out").at("metrics.txt") for a pod that wrote its own
numbers. The daemon accepts both a bare blob and an FsNode file.
fromFile(file: Artifact): MetricsSee Artifact.
.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>metricsFromCoverage(source: Coverage, opts?: MetricsTagOptions & { … })
function
Turn a Coverage report into metrics rows.
granularity: "summary" (default) emits one coverage row of workspace
totals; "member" emits one per member with the member tag.
metricsFromCoverage(source: Coverage, opts?: MetricsTagOptions & { … }): MetricsSee Coverage, Metrics, MetricsTagOptions.
metricsFromPodDag(result: Artifact, opts?: MetricsTagOptions)
function
Turn a pod.dag result into metrics rows: one pod_op row per op
(op/action tags) carrying duration_ms, exit_code, cpu_usage_usec
and memory_peak_bytes — whatever the op recorded.
metricsFromPodDag(result: Artifact, opts?: MetricsTagOptions): MetricsSee Artifact, Metrics, MetricsTagOptions.
metricsFromTestResults(source: Report, opts?: MetricsTagOptions & { … })
function
Turn a test Report into metrics rows.
granularity: "case" (default) emits one test_case row per case with
name/status tags and duration_ms/count fields; "summary" emits a
single tests row with total/passed/failed/skipped counts.
metricsFromTestResults(source: Report, opts?: MetricsTagOptions & { … }): MetricsSee Metrics, MetricsTagOptions, Report.
metricsTransform(sources: Metrics | Metrics[], opts?: MetricsTransformOptions)
function
Merge one or more Metrics blobs and optionally filter/aggregate the
result, as a build step — so a target can carry a distilled series (say,
per-suite duration sums) instead of, or beside, the raw rows.
metricsTransform(sources: Metrics | Metrics[], opts?: MetricsTransformOptions): MetricsSee Metrics, MetricsTransformOptions.
narrowSources(sources: readonly SourceOrigin[], path: string)
function
The entries that still apply after narrowing to path — what .resolve()
does.
Two cases, and both matter. An entry containing path (a whole member,
narrowed to one directory inside it) becomes the root of the result, since
everything left came from it. An entry inside path (a layer that was
patched into the part being kept) survives, re-rooted. An entry that neither
contains nor is contained by path describes a part that was narrowed away.
narrowSources(sources: readonly SourceOrigin[], path: string): SourceOrigin[]See SourceOrigin.
nestSources(sources: readonly SourceOrigin[], path: string)
function
Every entry of sources, moved under path — what .at(path) does.
nestSources(sources: readonly SourceOrigin[], path: string): SourceOrigin[]See SourceOrigin.
nodeCapabilities()
function
Every capability the node advertises — for diagnostics and reporting.
nodeCapabilities(): string[]nodeSupports(name: string)
function
Does the node running this build support name?
if (nodeSupports("pod.cache-volumes")) pod.cache(volume, "/target");A name nobody advertises answers false, which is what makes it safe to
check for something that does not exist yet.
nodeSupports(name: string): booleanNoopPipelineAction
class
No action: the step exists to sequence or gate others. What a step stores when its props declare no action.
.constructor()
constructor
constructor(): NoopPipelineActionNotPipelineCondition
class
Negation of one condition.
.constructor(condition: PipelineCondition)
constructor
constructor(condition: PipelineCondition): NotPipelineConditionSee PipelineCondition.
condition: PipelineCondition
property
See PipelineCondition.
.allOf(...conditions: PipelineCondition[])
static method
Holds when every one of conditions holds (none: always holds).
allOf(...conditions: PipelineCondition[]): AllOfPipelineConditionSee AllOfPipelineCondition, PipelineCondition.
.anyOf(...conditions: PipelineCondition[])
static method
Holds when at least one of conditions holds (none: never holds).
anyOf(...conditions: PipelineCondition[]): AnyOfPipelineConditionSee AnyOfPipelineCondition, PipelineCondition.
.noneOf(...conditions: PipelineCondition[])
static method
Holds when none of conditions hold — not(anyOf(…)), spelled the
way a reader thinks it.
noneOf(...conditions: PipelineCondition[]): NotPipelineConditionSee PipelineCondition.
.not(condition: PipelineCondition)
static method
Holds when condition does not.
not(condition: PipelineCondition): NotPipelineConditionSee PipelineCondition.
Pipeline
class
A content-addressed build artifact: a lazy, memoized computation of a CID.
Composition on the subclasses is synchronous and builds a graph; nothing runs
until the artifact is forced. Force with cid (or the inherited
get(), which it aliases).
.constructor(source: ArtifactSource | PipelineSpec, labelHint?: string)
constructor
A pipeline authored in place — new Pipeline({ name: "bldr" }), then
addStep for each step, before anything forces the CID.
The ArtifactSource form is what fromCid and Artifact.dynamic use
to wrap an existing CID; build code writes the spec form.
constructor(source: ArtifactSource | PipelineSpec, labelHint?: string): PipelineSee ArtifactSource, PipelineSpec.
nodeLabel
accessor
shape
accessor
valueScope
accessor
.addStep(props: PipelineStepProps)
method
Add a step. Returns the step, which is the handle later steps name in
dependsOn — dependencies are references to steps of this pipeline,
never bare strings, so an edge to a missing or foreign step cannot be
written. Ids are assigned in declaration order (step-0, step-1, …),
which keeps the stored block deterministic.
addStep(props: PipelineStepProps): PipelineStepSee PipelineStep, PipelineStepProps.
.cid(inherited?: TaskSlot)
method
Force and return this artifact's CID. Alias of the inherited get(),
kept because .cid() reads better at a build's call sites. inherited
is passed by a consuming composition step — see Lazy.get.
cid(inherited?: TaskSlot): Promise<string>.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<string>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.map(f: (…) => …)
method
Derive a new lazy value from this one without forcing it now. Inherits this value's scope, so composition stays in the subtree that owns it.
map(f: (…) => …): Lazy<U>.dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …)
static method
A dynamic artifact: one whose subgraph cannot be known until deps
have resolved, because deciding what to build requires reading them.
Called on the concrete class, which is what types the result:
const crates = Directory.dynamic("compile each crate", [src], async () =>
Directory.merge(...(await src.entries()).map(([n, e]) => compile(n, e))))
.in(scope);label is what this node is called in the build tree — the one thing a
dynamic node cannot derive, since it runs no node function of its own.
Where it goes is [in][Lazy.in]'s business, like every other value; a
dynamic artifact with no scope groups its expansion under whichever
consumer forces it.
This is the only asynchronous surface in the API: everything else composes synchronously. The graph has a hole inside a dynamic node, but every edge around it is static — the returned handle is an ordinary artifact, so downstream work is declared normally.
deps are forced before expand runs, and are the node's declared
dependency edges.
dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …): TSee Artifact, ArtifactSource.
.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.fromCid(cid: string)
static method
A Pipeline from an existing DataType::Pipeline CID.
fromCid(cid: string): Pipeline.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>PipelineAction
class
What a step does. Abstract: the family is open — each concrete action
serialises to { "type": "<tag>", … }, one variant of the Rust
PipelineAction enum.
.constructor()
constructor
constructor(): PipelineActionPipelineCondition
class
A gate on a step. Abstract: the family is open — each concrete condition
serialises to { "type": "<tag>", … }, one variant of the Rust
PipelineCondition enum.
The combinators are static helpers here (PipelineCondition.allOf(a, b))
and ordinary members of the family in the block: combination is data, not
syntax, so a stored condition tree is uniform.
.constructor()
constructor
constructor(): PipelineCondition.allOf(...conditions: PipelineCondition[])
static method
Holds when every one of conditions holds (none: always holds).
allOf(...conditions: PipelineCondition[]): AllOfPipelineCondition.anyOf(...conditions: PipelineCondition[])
static method
Holds when at least one of conditions holds (none: never holds).
anyOf(...conditions: PipelineCondition[]): AnyOfPipelineCondition.noneOf(...conditions: PipelineCondition[])
static method
Holds when none of conditions hold — not(anyOf(…)), spelled the
way a reader thinks it.
noneOf(...conditions: PipelineCondition[]): NotPipelineConditionSee NotPipelineCondition.
.not(condition: PipelineCondition)
static method
Holds when condition does not.
not(condition: PipelineCondition): NotPipelineConditionSee NotPipelineCondition.
PipelineStep
class
One step of a pipeline. Constructed by Pipeline.addStep — the value
of holding one is naming it in another step's dependsOn.
id: string
property
Identifies the step within its pipeline (step-0, step-1, …).
Pod
class
.constructor(rootfs: string | Directory)
constructor
rootfs is an image reference, a rootfs CID, or a rootfs Directory.
constructor(rootfs: string | Directory): PodSee Directory.
.allowExit(...codes: number[])
method
Accept these exit codes as success. By default only 0 is accepted and
anything else fails the build at this pod's task, with its log.
Pass no codes to accept any exit — for a command whose non-zero exit is data rather than failure (a test harness reporting failures through its output, a linter whose verdict you want to branch on).
allowExit(...codes: number[]): this.allowRunning(on?: boolean)
method
Let this pod's captures read a filesystem that is still being written.
A capture is otherwise only valid for a run that finished, and the
daemon enforces it twice: a snapshot must be ordered after something
that establishes its pod exited, and the node refuses to capture a pod
still running. The reason is that a live overlay is read mid-write, so
what comes back is whatever the pod had got to by then — and pod.dag's
result is stored in the invocation cache under the spec's CID, so a
single torn tree would be replayed as the answer for every later
identical build. That is the bug this defaults against.
Some callers genuinely want the live read — peeking at a long build's progress, inspecting a pod that is stuck — and the overlay's cache hygiene is written to tolerate it. This is the opt-out for them, with the understanding that the result is a point-in-time observation, not a reproducible build output.
A pod driven from here always runs to completion before its captures
dispatch (run { wait: true }), so this changes nothing for the ordinary
run-then-capture shape; it is the escape hatch for a capture the daemon
would otherwise refuse as "still running".
allowRunning(on?: boolean): this.cache(volume: CacheVolume, dest: string)
method
Mount a CacheVolume at dest: a writable directory the daemon
keeps between runs, seeded from what the volume held last time and written
back when this pod exits.
It is not an input. The volume's name reaches the pod's spec; its content
never does, so mounting a cache cannot change what the pod produces or
which cached result a repeat of this pod is served — see cache.ts.
Requires the pod to run to completion (every Pod does), because
"when it exits" is when the write-back happens.
cache(volume: CacheVolume, dest: string): thisSee CacheVolume.
.cacheCapture(volume: CacheVolume, subpath?: string)
method
Read subpath out of a mounted CacheVolume, merged.
The way to take an artifact out of a directory that is also a cache: a
cache volume is a separate mount, so it is not part of the pod's rootfs
diff and Pod.capture would find nothing there. Merged rather than
diffed, because an incremental build's whole point is that it did not
rewrite what it could reuse.
cacheCapture(volume: CacheVolume, subpath?: string): DirectorySee CacheVolume, Directory.
.capture(subpath: string)
method
A directory of the pod's rootfs changes (the overlay delta) at
subpath, as a Lazy Directory. Call it any number of times, for different
subpaths, before forcing — the pod runs once and every capture shares the
single snapshot. Forcing a capture of a failed run (non-zero exit) throws.
capture(subpath: string): DirectorySee Directory.
.captureMerged(subpath: string)
method
A directory of the pod's merged rootfs view (base + changes) at
subpath, as a Lazy Directory. Use this to read a path regardless of whether
the run touched it; use capture to read only what changed.
captureMerged(subpath: string): DirectorySee Directory.
.env(key: string, value: string)
method
Set an environment variable for the run (cleared env otherwise).
env(key: string, value: string): this.envAll(vars: Record<string, string>)
method
Set several environment variables at once.
envAll(vars: Record<string, string>): this.envLazy(source: Lazy<Record<string, string>>)
method
A lazily-resolved env source (e.g. an image's effective env), merged under any statically-set vars when the pod runs.
envLazy(source: Lazy<Record<string, string>>): this.exitCode()
method
The pod's exit code (forcing it runs the pod once).
Asking for the code implies allowExit with no codes: the point is
to report the exit, so a non-zero one must not fail the build first —
otherwise a non-zero value could never be observed.
exitCode(): Value<number>See Value.
.internet(on?: boolean)
method
Approve internet access for this pod's DAG: the pod and every
service beside it.
A DAG is isolated by default. Its pods share one network namespace of
their own — so they reach each other on 127.0.0.1 — and that namespace
has no way out: no package index, no registry, no release download. That
is what makes a pod's result a function of its declared inputs, and a
step that fetches is the exception a build has to name. This is how it
names it.
What is approved is the internet, not the host. The node's own loopback
stays out of reach either way; what the node serves to a pod (the
notification facade, sccache) arrives through the socket the matching
method binds into the pod (notify, sccache), never
through the network.
The approval is per DAG because the namespace is: asking for it on a
service approves the pod it serves as well. It reaches the DAG's spec, and
so its invocation-cache key. internet(false) takes the request back.
Accepted and emitted as nothing on a node that does not advertise
pod.internet: such a node runs its pods on the host's network, where
the approval is already satisfied.
internet(on?: boolean): this.keepMtime(on?: boolean)
method
Keep the pod's real file mtimes in everything this pod captures.
By default every capture normalises mtimes away, so a pod that produces
identical bytes produces an identical CID and the build cache keeps
working. (The pod's filesystem tracks real mtimes either way — make,
cargo and tsc --incremental see time advance inside the run; only the
captured content is normalised.)
Turn this on when the timestamps are part of what you are producing —
extracting an archive that must round-trip its stamps, an rsync-like
flow, a release tarball whose dates are meaningful. The output CID then
depends on the wall clock, so such a capture never hits the cache.
keepMtime(on?: boolean): this.logs()
method
The pod's captured stdout/stderr as a File (sealed once the pod
exits) — a build artifact you can scope.addOutputDirectory (wrapped),
fold into a report, or read for assertions.
logs(): FileSee File.
.mount(source: string | Directory, dest: string, opts?: MountOptions)
method
Mount a tree at dest (read-only by default; { mutable: true } for a
writable overlay whose diff you can capture with mountDelta).
mount(source: string | Directory, dest: string, opts?: MountOptions): thisSee Directory, MountOptions.
.mountDelta(id: string, subpath?: string)
method
The diff of a mutable mount (by its id, default m<index>) as a Lazy
Directory rooted at the mount point. Only meaningful for a mount created with
{ mutable: true }. Pass a subpath to re-root within it.
mountDelta(id: string, subpath?: string): DirectorySee Directory.
.mountMerged(id: string, subpath?: string)
method
The merged view of a mutable mount — what the pod could see there: the files it wrote on top of the ones the mount was seeded with.
This is the difference that matters for a CacheVolume: what a
cached build leaves behind is largely what it inherited, so the diff
alone is empty exactly when the cache did its job.
mountMerged(id: string, subpath?: string): DirectorySee Directory.
.notify(on?: boolean)
method
Give this pod the build-notification facade: a bldr.notify.v1
server on a Unix socket bound for this pod alone, the statically-linked
bldr-notify client at /bin/bldr-notify, and BLDR_NOTIFY_SOCKET
naming the socket.
It is how a pod reports what only it knows — the test case it is running, the file that failed to compile — to the build that started it, as structure rather than as log text. Everything else a build learns about a pod is observed from outside it: an exit code, a log stream, a captured tree.
Off unless asked for. The socket reaches the daemon, and that is authority a pod holds only because its build said so.
notify(on?: boolean): this.profile(on?: boolean)
method
CPU-profile this pod: the node samples its cgroup and seals collapsed
stacks on the pod, readable with bldr pod profile <id> or from the pod's
page in the console.
profile(false) opts out — of the node's profiling.enabled default,
and of a bldr build --profile that asked for everything. The narrower
statement wins.
Unlike a cache volume, this does change the pod's spec and therefore its invocation-cache key, so asking for a profile makes the pod run. That is the point: a profile of a pod that was served from the cache would be a profile of nothing.
profile(on?: boolean): this.resources(resources: DagResources)
method
What this run is admitted against, and then held to — see
DagResources.
pod.resources({ cpus: 8, memory: 12 * GiB })Dropped on a node that cannot read it, which runs the work on its own default share instead.
resources(resources: DagResources): thisSee DagResources.
.rootfsDelta()
method
The pod's entire rootfs delta (the overlay upper — everything the run
created, modified, or deleted, deletions marked as overlay whiteouts), as
a Lazy Directory rooted at /. This is a container image layer. Forcing a
delta of a failed run (non-zero exit) throws.
rootfsDelta(): DirectorySee Directory.
.rootfsMerged()
method
The pod's entire merged rootfs view (the base image with the run's
changes applied — what the container actually saw at /), as a Lazy
Directory. Unlike rootfsDelta this includes untouched base files.
Forcing a merged view of a failed run (non-zero exit) throws.
rootfsMerged(): DirectorySee Directory.
.run(command: Command)
method
The command to run (shell string, argv, or a thunk of either).
run(command: Command): thisSee Command.
.runtime(id: string)
method
Run this pod on a named container runtime (a runtimes: id in the node
config, e.g. a cloud-hypervisor VM runtime). Omit for the node's default runtime.
Selection is per-DAG: this one pod's run executes in that runtime.
runtime(id: string): this.sccache(opts?: false | SccacheOptions)
method
Give this pod sccache, served by the node: a socket bound for this pod
alone at /run/bldr-sccache.sock, the sccache wrapper the node ships
on PATH, and the variables that point the wrapper at the socket
(SCCACHE_SERVER_UDS, SCCACHE_CLIENT_SIDE). The wrapper compiles in
the pod; only cache entries cross the socket, and the node keeps them in
its content-addressed store, so every pod of every build on the cluster
shares one compile cache.
Nothing routes a compiler through it by itself — set RUSTC_WRAPPER or
cargo's build.rustc-wrapper (see sccacheCargoConfig), or prefix
a C compiler with sccache. bldr/rust-tools does this for its Cargo
pods.
Like a cache volume, it cannot change what the pod produces: the declaration reaches the pod's spec (and so its invocation-cache key); the cache's contents never do. Entries are the tenant's own — a pod never reads another tenant's, because the socket it is given is what decides whose cache it is.
mode: "read-only" looks entries up and stores nothing. sccache(false)
takes the request back.
Throws on a node that does not advertise pod.sccache: asked for
explicitly, a cache that silently is not there is a slow build nobody can
explain. Build code that wants it only where available checks
sccacheSupported first.
sccache(opts?: false | SccacheOptions): thisSee SccacheOptions.
.service(id: string, pod: Pod)
method
service(id: string, pod: Pod): this.shell(shell: string[])
method
The shell for shell-form commands (default ["/bin/sh", "-c"]).
shell(shell: string[]): this.user(user: string)
method
Run a shell-form command as this user (su; no-op for root).
user(user: string): this.waitHealthy(probe?: Command, opts?: { … })
method
Declare when this pod is ready to be used — the probe that answers it, and the bounds on waiting for that answer.
Only meaningful on a pod passed to service: a service is started
rather than waited on, so "the pod is running" is the last thing the DAG
knows about it, and that is strictly earlier than "the process inside is
listening". Whatever talks to it next would otherwise race the service and
lose intermittently, under exactly the load that makes the race likely.
probe runs inside the pod (an exec, so every runtime that can exec can
do this — native and VM alike) and is retried until it exits 0. Omit it
to use the image's own HEALTHCHECK, the same way omitting a command
uses the image's CMD: an image that ships a probe has already said how
to tell whether it is serving, and restating it here is a second copy to
keep in step. A build fails if neither names one.
etcd.waitHealthy(["etcdctl", "endpoint", "health"], { timeoutSecs: 60 })
postgres.waitHealthy() // the image's HEALTHCHECKThe wait is bounded twice over, and neither bound is optional: a total
timeoutSecs, and a maxAttempts cap so that a tiny intervalMs cannot
turn this into a spin of execs. Whichever is reached first ends the wait,
failing the build with the pod's name and the probe's last exit code — a
readiness wait that could not end would otherwise present as a hung build
with nothing to point at.
Progress is reported per attempt, so the wait is visible while it happens.
waitHealthy(probe?: Command, opts?: { … }): thisSee Command.
.workdir(dir: string)
method
Run a shell-form command from this directory (cd before it).
workdir(dir: string): thisregistrations()
function
Everything registered so far. The driver reads this after running the members' build functions.
registrations(): RegistrationsSee Registrations.
Report
class
A content-addressed build artifact: a lazy, memoized computation of a CID.
Composition on the subclasses is synchronous and builds a graph; nothing runs
until the artifact is forced. Force with cid (or the inherited
get(), which it aliases).
.constructor(source: ArtifactSource, labelHint?: string)
constructor
constructor(source: ArtifactSource, labelHint?: string): ReportSee ArtifactSource.
nodeLabel
accessor
shape
accessor
valueScope
accessor
.cid(inherited?: TaskSlot)
method
Force and return this artifact's CID. Alias of the inherited get(),
kept because .cid() reads better at a build's call sites. inherited
is passed by a consuming composition step — see Lazy.get.
cid(inherited?: TaskSlot): Promise<string>.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<string>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.map(f: (…) => …)
method
Derive a new lazy value from this one without forcing it now. Inherits this value's scope, so composition stays in the subtree that owns it.
map(f: (…) => …): Lazy<U>.merge(..._others: Report[])
method
Combine this report with others into one — summing totals and
concatenating cases. Planned composition feature (a test.merge node
function); the multi-binary case is already handled by
fromLibtestJson's globbing, which merges every matched report in one
call.
merge(..._others: Report[]): Report.dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …)
static method
A dynamic artifact: one whose subgraph cannot be known until deps
have resolved, because deciding what to build requires reading them.
Called on the concrete class, which is what types the result:
const crates = Directory.dynamic("compile each crate", [src], async () =>
Directory.merge(...(await src.entries()).map(([n, e]) => compile(n, e))))
.in(scope);label is what this node is called in the build tree — the one thing a
dynamic node cannot derive, since it runs no node function of its own.
Where it goes is [in][Lazy.in]'s business, like every other value; a
dynamic artifact with no scope groups its expansion under whichever
consumer forces it.
This is the only asynchronous surface in the API: everything else composes synchronously. The graph has a hole inside a dynamic node, but every edge around it is static — the returned handle is an ordinary artifact, so downstream work is declared normally.
deps are forced before expand runs, and are the node's declared
dependency edges.
dynamic(this: (…) => …, label: string, deps: readonly Artifact[], expand: (…) => …): TSee Artifact, ArtifactSource.
.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.fromCid(cid: string)
static method
A Report from an existing DataType::TestResults CID.
fromCid(cid: string): Report.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>resetRegistrations()
function
Forget everything. For tests, and for a driver that runs more than one build in a process.
resetRegistrations(): voidrootScope()
function
The tree's root. Build code should prefer its member's own scope.
rootScope(): ScopeSee Scope.
runnable()
function
Convenience constructor: runnable().service(...).
runnable(): RunnableSee Runnable.
Runnable
class
A runnable builder. Fluent: declare network(...)s and service(...)s, then
hand it to scope.addRunnable(name, runnable). The framework calls
materialize() to force all artifact CIDs into the stored block.
.constructor()
constructor
constructor(): Runnable.materialize()
method
Force every artifact CID and build the stored block. The field names match
the Rust Runnable struct's serde (snake_case) exactly.
materialize(): Promise<unknown>.network(name: string, def?: NetworkDef)
method
Declare a bridge network the services can attach to.
network(name: string, def?: NetworkDef): this.service(name: string, def: RunServiceDef)
method
Declare a service (a long-lived pod).
service(name: string, def: RunServiceDef): thisSee RunServiceDef.
SCCACHE_BIN_DIR: "/usr/local/lib/bldr/sccache"
variable
Where the node mounts the sccache wrapper inside the pod. The node also
prepends this directory to the pod's PATH, so sccache resolves by name.
SCCACHE_CAPABILITY: "pod.sccache"
variable
The node capability a pod needs before it may ask for sccache.
SCCACHE_SOCKET: "/run/bldr-sccache.sock"
variable
Where the node binds the pod's sccache socket ($SCCACHE_SERVER_UDS).
sccacheCargoConfig()
function
The Cargo configuration (TOML) that runs every rustc through the node's
wrapper: [build] rustc-wrapper.
By name, not by path — the node puts SCCACHE_BIN_DIR on PATH. A
config-file setting rather than RUSTC_WRAPPER in the environment, because
cargo ranks the environment above its config files: a build that sets its
own RUSTC_WRAPPER keeps it.
sccacheCargoConfig(): stringsccacheSupported()
function
Can the node running this build serve sccache to a pod?
sccacheSupported(): booleanScope
class
A named place in the build tree.
Obtained three ways:
- the ambient
scopeof the member being built (its root node); import { scope as foobarScope } from "foobar"— another member's root, for work performed on that member's behalf;Scope.childScope, for a subtree you own.
The underlying task node is created lazily, on first use, and only once. A scope that nothing ever runs in leaves no empty box in the tree, so a tool can offer fine-grained scopes without cluttering builds that skip that work. Creating a child forces its ancestors, so the chain always appears in order.
.constructor(name: string, parent?: Scope, options?: ScopeOptions)
constructor
Prefer Scope.childScope; the roots are supplied by the runtime.
constructor(name: string, parent?: Scope, options?: ScopeOptions): ScopeSee ScopeOptions.
name: string
property
options: ScopeOptions
property
See ScopeOptions.
.addCompanion(name: string, companion: string, register: (…) => …)
method
Register a companion target beside name and declare it a peer.
The two halves always went together and were written out separately, so one of them was sometimes forgotten: a companion registered without the peer exists, is derived, and is never selected — which is the same as not existing, except that it looks fine in the target list.
Companions live at <name>/<companion>, a tree path, so a narrowed build
forces one along with its base for the same reason a subtree filter does.
scope.addCompanion(n, "metrics", (c, m) => c.addMetrics(m, rows));
addCompanion(name: string, companion: string, register: (…) => …): void.addCoverage(name: string, report: Artifact)
method
Register a named test coverage output: a CoverageReport block
naming what the tests actually executed, indexed by member and then by
path within it.
Its own registration rather than an output directory because the
difference is what a consumer can do with it. A coverage report has a
percentage to show, files to rank by it, and a previous run to compare
against; handing back a directory of lcov.info puts every one of those
behind a file nobody can read. The value here is the converted block —
see coverage.from-* — not the tool's raw output.
addCoverage(name: string, report: Artifact): voidSee Artifact.
.addDeployment(name: string, functionName: string, options?: unknown, labels?: Record<string, string>)
method
Register a named deployment: a node function to invoke with options,
runnable on its own with bldr deploy <target>.
options is plain data, or an async function producing it. The function
form runs only when the deployment is selected, so a deployment whose
options embed a built CID (an image push) forces that build on demand
rather than every time the member's build file evaluates.
addDeployment(name: string, functionName: string, options?: unknown, labels?: Record<string, string>): void.addDiagnostics(name: string, diagnostics: Artifact, labels?: Record<string, string>)
method
Register a named diagnostics output: a Diagnostics block holding
the messages a tool emitted about code — compiler errors, lints, type
errors (see api/diagnostics.ts).
Its own registration for the same reason coverage and metrics are: what
a consumer does with findings is rank them by severity, group them by
rule and jump to the place each one names. A directory of report.json
puts every one of those behind a file nobody reads, and a test target
would reduce the set to a single bit. The value here is the converted
block — see diagnostics.from-* — not the tool's raw output.
Registering findings does not fail a build. Whether errors should stop anything is a policy question, and this is the report that policy would be applied to; a report that only exists when there is nothing to report is not a report.
addDiagnostics(name: string, diagnostics: Artifact, labels?: Record<string, string>): voidSee Artifact.
.addDocumentationDirectory(name: string, dir: Directory, labels?: Record<string, string>)
method
Register an output directory that is documentation, labelled so a consumer can find it without knowing its name.
Goes through addOutputDirectory rather than beside it, so
documentation is subject to every rule an ordinary output is, and the
role label is the only thing this adds.
addDocumentationDirectory(name: string, dir: Directory, labels?: Record<string, string>): voidSee Directory.
.addImage(name: string, image: ContainerImage, labels?: Record<string, string>)
method
Register a named container image output.
labels are target labels, selectable with [key=value] filters — the
way a matrix of images (one per architecture, engine, runtime) is built
a slice at a time. They are not the image's own OCI labels; set those on
the image with ContainerImage.label.
addImage(name: string, image: ContainerImage, labels?: Record<string, string>): voidSee ContainerImage.
.addLanguageServer(name: string, server: LanguageServer, labels?: Record<string, string>)
method
Register a named language server: a runnable an editor session can be routed to, marked with the label the daemon scans for.
Goes through addRunnable rather than beside it, so a language
server is subject to every rule an ordinary runnable is — and so the
well-known label is spelled once, where it cannot be got wrong.
addLanguageServer(name: string, server: LanguageServer, labels?: Record<string, string>): voidSee LanguageServer.
.addMetrics(name: string, metrics: Artifact)
method
Register a named metrics output: a blob of InfluxDB line-protocol
rows (see api/metrics.ts). Queryable via GetBuildMetrics with this
target's name as the target tag, shown by the CLI after a build and
in the build page's metrics tab.
Its own registration for the same reason coverage is: what a consumer does with metrics is chart and compare them, and a directory view of a numbers file offers none of that.
Convention: a companion for target <base> is <base>/metrics —
scope.childScope(base).addMetrics("metrics", …) — so the companion's
key is a tree path under its base. Narrowed builds (--export <full base name>:…)
force <base>/metrics along with <base> by that name.
addMetrics(name: string, metrics: Artifact): voidSee Artifact.
.addOutputDirectory(name: string, dir: Directory, labels?: Record<string, string>)
method
Register a named output directory — a filesystem tree the build
produces, exportable with bldr build --export <filter>:<path>.
labels describe what the tree is, travelling beside its CID so a
consumer can select it by role rather than by knowing its name — the same
bargain addRunnable makes. Prefer a role-specific wrapper where
one exists (addDocumentationDirectory), so the well-known label is
spelled in one place instead of at each call site.
An unlabelled output is stored exactly as it was before labels existed: an empty map is not recorded, so its build output is byte-identical.
addOutputDirectory(name: string, dir: Directory, labels?: Record<string, string>): voidSee Directory.
.addPeers(name: string, ...peers: string[])
method
Declare that building name should also build peers.
A selection says what was asked for, and some asks are incomplete on
their own. bldr build bldr/cli/binary runs a compiler that has just
finished saying things about the code — but the target carrying what it
said is a different one, so the findings were produced and dropped unless
whoever typed the command happened to name the diagnostics target too.
Requiring that is the kind of rule that is remembered right up until it
matters.
So a target declares its companions once, here, and every selection that takes it takes them. Peers are followed transitively, and a peer that names nothing declared is ignored rather than an error — a companion is an offer, and a build that does not declare one is not malformed.
Peers are not dependencies. Nothing is ordered by this and nothing is forced by it; a peer that fails fails on its own, and a target whose peer fails still succeeds. It only widens what a filter selected.
Each peer is a full target path — what target returns, so a
companion registered in a child scope is named
childScope(n).target("diagnostics") rather than assembled by hand.
addPeers(name: string, ...peers: string[]): void.addPipelineTarget(name: string, pipeline: Pipeline, labels?: Record<string, string>)
method
Register a named pipeline target: a Pipeline block holding a
declarative pipeline definition, authored in build code
(new Pipeline({ … }) — see api/pipeline.ts).
Its own registration because a consumer executes a pipeline rather
than browsing it: the block is a definition with a shape of its own, and
registering it as a directory would hide that behind a tree with nothing
in it to open. Only a Pipeline is accepted — the nominal brand is
what stops a directory or a report being registered as one.
addPipelineTarget(name: string, pipeline: Pipeline, labels?: Record<string, string>): voidSee Pipeline.
.addRunnable(name: string, run: Runnable, labels?: Record<string, string>)
method
Register a named runnable: networks and long-lived services, launched
with bldr run <target>.
labels describe what it is for, travelling beside its CID so a
consumer can find one by purpose without fetching every runnable block.
Prefer a role-specific wrapper where one exists — addLanguageServer sets the label the daemon looks for.
addRunnable(name: string, run: Runnable, labels?: Record<string, string>): voidSee Runnable.
.addTest(name: string, node: Artifact, labels?: Record<string, string>)
method
Register a named test: any build value whose CID resolves. Running the
test IS building that CID, so it is cached by its inputs like anything
else. It passes if the subgraph builds and — when the built CID is a
Report — contains no failed cases.
labels are target labels, selectable with [key=value] filters.
addTest(name: string, node: Artifact, labels?: Record<string, string>): voidSee Artifact.
.childScope(name: string, options?: ScopeOptions)
method
A child scope named name, for a subtree of work this scope owns.
Synchronous and free: nothing is created until work actually runs in it.
childScope(name: string, options?: ScopeOptions): ScopeSee ScopeOptions.
const cargoScope = scope.childScope("Cargo", { description: "Rust build" });
const tests = cargoScope.childScope("test");.node()
method
The host task node for this scope, created on first call and memoised.
Internal: build code passes Scope values around and never awaits them;
the invocation layer resolves the node when it actually spawns work.
node(): Promise<any>.path()
method
This scope's ancestors then itself, root first — its path in the tree.
path(): string[].target(name: string)
method
The target name name gets when registered here.
The scope's path with the build root dropped — that node is the tree's
frame, not part of anything's identity — so a target registered on the
tests subtree of member bldr/daemon is bldr/daemon/tests/unit.
A target's name is where it lives. There is no second naming scheme to keep in step, and a reader who can see the tree can predict the name.
target(name: string): stringsetWorkspaceOptions(options: unknown)
function
Publish the build's effective options. The driver calls this once per generation, before any member module is imported.
setWorkspaceOptions(options: unknown): voidshquote(s: string)
function
POSIX single-quote a string for safe interpolation into a shell command.
shquote(s: string): stringtailLines(text: string, maxLines: number)
function
The last maxLines lines of text, surrounding blank lines trimmed. When
lines were left out, the first line says how many, so a reader knows the
quote is not the whole log.
tailLines(text: string, maxLines: number): stringTimeWindowPipelineCondition
class
A cron-style time window: holds when the current time matches every field.
Each field is one cron field expression — *, a value (5, fri), a
range (9-17, mon-fri), a step (/15, 9-17/2; a bare /step reads
as every step-th from *), or a comma list of those — with standard cron
bounds (see TimeWindowProps).
Validated here, in the constructor, and again when a daemon decodes the
block (bldr-types/src/pipeline.rs runs the same rules): a window is
something a scheduler will evaluate, so an expression that cannot mean
anything must fail where it was written, not when the step comes due.
.constructor(props?: TimeWindowProps)
constructor
constructor(props?: TimeWindowProps): TimeWindowPipelineConditionSee TimeWindowProps.
dayOfMonth: string
property
dayOfWeek: string
property
hour: string
property
minute: string
property
month: string
property
.allOf(...conditions: PipelineCondition[])
static method
Holds when every one of conditions holds (none: always holds).
allOf(...conditions: PipelineCondition[]): AllOfPipelineConditionSee AllOfPipelineCondition, PipelineCondition.
.anyOf(...conditions: PipelineCondition[])
static method
Holds when at least one of conditions holds (none: never holds).
anyOf(...conditions: PipelineCondition[]): AnyOfPipelineConditionSee AnyOfPipelineCondition, PipelineCondition.
.noneOf(...conditions: PipelineCondition[])
static method
Holds when none of conditions hold — not(anyOf(…)), spelled the
way a reader thinks it.
noneOf(...conditions: PipelineCondition[]): NotPipelineConditionSee NotPipelineCondition, PipelineCondition.
.not(condition: PipelineCondition)
static method
Holds when condition does not.
not(condition: PipelineCondition): NotPipelineConditionSee NotPipelineCondition, PipelineCondition.
toYaml(value: unknown)
function
One YAML document for value, ending in a newline.
toYaml(value: unknown): stringunlink(value: unknown)
function
Extract the CID string from an IPLD link.
unlink(value: unknown): stringValue
class
A lazily-decoded non-CID value read out of the graph — an image's
effective config, a parsed manifest. Distinct from Artifact: it does
not name a block, so it is not an output, not a mount, and not something a
dependency edge can point at.
Note what is deliberately absent: a process exit code is not a Value.
Exit codes are assertions, not data (docs/pod-api.md, "Exit codes").
.constructor(compute: (…) => …, labelHint?: string)
constructor
constructor(compute: (…) => …, labelHint?: string): Value<T>
constructor(shape: Shape<T>, labelHint?: string): Value<T>nodeLabel
accessor
shape
accessor
valueScope
accessor
.dependsOn(...deps: unknown[])
method
Record what this value is waiting on.
Only values that are somewhere in the tree count — something with no node and no scope is nothing a reader could go and look at. Accepts anything so callers can pass a mixed bag of inputs (a CID string, a member) without filtering.
dependsOn(...deps: unknown[]): this.get(inherited?: TaskSlot)
method
Force the computation (memoized — subsequent calls reuse the result).
inherited is the node of the value being computed from this one. An
intermediate step of a composition — the resolve in
dir.resolve(…).filterGlob(…).in(scope) — has no scope of its own and is
private to that chain, so it belongs wherever its consumer went. A value
with its own scope ignores the offer and stays where it was placed.
Since the result is memoised, a shared unscoped value keeps whichever
parent forced it first. That is the ambiguity Scope exists to resolve:
give a value used in two places its own scope and it stops floating.
get(inherited?: TaskSlot): Promise<T>.in(scope: Scope)
method
Place this value's work in scope.
Fixed at declaration time, so a value shared by several consumers appears
once, where its owner put it — not under whichever consumer forced it
first. Derived values (map, artifact composition) inherit it.
in(scope: Scope): thisSee Scope.
.map(f: (…) => …)
method
Derive another value from this one without forcing it now.
map(f: (…) => …): Value<U>.expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …)
static method
A value whose subgraph is decided once deps have resolved: expand
is called with their values and returns the value this one stands for.
label names the node, since it runs no node function of its own.
expansion(label: string, deps: readonly Lazy<unknown>[], expand: (…) => …): Lazy<T>.node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …)
static method
The output of node function fn on the input input builds from the
resolved deps. The value is the output block's CID; read a field of it
with map.
node(fn: string, deps: readonly Lazy<unknown>[], input: (…) => …): Lazy<string>.of(value: T)
static method
Already-resolved value.
of(value: T): Lazy<T>workspaceOption(name: string)
function
One scalar workspace option as text, or undefined when it is unset, empty,
or not a scalar. A build file reads a per-build knob with a default this way:
const kubeconfig = workspaceOption("k8sTestKubeconfig") ?? "secret://k8s/current";A number or a boolean is returned in its text form. An option set on the
command line is text to whoever typed it, but -o k8sTestRun=2 reaches the
build as the number 2: a reader that took strings alone would treat the
knob as unset, and say nothing.
workspaceOption(name: string): stringworkspaceOptions()
function
The build's effective options, as published by the driver. Empty until it has, and under a driver too old to publish them.
workspaceOptions(): Record<string, unknown>yamlDocuments(values: unknown[])
function
Several values as one multi-document YAML stream: --- before each
document after the first, the form k8s.apply splits on.
yamlDocuments(values: unknown[]): string