Bufa Reference

This document describes every setting of a BUFA build config and of the .BUFA project root config, every environment variable Bufa reads or sets, and the two files a build dir can publish for its dependents: BUFA.env and BUFA.shell. The README explains the concepts; this file is the complete list.

Contents:

Conventions

Files. A directory becomes a build dir when it holds a file named BUFA, even a completely empty one: an empty BUFA has nothing to build, but it excludes the directory from its parent's source and makes it a valid deps.src target. A <name>.BUFA file declares a virtual build dir <name> inside the directory holding the file (same settings, no source of its own). A .BUFA file marks the project root. The whole *.BUFA suffix is reserved: no source file or [[deps.ext]] artifact may be named BUFA or *.BUFA (compared case-insensitively). BUFA.env and BUFA.shell are deliberately outside that rule.

Paths. Everywhere a setting names a project path (deps.*, shell, [shells]), a /-prefixed path is relative to the project root and a plain path is relative to the build dir (/build/antlr, ../Grammar, tools/nu). Relative paths in a virtual config resolve against the virtual dir, not the file's parent.

Typos fail. .BUFA is TOML or empty; BUFA is TOML, empty, or a raw script (below). In either TOML file an unknown key is an error, never silently ignored.

Two forms of BUFA. A BUFA file that does not parse as TOML is a raw script: the whole file becomes the cross-platform cmd, unsafe is implied (a raw script cannot declare deps, so it gets your real PATH), and every other setting is at its default. A raw script must not start with anything that parses as TOML, such as a NAME=… line or a [table] header. A file that parses but holds unknown keys is a broken config, not a script.

BUFA — build dir settings

All keys, with their defaults:

Key What Type Default Platform fold
cmd the build script string or false absent replace
shell shell running cmd string "" (inherit) replace
unsafe non-hermetic build bool false replace
largeOutput consumers link, not copy bool false replace
task run every time, no output bool or list of names false replace
deps.src source deps list of paths [] append
deps.bld built deps list of paths [] append
[[deps.ext]] pinned downloads list of tables [] append
deps.export deps shipped in output list of paths/names [] append
deps.cacheDir persistent cache dir bool false replace
[env] script env vars table empty merge per key
filters.src what is source list of rules ["-.**"] append
filters.bld what is output list of rules [] append
filters.dirty what dirty mode hashes list of rules ["-.**"] append
[windows]/[unix] per-platform overrides table empty —
[linux]/[macos] per-OS overrides on Unix table empty —

cmd

The build script, or false.

A root-table cmd is cross-platform. A platform section's cmd replaces it as a whole value on that platform: with a root cmd = "./build.sh" and [windows] cmd = "build.cmd", Windows runs build.cmd and every other platform runs ./build.sh. The replacement is total, so [windows] cmd = false silences a root script on Windows only. On Linux and macOS a [linux]/[macos] cmd in turn replaces the [unix] one, see Platform Sections.

shell

Which shell runs cmd. One of:

Precedence: platform section > root table > .BUFA root > native preset (cmd on Windows, bash elsewhere).

A provider-backed shell is an implied deps.bld: the provider builds first and is staged like any dep. Listing the provider in your own deps.bld is an error, and a provider that resolves to itself (it forgot to name its own native shell under a provider-backed default) is an error too. Exception: an alias that shadows a preset name ([shells] bash = "/build/bash") lets that provider build under the real preset.

A scriptless dir (cmd = false) resolves no shell unless --shell/--post-shell targets it. See 'BUFA.shell' for what a definition contains and how the presets behave.

unsafe

unsafe = true makes the build non-hermetic:

A raw-script BUFA is implicitly unsafe.

[[deps.ext]] downloads don't need unsafe=true: a pinned hash makes the fetch deterministic, so the build stays hermetic.

largeOutput

largeOutput = true declares that this dir's output is large and that consumers only read it. Every dir that lists it in deps.bld stages the output as one directory symlink into the store instead of copying it. A copy-staged dep is a disposable copy the consumer's script may edit, move, or delete; a linked tree is the store itself, so the script must treat it read-only, run and read it in place, and may not name it in deps.export. Clean mode only; deps.src staging and dirty builds are unaffected. No cache-key effect on consumers.

