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
BUFA— build dir settings.BUFA— project-wide settings- Environment variables
BUFA.env— dependency-published env variablesBUFA.shell— shell definitions
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.
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 | — |
The build script, or false.
- A string is the script body. It is written to a file named
BUFA<ext>(extfrom the shell definition) under the store'stmp/dir and run by the dir's shell with its cwd in the build dir: the sandbox copy in a clean build, the real dir in a dirty build. A non-zero exit fails the build. falsedeclares the dir deliberately scriptless: it only stages its deps and publishes them. This is the one way to make a build dir without a script; combined with an exported[[deps.ext]]it is a complete artifact provider.cmd = trueor any other type is a decode error.- Absent is legal to parse but not to build:
bufaon such a dir fails with report that it has nothing to build. An emptyBUFAis still useful, since it excludes the directory from its parent's source and makes it a validdeps.src.
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.
Which shell runs cmd. One of:
- A bare name: an alias from the
.BUFA[shells]catalogue, else a built-in preset —cmd(Windows only) orbash(every platform, resolved from Bufa's ownPATH). - A provider dir path (anything with a leading
.or containing/or\): a build dir whose output holds aBUFA.shelldefinition,/build/nuroot-relative or./tools/nudir-relative. ""(absent): inherit the.BUFA'sshell, else the platform's native preset.
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 = true makes the build non-hermetic:
- The script inherits Bufa's whole environment, including
PATH,HOME,USERPROFILE, etc. OnlyTEMP/TMP/TMPDIRare still redirected to the store'stmp/dir, so builds cannot accidentally leak state to each other through a shared temp dir. - The result is cached, but under a time bucket: the cache key includes the build start time truncated to
.BUFA's[unsafe].ttl, so once a later invocation starts in a new time bucket the dir rebuilds. Bufa cannot hash the inherited tools and variables, so it rebuilds periodically instead: a toolchain change is noticed within one TTL rather than never. - Symlinks in the dir's source are staged as link objects instead of being rejected, and symlinks the script creates in its output are published as links.
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 = 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 = 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.
- A task is the only dir on its command line:
bufa somedir deployis an error even thoughsomediris a plain build dir, and everything afterdeployis an argument — never a dir to build, never a command name (bufa deploy gcdeploys togc). - A name without an arg takes the caller's variable of that name instead (
out=~/bin bufa deploy;set out=C:\Bin && bufa deployunder cmd) — unset there too, it is an error. An arg always wins over the variable.*restwithout args is the caller'srest, if set. - An unexpected arg is an error (a task with no
*name takes exactly as many args as it has names), as is an empty arg and a multi-line one. All of these fail before anything builds. - An argument that starts with
-is a bufa flag. To hand one to the task, put the whole dir list after--:bufa -o -- deploy ~/bin --fast. Otherwise flags go before the dirs or after the last argument, as usual. - The arguments reach the task's own script and shell session (
--shell/--post-shell) only: the dirs the task depends on keep their hermetic environment, and no name or value ever enters a cache key (a task is never cached). Underunsafe = trueand in dirty mode the script inherits the whole environment anyway; an arg still wins.
The following are all config errors:
- A task must have a script:
task = truewithcmd = falsemakes no sense. - A task publishes nothing, so
largeOutput,deps.export(or an[[deps.ext]]export = true), and[filters].bldare not allowed. - A task cannot be anyone's
deps.blddependency or shell provider — it has no output to depend on. It may be adeps.srcdependency (its source tree only), and may itself listdeps.bldanddeps.src. - An argument name follows the
[env]name rules: non-empty, unique ignoring case, never theBUFA_prefix. It also must not be one Bufa sets itself (PATH,TEMP,TMP,PATHEXT,ComSpec,HOME,TMPDIR) or one the dir's own[env]defines — either would silently override the caller's value. - The
*sigil on any name but the last, or on more than one (task = ["*a", "b"]).
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.
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.
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.
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').
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:
url(required) — the download source, any scheme.${NAME}references expand from this dir's[env], not from.BUFA's.hash(required) — the BufaF…content hash of the file, or"?"to bootstrap: Bufa downloads, prints the computed hash, and fails the build. Paste the printed hash intoBUFAas the newhashvalue and build again: the download is already in the the global cache, so the second build is a pure cache hit with no re-download.name— the landing path relative to the build dir. Default: the url's file name. May point into a subdir (libs/antlr.jar), which Bufa creates. Must be strictly local (no.., not absolute) with a basename other thanBUFA/*.BUFA.large—truestages a symlink to the global cache instead of a writable copy. Treat it read-only and never relocate it. Defaultfalse.export—trueships the artifact in the build output: the bytes for a copy, the link object forlarge. A shortcut for listingnameindeps.export. Defaultfalse: the artifact is an input, pruned at publish.
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.
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 = 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].
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 value must trim to one non-empty line. A file path is relative to the build dir, strictly local, and its basename
must not be
BUFA/*.BUFA. A virtual config may not use{ file = … }(it has no source), literals are fine. - Keys must be non-empty, unique case-insensitively, and must not start with
BUFA_(the only reserved prefix). Any other name,PATHincluded, is legal and overrides the inherited or Bufa-set variable of that name. - Values may reference earlier variables as
${NAME};$${is the one escape and writes a literal${. Expansion happens at script start against the environment assembled so far, in document order, so a value sees the inherited environment, Bufa'sBUFA_*and platform variables,.BUFA's[env], the deps'BUFA.envlayers, and its own table's earlier entries. A self-reference (PATH = '${PATH};tools') chains on the value below. A reference to a name that a later entry of the.BUFA/BUFApair defines or redefines is an error, and so is an undefined name. Names are looked up the way the OS does it (case-insensitively on Windows). ${NAME}also expands inside[[deps.ext]]urls, at config read, from this dir's[env]only; a value that itself contains${may not be referenced from a url.[windows.env]/[unix.env]/[linux.env]/[macos.env]merge per exact key: a platform value replaces the shared one in its position, new keys follow.- Unused variables are still resolved, so a missing file fails the build even if nothing reads the value.
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.
Each filter is a list of rules, applied to paths relative to the build dir.
- Each rule is
+pattern(include) or-pattern(exclude), matched case-insensitively. *matches within one path segment,**across segments;?,[abc], and{a,b}work as usual.-src/*.cexcludessrc/main.cbut notsrc/sub/deep.c;-src/**.cexcludes both.-test?.logexcludestest1.logbut nottest10.log.-*.[ch]excludes.cand.hfiles;-*.{png,jpg}excludes both image types with one rule.
- A trailing
/names a directory and everything below it (dir/is rewritten asdir/**).-build/excludesbuild/out.exeandbuild/obj/x.o;-buildexcludes only a file namedbuild.
- A pattern without a leading
/matches at any depth (it is rewritten as**/pattern); a/-prefixed one is anchored at the build dir — this dir, never the project root, unlike a/-prefixed dep path.-tmp/(becomes-**/tmp/**) excludestmp/andsrc/tmp/alike;-/tmp/(becomes-/tmp/**) excludes only the top-leveltmp/.-*.log(becomes-**/*.log) excludesa.logandlogs/b.log;-/*.logexcludes onlya.log.
- The last matching rule wins.
["-build/", "+build/keep.txt"]dropsbuild/exceptbuild/keep.txt. Append"-*.txt"andkeep.txtis dropped again: position matters, specificity does not.
- The first rule decides what happens to an unmatched path: a list opening with
-rulsis a blacklist (unmatched ⇒ included), list opening with+ruleis a whitelist (unmatched ⇒ excluded).["-*.tmp"]takes everything except.tmpfiles;["+*.c", "+*.h"]takes.cand.hfiles and nothing else.["+src/", "-src/gen/"]takes onlysrc/, minussrc/gen/.
- Rules act on files and symlinks, never on directories: a directory is included when something in it is.
- A nested build dir is never part of this dir's source, whatever the rules say.
[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.
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.
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.
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.
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:
- Scalars (
cmd,shell,unsafe,largeOutput,task,deps.cacheDir) replace the value so far when present in the section, whatever their value: an explicit[windows] unsafe = falsebeats a rootunsafe = true. - Lists (
deps.src,deps.bld,deps.export,[[deps.ext]],filters.*) are appended: root's, then[unix]'s, then the leaf's. [env]merges per exact key, the later section's value winning in the earlier key's position.
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.
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.
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.
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"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.
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.
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.
Set these in the environment bufa runs in:
BUFA_BUILD_ROOT— puts the build store at$BUFA_BUILD_ROOT/<project path>.BUFAinstead of beside the project. Allows hosting build store farther away from source, or on faster storage.<project path>is the project root's full path flattened into one name, so projects sharing a last directory name never share a store::\becomes one_, and every other:,\, and/becomes_—C:\Src\Proj⇒C_Src_Proj.BUFA,/home/me/proj⇒_home_me_proj.BUFA. Required (must be set) when the project root is a filesystem root, where no sibling can exist (the store is then$BUFA_BUILD_ROOT/C_.BUFA, or_.BUFAfor/).BUFA_NO_DAEMON— any non-empty value: never start or use the watcher daemon, like--no-daemonon every command.bufa nukestill stops a running daemon before deleting the store.BUFA_GLOBAL_CACHE_DIR— the parent dir of the global[[deps.ext]]artifact cache, which is always itsbufasubdir (a dir Bufa owns outright, sobufa check --fixmay delete anything unrecognized in it).
Default: the user cache dir — giving%LOCALAPPDATA%\bufaon Windows,$XDG_CACHE_HOME/bufaor~/.cache/bufaon Linux,~/Library/Caches/bufaon macOS.LOG_LEVEL— the log level when--log-levelis not given:none(default, silent),error,warn,info,debug. Also read by the daemon process.LOG_SOURCE—trueor1: append thefile:lineof each log record.
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.
Every script, in every mode, gets these:
BUFA_BUILD_ROOT— absolute. In a clean build the sandbox root,<store>/bld; in a dirty build the project root.BUFA_BUILD_DIR— the build dir's project-relative path with OS separators,.for the root dir. Same in both modes.BUFA_CACHE_ROOT—<store>/user, absolute. Same in both modes, set whether or not the dir uses it.BUFA_CACHE_DIR— only for a dir withdeps.cacheDir = true: the name of its persistent cache dir underBUFA_CACHE_ROOT, unique per requesting build dir. Same value in both modes.BUFA_COPY_OR_MOVE— the shell definition'smoveverb in a clean build, itscopyverb in a dirty one.
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:
- Inherited: on Windows only
OS,SystemDrive,SystemRoot, andwindir; on Unix nothing at all — plus, for a task, its arguments: the command-line tokens, or the caller's variables of those names. PATH: on Windows%SystemRoot%\System32,%SystemRoot%,%SystemRoot%\System32\Wbem, and%SystemRoot%\System32\WindowsPowerShell\v1.0; on Unix/usr/bin:/bin:/usr/sbin:/sbin.TEMPandTMP(Windows) orTMPDIR(Unix): the store'stmp/dir, recreated empty before every script, removed after a successful one and kept after a failure together with the sandbox.HOME(Unix): the sametmp/dir, a synthetic home wiped with it. Windows has noHOME.PATHEXT(Windows):.COM;.EXE;.BAT;.CMD, executables and batch files only.ComSpec(Windows):%SystemRoot%\System32\cmd.exe, regardless of system default.
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:
- Inherited env variables (filtered as above).
- The
BUFA_*variables and the platform variables from the tables above. .BUFA's[env], in document order.- Each direct
deps.blddep's publishedBUFA.env, indeps.bldorder, the implied shell provider last. - 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.
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):
KEY=VALUEper line, split at the first=; the value is everything after it, verbatim. No quoting, no escaping, no inline comments: a#inside a value is part of the value.- Lines are trimmed, so CRLF and trailing spaces from
echo KEY=VAL >> BUFA.envare harmless. - Blank lines and lines starting with
#(comments) are skipped. - Hard errors, reported with line numbers: a line without
=, an empty value, a duplicate key (case-insensitive), a key that is empty or starts withBUFA_.
Semantics.
- Variables are exported in file order, between
.BUFA's[env]and the consumer's[env], deps indeps.bldorder. Last wins, so a dep may override a root value and the consumer's[env]may override a dep's. - Each file is a closed unit:
${NAME}references resolve against everything below it (inherited, Bufa, root[env], earlier deps) plus the file's own earlier lines, and a self-reference chains (PATH=…;${PATH}). A reference to a name the same file defines later is an error; the consumer's redefinitions never affect a dep's references. - A later dep may redefine an earlier dep's variable only with the identical value or by chaining on it (its
value must contain
${NAME}). A plain overwrite is a hard error, since it would silently hide the earlier provider's tools. - Values should address the provider's own published tree via
${BUFA_BUILD_ROOT}/<provider's project path>, never${BUFA_BUILD_DIR}, which expands to the consumer's dir. - The file is part of the provider's output, so editing it rebuilds every consumer. No other cache-key effect.
- A virtual
cmd = falseprovider cannot publish one: it has no source and no script.
Note: Windows PowerShell's
>>writes UTF-16LE; generate the file withOut-File -Encoding utf8or fromcmd/bash.
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"exe(required) — the interpreter../xorbin/xis relative to the provider's output dir. A barexis looked up on Bufa'sPATHand handed to the child as an absolute path (an ambient, unpinned shell). An absolute path is used as is. Windows needs no.exe.ext(required) — the script file's extension,.-prefixed, no separators. The script is written asBUFA<ext>; PowerShell requires.ps1.run(required) — the argv afterexefor a build. Must contain${script}.shell— the argv for--shell. Absent ⇒--shellon a dir using this shell fails at invocation.[]is a present, empty argv.postShell— the argv for--post-shell: the shell process itself runs the script and stays open. Must contain${script}. Absent ⇒--post-shellfails at invocation.prompt—{ env = "…", default = "…" }: the session prompt variable and its fallback value.defaultneedsenv. Absent ⇒ no prompt marker.move,copy— theBUFA_COPY_OR_MOVEspellings for clean and dirty mode. Both or neither; neither ⇒ the variable is not exported.
${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.