Caution

This is one of the "For Adults" aspects of Bufa: it's a great optimization, essential for making Shells and Toolchains regular build dependencies, but nothing stops a consumer's script from writing through the link into the store, which corrupts that output for every consumer.

Such corruptions could be detected with bufa check after the fact.
And bufa check --fix removes it from the store, so the next build republishes a clean copy.

task

task = true makes the dir a task: its script runs on every invocation and it publishes nothing. A deploy step is the typical one — a side effect, for which "same inputs, serve the cached output" is the wrong model:

# deploy.BUFA
task = true
deps.bld = ["/"]

cmd = '$BUFA_COPY_OR_MOVE ../bufa ~/bin'

bufa deploy builds / (cached as usual), stages deploy exactly as a build would — deps, own source, the hermetic environment, cwd, shell — and runs its script. Then, instead of publishing and printing a Build result: line, it prints Task succeeded; a failing task script fails the run exactly as a build's would. A task is never a cache hit in either build mode and under any flag: --force is a no-op on it, --force-all still forces its deps; a dirty task records no skip hash. --shell/--post-shell open its shell as for any dir, printing no result line. A task's own deps.bld = ["/"] is what guarantees a fresh dependency output on every run.

Arguments. A task may take per-invocation values from the command line. task = ["out"] is a task whose script sees the out the command line names, or the caller's out env variable when there is no argument — the one deliberate hole in the hermetic environment, which otherwise inherits next to nothing:

# deploy.BUFA
task = ["out", "*rest"]
deps.bld = ["/"]

cmd = '$BUFA_COPY_OR_MOVE ../bufa "$out"'

bufa deploy ~/bin runs the script with out set to ~/bin exactly as typed: no trimming, no expansion, no path resolution (and the script's cwd is its sandbox, so pass an absolute path). The tokens after a task's dir are its arguments, bound to the names in order; the last name may start with *: *rest takes every remaining token, space-joined, and is absent when there is none — the script tests for it like any other variable. task = [] is the same as task = true.

The following are all config errors:

A raw-script BUFA is never a task. Like every other scalar, task replaces per platform as a whole (names never concatenate): [windows] task = false turns a root task off on Windows.

[deps]

There are 3 dependency kinds plus 2 modifiers. Every dep is staged at its project-relative path, so the sandbox mirrors the source tree and scripts reach a dep by the same relative path they would use in the project.

deps.src

A list of build dirs whose filtered source is staged into your sandbox. Each must be a build dir (an empty BUFA will do), is not built (by this reference), is filtered by its own [filters].src, and its content hash is part of your cache key. In dirty mode the dep is hashed but nothing is staged.

deps.bld

A list of build dirs whose build output is staged into your sandbox. They build first, recursively (each dir at most once per run, circular chains rejected before any script runs). Only the dep's output enters your sandbox and only its output hash enters your cache key, so a dep change that leaves its output byte-identical does not rebuild you. Nothing is transitive. In dirty mode the deps build dirty, in place, before you. A dep's published BUFA.env is exported into your script's environment (see 'BUFA.env').

[[deps.ext]]

A list of pinned downloads, fetched by Bufa, verified once, and kept in the Global (per-user) Artifact Cache. The network is never on the rebuild path, and since the pinned hash makes the fetch deterministic, a dir using them stays hermetic: no unsafe = true needed.

One TOML table per artifact:

A hash mismatch fails loudly naming the computed hash. Editing url with an unchanged pin costs one re-verifying download, use "?".
Two entries landing at the same name (after the platform sections are merged in) are an error, and so is a name already taken by a real source file.

[[windows.deps.ext]]/[[unix.deps.ext]]/[[linux.deps.ext]]/[[macos.deps.ext]] entries are added to the shared ones, so a download that differs per OS is one entry per section.

In dirty mode the artifact is placed into the real dir at name; export is a no-op.

Caution

large is the same "For Adults" trade-off as largeOutput, with a wider blast radius: the link points into the global artifact cache shared by every project on the machine. Entries are marked read-only at insert, so a plain accidental write fails loudly, but a script dedicated to misbehave, that corrupts or replaces the file corrupts the entry for every project, and since a cache hit is trusted by name with no re-verification, no later build notices.

bufa check verifies the global cache along with the project's store: it re-hashes every cached artifact against its name and reports any that no longer match. bufa check --fix deletes a corrupted artifact, so the next build downloads and verifies it afresh.

--fix also deletes anything in the cache dir that Bufa does not recognize, except download staging files younger than 5 minutes (another Bufa process may still be fetching).

--no-global skips the global cache; --global-only checks nothing but the global cache, and works outside a project.

bufa nuke --global-only (or --global) deletes the whole cache; the next build re-downloads and re-verifies every artifact.

The Global Artifact Cache and every <project>.BUFA build dir hold a CACHEDIR.TAG file (the Cache Directory Tagging Specification), so backup tools that honor it skip them: restic, borg, and GNU tar with --exclude-caches, kopia by default.

deps.export

A list of deps whose staged tree ships with your build output instead of being pruned at publish. Entries use the dep-path grammar and must name a deps.bld/deps.src entry lying strictly below the build dir, or a [[deps.ext]] artifact by its name. Anything else — a sibling or ancestor dep, the dir itself, a name that is no dep — is a config error. An exported dep publishes as its staged content plus whatever the script wrote into it (the accumulate pattern).

A largeOutput dep cannot be exported. Deps nested inside an exported dep are still pruned unless exported themselves. [filters].bld still applies inside an exported tree.

A no-op in dirty mode, still validated.

deps.cacheDir

deps.cacheDir = true gives the dir a persistent, writable directory of its own that survives unchanges across buids, and exports its name as BUFA_CACHE_DIR. Bufa creates it before the script runs, never wipes it (not on failure, not on --force, not on bufa gc while the dir exists), and never looks inside. Point GOCACHE, CARGO_HOME, or a Maven repo at %BUFA_CACHE_ROOT%\%BUFA_CACHE_DIR%. Legal in a virtual config.

Caution

The cache dir is another "For Adults" feature: its content is history that Bufa never hashes, so a build's output may silently depend on what earlier builds left there, and the same cache key can yield different bytes on a machine whose cache is colder or warmer. Requesting it asserts that the tool's cache is transparent, as Go's or Cargo's is. A --force rebuild that prints the non-reproducibility warning is the first sign that it is not.

bufa nuke --cache-only empties every such dir;

bufa gc removes the entry of a build dir that no longer exists, but does no other cleanup.

Written as deps.cacheDir = true at the top level or cacheDir = true inside [deps].

[env]

Variables exported into the build script's environment, in both modes. Each value is a literal or read from a file in the build dir:

[env]
JAVA_VER  = "21"
ANTLR_VER = { file = "antlr.ver" }
PATH      = '${BUFA_BUILD_ROOT}\tools;${PATH}'

A dir's [env] layers on top of .BUFA's: an exact-key match overrides the root value, a match that differs only in case is an error.

[filters]

Each filter is a list of rules, applied to paths relative to the build dir.

Rules language

[windows.filters]/[unix.filters]/[linux.filters]/[macos.filters] lists are appended after the shared ones, so a platform rule overrides a shared one for the paths it names.

filters.src

A list of rules deciding what counts as the dir's source, applied before hashing and staging. Also what a consumer listing this dir in deps.src receives.
Default: dot-prefixed names excluded at any depth (-.**). BUFA and *.BUFA are always excluded; no rule can add them back.

filters.bld

A list of rules deciding what counts as build output, applied to the sandbox after the script ran (and succeeded): excluded files are deleted, the rest is the output.
Default: nothing excluded, the dir publishes verbatim, hidden files and empty dirs included.

["-**"] publishes nothing: right for a test runner or a "build everything" aggregator, where a successful (and cached) run is the whole point and there is no output worth keeping.

filters.dirty

A list of rules deciding what the dirty-mode content hash sees, the only filter dirty mode uses. Same defaults as src. Outputs written into the dir are hashed by default, which is what makes a deleted or hand-edited output rebuild.

Caution

A dirty build can overwrite or delete your source files: the script runs right in the real source tree, with no sandbox to protect it. That is no different from a plain Makefile or build.sh/build.cmd script.

Platform Sections [windows] / [unix] / [linux] / [macos]

Each section has exactly the shape of the root table and is folded onto it for the running platform. The sections form a tree, folded general to specific:

root
 ├─ [windows]
 └─ [unix]
     ├─ [linux]
     └─ [macos]

Windows folds [windows]; Linux folds [unix] then [linux]; macOS folds [unix] then [macos]; any other Unix folds [unix] alone. All four are top-level tables ([macos], not [unix.macos]). Each fold applies the same rules, so a [linux] setting beats the [unix] one exactly as [unix] beats the root:

A section written only through subtables ([windows.deps], [[windows.deps.ext]]) counts as present; [linux] needs no [unix] beside it. There are no platform sections in .BUFA or BUFA.shell.

.BUFA — project-wide settings

The .BUFA file at the project root is optional and may be empty: its presence alone anchors the project root and beats every other marker. Without one, a .git or the topmost BUFA determine project's root; the full walk, its precedence and what it remembers between runs are explained in the README's Project Root section.

When .BUFA has content, it is TOML with these keys and nothing else (no platform sections):

Key What Type Default Rebuilds every dir
shell project default shell string "" yes
[shells] shell aliases table of alias→path empty no
[env] project-wide env vars table of literals empty yes
[unsafe].ttl unsafe rebuild interval duration "15m" unsafe dirs only
allowBufaDir allow a BUFA directory bool false no

The last column says which edits rebuild the whole project: Bufa's own version is also part of every dir's cache key, so upgrading Bufa is a full rebuild too.

shell

The default shell for every dir whose own shell is not specified. Stricter than a dir's: a non-empty value must be an alias from [shells] or a /-prefixed provider dir path (no relative pahts). Preset names are not accepted here; leave it empty for the native preset. Changing it rebuilds every dir.

[shells]

Aliases for shell provider dirs. Keys are bare names, values are /-prefixed project-root-relative provider dirs; relative values are rejected. An alias may shadow cmd or bash, which lets a project pin a versioned Bash while the provider itself still builds under the real preset. Re-pointing an alias rebuilds exactly the dirs using it.

[shells]
nu   = "/build/nu"
pwsh = "/build/pwsh"

[env]

Variables every build script in the project sees, in both modes. Literals only: a { file = … } entry is a parse error. Same name and value rules as a dir's [env] (one non-empty line, case-insensitively unique keys, no BUFA_ prefix), validated whenever Bufa reads the marker. Exported before the dir's [env], so a dir may override a root key exactly or chain on it with a self-reference. Root values are not expanded into [[deps.ext]] urls. The whole table is part of every dir's cache key, so any edit rebuilds the project.

[unsafe].ttl

The Time-To-Live of an unsafe = true build results: how long it may be trusted, as a Go duration string ("24h", "90m", "0"). The cache key of every unsafe dir carries the build start time rounded down to this precision, so a new invocation with different rounding result rebuilds. 0 means one bucket per invocation (millisecond precision, same-invocation hits kept). Negative values are rejected. Changing it rebuilds unsafe dirs only.

allowBufaDir

A source directory named BUFA (any case) is normally reported as a reserved-name error when a walk meets it. allowBufaDir = true treats such a directory as ordinary content. Needed only to build the Bufa itself, whose cmd/bufa matches the name case-insensitively.

Environment variables

Read by Bufa

Set these in the environment bufa runs in:

A BUFA_BUILD_ROOT seen by Bufa and the BUFA_BUILD_ROOT a script receives (below) share a name but not a job. The name round-trips on purpose: a nested bufa invoked from a build script resolves its own store under the sandbox.

Set for build scripts

Every script, in every mode, gets these:

BUFA_BUILD_ROOT joined with BUFA_BUILD_DIR is the script's cwd, and BUFA_CACHE_ROOT joined with BUFA_CACHE_DIR is its cache dir.
The root/relative split is deliberate: a script that bakes only the relative half into a generated file (%%BUFA_BUILD_ROOT%%\%BUFA_BUILD_DIR%) produces byte-identical output on every machine.

BUFA_COPY_OR_MOVE is move/copy under cmd, mv/cp under bash and most custom shells, and absent when the shell definition declares no verbs: use it to relocate dep files inside sandbox, since deps staged deps in a sandbox are disposable copies but dirty-mode deps are the real source tree. EXCEPT the deps staged by link.

What else the script sees depends on the mode.

A clean, safe build (the default) starts from a scrubbed environment:

A clean unsafe = true build inherits everything (PATH, HOME, PATHEXT, ComSpec included); only the temp variables are still redirected to tmp/. A dirty build inherits everything verbatim and redirects nothing.

Layering order. The script environment is assembled in this order, each later layer overriding an earlier namesake and each ${NAME} reference seeing only what precedes it:

  1. Inherited env variables (filtered as above).
  2. The BUFA_* variables and the platform variables from the tables above.
  3. .BUFA's [env], in document order.
  4. Each direct deps.bld dep's published BUFA.env, in deps.bld order, the implied shell provider last.
  5. The dir's [env] (platform-merged), in document order.

Shell sessions. --shell and --post-shell open the dir's shell with the identical cwd and environment plus one prompt marker: the definition's prompt.env variable (PROMPT for cmd, PS1 for bash) is set to (bufa shell) followed by its previous value or the definition's prompt.default.

BUFA.env — dependency-published variables

A build dir whose published output build dir contains a BUFA.env file exports its variables into the script environment of every dir that lists it in deps.bld — the implied shell provider included, deps.src deps excluded. Direct dependents only, no transitivity, and no way to opt out. The file may be static source in the provider or written by its build script. An absent file is a silent no-op.

# BUFA.env published by /build/go
GOROOT=${BUFA_BUILD_ROOT}/build/go
PATH=${GOROOT}/bin;${PATH}

Format. Dotenv-style, UTF-8 (a BOM is stripped):

Semantics.

Note: Windows PowerShell's >> writes UTF-16LE; generate the file with Out-File -Encoding utf8 or from cmd/bash.

BUFA.shell — shell definitions

Bufa runs a definition, not a shell: the executable, the script file extension, and the argv shapes for a build and for the two interactive sessions. Two definitions are built in (cmd, bash); any other shell is a provider dir that publishes a BUFA.shell in its output build dir, usually beside the unpacked binary.

exe       = "./nu"                                    # required
ext       = ".nu"                                     # required
run       = ["-n", "${script}"]                       # required
shell     = ["-n"]                                    # optional: --shell argv
postShell = ["-n", "-e", "source '${script}'"]        # optional: --post-shell argv
prompt    = { env = "PROMPT_COMMAND", default = "" }  # optional
move      = "mv"                                      # optional pair
copy      = "cp"

${script} is the script's absolute path and the only placeholder; it is substituted in every argv. Unknown keys are errors. No platform sections: a shell that differs per OS is two provider dirs plus a per-dir [windows]/[unix] shell override. Hermeticity flags (-n, -NoProfile, --norc) belong in the definition, not in Bufa.

Presets. Built in, with the flags Bufa always used:

Name ext run shell postShell prompt verbs
cmd .cmd /D /C ${script} /D /K /D /K ${script} PROMPT, $P$G move/copy
bash .bash ${script} --norc --rcfile ${script} -i PS1, \s-\v\$ mv/cp

cmd preset exists on Windows only; bash on every platform (but must be installed on Windows).

cmd is %SystemRoot%\System32\cmd.exe by absolute path; /D skips the AutoRun registry commands. bash is whatever bash Bufa's own PATH finds (Git Bash or WSL on Windows).

Provider dirs. A provider is an ordinary build dir: typically a pinned archive as a non-exported large [[deps.ext]], a script that unpacks it under the native shell named explicitly ([windows] shell = "cmd", [unix] shell = "bash"), BUFA.shell as own source so it publishes beside the binary, and largeOutput = true so consumers link it in. A definition-only provider (cmd = false plus a BUFA.shell with a bare exe) wraps a shell installed on the machine. The definition is read from the provider's output at script time; an output without one, or with a malformed one, fails at the consumer's script step. Both the binary and the definition are part of the provider's output hash, so editing either rebuilds every consumer. The Examples directory holds working providers for Nushell, PowerShell 7, Windows PowerShell, Elvish, and prefix-dev/shell.