Skip to content

Language syntax

A fabr build file (PROJECT.fabr, and any files it includes) is written in a small declarative language made of these statements:

Statement Example Section
Load a plugin or another file plugin @fabr-build/js; Plugins and includes
Set a property JS_TARGET = es2022-esm; Properties
Declare a target js_package mylib { srcs = src:**/*.ts; } Target declarations
Declare a fallback default TYPESCRIPT = @npm:typescript:5.6.3; Default properties
Declare a name for one file only private VERSION = 2.4.0; Private declarations
Apply a guard to several declarations <TARGET=*-apple-*> { … } Guarded properties
Define a target type targetdef mytype { srcs = FILES; } Targetdef declarations
Define a type from another targetdef mytype extends js_package { docs = FILES; } Extending a type
Say how a type is built rule mytype t<BUILD_OPERATION=build> = …; Rule and repository declarations

Most of what you write inside them is references: the patterns that name files, targets and packages.

Two rules apply throughout. Every name, whether a property or a target, is declared once in the project; a default that another declaration replaces, guarded declarations of one property, and private names, which belong to one file, are the exceptions. And the order of declarations doesn’t matter: a name can be used before the line that declares it, and nothing is worked out until a build needs it.

A # starts a comment that runs to the end of the line. Spaces, tabs and newlines only separate words.

# This is a comment.
JS_TARGET = es2021-commonjs; # so is this

A comment directly above a property, target or target type is its documentation: fabr list-targetdefs and fabr list-properties show it.

plugin <name>; loads a plugin. The name is a reference to the package that provides it, read as any other: fabr’s own plugins by their package names, another plugin installed alongside fabr through @fabr-home, or a fabr_plugin target of the project.

plugin @fabr-build/js;
plugin @fabr-home:@example/text-lint;

Loading a plugin makes its target types and settings available; nothing else needs including. Fabr’s own standard definitions are always available.

include <path>; reads another build file, by a path relative to the file containing the include:

include ./targets/frontend.fabr;
include ./packages/*/BUILD.fabr;

The path can be a pattern, which includes every file that matches. It can’t contain a ${…} substitution, and it must name at least one file: a path that doesn’t exist and a pattern that matches nothing are both errors.

include <package>:<path>; reads one of the optional .fabr files a plugin provides, which a project reads only where it includes them. <package> is the plugin’s package name, whatever name your plugin declaration uses for it, and <path> is the file’s path within the package:

plugin @fabr-build/java;
include @fabr-build/java:lib/spring-boot.fabr;

The project must use the plugin. The path can be a pattern, as above, and must name at least one of the plugin’s optional files; the error lists the ones it has.

Name Characters allowed
Property, target type letters, digits, _, @, -
Target the same, plus / and .

Every name starts with a letter, _ or @, so my-tool and @fabr-build are names but -x and 2fa are not. Registries and catalogs are conventionally named with a leading @ (@npm, @pkg), which isn’t required.

A reference names files. It can be a path or a pattern in your source tree, a target, a package in a registry, or a selection of files from any of those:

srcs = src/index.ts; # a file
srcs = src/**/*.ts; # a pattern
deps = mylib; # a target
deps = @npm:lodash:4.17.21; # a package in a registry
srcs = mylib:build/*.js; # files selected from a target

A path is relative to the build file it is written in. It can use ../ to reach files elsewhere in the project, but not outside it. A name that is both a target and a directory means the target; write ./name for the directory.

A registry or catalog named on its own, as in deps = @npm;, names no package or files, and is an error ('@npm' names a repository, not a package or files). Name what you want from it: @npm:lodash:4.17.21. The ls command is the exception, listing what some repositories contain.

Shell glob patterns work, along with ** to match across directories:

srcs = src/*.tsx; # .tsx files directly in src
srcs = src/**/*.ts; # .ts files anywhere under src
deps = lib/*.[jt]s; # .js and .ts files, using a character class
srcs = src; # a directory: everything under it

Bash’s extended globs are supported too. Each is a prefix character followed by a parenthesised list of one or more patterns separated by |. Each matches within one path component, except !(…), described below:

Pattern Matches
?(…) zero or one of the listed patterns
*(…) zero or more
+(…) one or more
@(…) exactly one
!(…) anything except the listed patterns
srcs = src/!(generated)/**; # everything under src except the generated directory
srcs = src/!(dist|build|gen)/**; # ...except three directories
srcs = src/@(api|web)/*.ts; # only the api and web directories
srcs = src/!(*.test).ts; # .ts files other than .test.ts

The patterns in a list can themselves contain wildcards, character classes, further groups and ${…} substitutions. An unclosed group, such as !( with no ), is an error.

A !(…) group removes what it lists from the whole pattern. A path matches A!(X)B when it matches A*B and does not match AXB. The listed patterns can span directories, so a group can exclude a path at any depth:

srcs = **/!(@shorthand/api)/**; # everything not under an @shorthand/api directory
srcs = **!(/@shorthand/api/)**/*.ts; # the same, for .ts files
srcs = **/!(gen/out)/**/!(*.test.ts); # neither under gen/out, nor a .test.ts file
  • A group with no / in its patterns stands for one path component, as * does. With a /, it stands for any part of a path. A group that is a whole component by itself always matches something, as * does there.
  • Each group in a pattern is applied separately, and the path must satisfy all of them. In the last example, x.js at the top level doesn’t match, because the / after the first group requires a directory.
  • This differs from bash, where the group may match some other text at that position. In bash, !(a)* matches ab, because the group can match the empty string. In fabr it doesn’t, because ab matches a*.
  • A !(…) group can’t be inside another group. @(!(a)|b) is an error.

An unquoted word runs until whitespace or one of , = { } ; < > #. In it, * ? [ ] and the extended-glob forms above are wildcards, $ and a backtick start a substitution, and every other character stands for itself, so @npm:react:^19.1.0 needs no quotes. Outside an extended glob, (, ), | and ! are ordinary characters, so a leading ! on its own doesn’t negate anything.

Quote a value, or part of one, that contains whitespace or any of those characters:

  • Single quotes '…' take the text as it is: no substitution, no escapes, no wildcards.
  • Double quotes "…" allow ${…} and backtick substitution and backslash escapes, and take wildcards literally: "*.ts" names a file called *.ts.

A backslash, in double quotes or in an unquoted word, takes the next character literally (a\ b is one word, \" a double quote), except for the C escapes \n, \t, \r, \a, \b, \e, \f, \v, \xHH and \0NNN (octal). Quoted and unquoted parts written together make one word: 'it''s' is its, and "v${VERSION}"-rc is one word.

A quoted value may span lines. Each line break, and the indentation of the line after it, is part of the text, so a shell script written in one reads as the script would in a file:

build = "apk add --no-cache curl git &&
git clone --depth 1 https://example.com/tool.git /usr/src/tool &&
cd /usr/src/tool && make install";

$NAME or ${NAME} is replaced by the value of a property, in unquoted and double-quoted values:

VERSION = 2.4.0;
url = "https://example.com/releases/v${VERSION}/";
deps = @npm:my-package:${VERSION};

Without braces, the name is the letters, digits and _ that follow the $, so a property whose name contains - or @ needs braces: $my-prop substitutes my followed by the text -prop, while ${my-prop} substitutes my-prop.

The value is inserted as text, and every character of it is taken literally: a * in it matches only a *, and a constraint or rename in it is part of the name rather than acting on it. That includes a constraint written on the property’s own value. To use a property’s references where references are expected, write its bare name instead, as described under String properties:

G = ./*.ts;
srcs = G; # every .ts file
srcs = ${G}; # only a file named "*.ts"
CORE = core<BUILD_TYPE=release>;
deps = CORE; # core, built for release
deps = ${CORE}; # error: Unable to resolve 'core<BUILD_TYPE=release>'

To substitute the value a property has under different settings, write a constraint inside the braces, ${NAME<KEY=value, …>}. It changes those settings for reading NAME only, as a constraint on a reference does for building it:

OPT<BUILD_TYPE=debug> = -O0;
OPT<BUILD_TYPE=release> = -O2;
RELEASE_OPT = ${OPT<BUILD_TYPE=release>}; # -O2, whatever BUILD_TYPE this build uses

The < follows the name directly. Each value is an exact setting rather than a pattern, and may itself contain a substitution (${OPT<BUILD_TYPE=${MODE}>}).

To change the value as it is substituted, write an edit after the name, as in bash:

Form Result
${NAME#pattern} the value with the shortest prefix matching pattern removed
${NAME##pattern} the value with the longest matching prefix removed
${NAME%pattern} the value with the shortest matching suffix removed
${NAME%%pattern} the value with the longest matching suffix removed
${NAME/pattern/text} the first match replaced with text
${NAME//pattern/text} every match replaced with text
${NAME/#pattern/text} a match at the start replaced with text
${NAME/%pattern/text} a match at the end replaced with text
TRIPLE = aarch64-apple-darwin;
CPU = ${TRIPLE%%-*}; # aarch64
LINUX = ${TRIPLE/-*/-unknown-linux}; # aarch64-unknown-linux
ARCHIVES = a.tar.gz b.tar.gz;
LIST = ${ARCHIVES// /,}; # a.tar.gz,b.tar.gz

A replacement match is the longest one starting at the leftmost place any match starts. Leaving out /text removes the match. A value of several words is edited as the one text it is substituted as, its words separated by spaces.

In a pattern, * matches any characters, / included, ? matches one character, and […] matches one of a set, as in bash. Unlike in a file pattern, * is not stopped by a /, since an edit changes text rather than paths. A backslash takes the next character literally: write \/ for a / in the pattern, and \} for a }. A pattern or replacement can’t contain a ${…} or a backtick substitution, and & in a replacement is a plain & rather than the matched text.

An edit can follow a constraint, and applies to the value read under it: ${OPT<BUILD_TYPE=release>#-} is O2.

Backticks run a command and are replaced by what it prints, for information that comes from a tool, such as its version:

TSC_VERSION = `@npm:typescript:5.6.3:tsc --version`;

Like ${NAME}, a backtick expression is one part of a value. It can be joined to the text around it and used anywhere a substitution can be, including inside double quotes and inside a reference. Inside single quotes it is ordinary text.

srcs = generated/`version_tool --short`/*.js;
  • The command must be something fabr can run: a runnable target, a property naming one, a package such as @npm:typescript:5.6.3:tsc, or a program installed on your machine, named through @host (`@host:uname -s`). A bare uname is looked up as a name in your project, not on your PATH.
  • The output is trimmed, and runs of whitespace inside it, newlines included, become single spaces, as a shell does. For output of several lines, use a generate target and read the file it produces.
  • The command is run once for a given tool and arguments, however many places use it, and the result is cached.

The command is a pipeline as in a command property, so | and redirections work. Because the value is the command’s stdout, a redirection to a file name discards that stream (2> log silences stderr), and 2>&1 includes stderr in the value.

<NAME=value> after a reference asks for that reference to be built with a setting changed:

deps = core<TARGET=arm64-apple-macosx15.0>; # core, built for another platform
srcs = mylib<JS_TARGET=es2022-esm>:*.js; # mylib's .js files, built as ES modules
deps = other<BUILD_TYPE=release, JS_TARGET=es2022-esm>;

The < follows the name directly, before any : or / selection. The setting applies to the target and to everything it depends on, unless one of those references sets it again. The rest of the build is unaffected, so the same target can be built several ways in one build.

The same notation on the left of a property declaration means something different; see Guarded properties.

A reference decides what its files are named as well as which files they are. Written with /, the names keep the whole path. A : in place of a / drops everything before it:

srcs = src/lib/index.ts; # named src/lib/index.ts
srcs = src:lib/index.ts; # named lib/index.ts

Selecting files from a target or a package works the same way: mylib:build/*.js, @npm:esbuild:0.28.1:package.json. What you get is those files only, not the package they came from.

-> renames. Each * and ** in the template on the right takes what the wildcard in the same position on the left matched:

srcs = src/index.ts -> main.ts;
srcs = src/**/*.ts -> lib/**/*.mts;
  • The -> has spaces around it and comes last in the reference.
  • Both sides use only * and ** as wildcards, not ?, […] or an extended glob other than !(…), and the same number of them.
  • A template with no wildcard names one file, so the left side can be any pattern that matches exactly one: srcs = vendor/*/dist/index.js -> vendor.js;. Matching two files is an error, since both would get the same name.
  • A !(…) group on the left is not one of its wildcards: it isn’t counted, and what it matched is left out of the new name. src/!(generated)/*.ts -> lib/*.js names src/api/client.ts lib/client.js. A selection whose only wildcard is inside a negation, such as src/!(*.test).ts, can therefore only be renamed to a single name.

A template can instead refer to the wildcards by number, $1 for the first and so on. The counts then needn’t match, so a numbered template can repeat a wildcard, reorder them or leave one out:

srcs = v*.tgz -> v$1/node-v$1.tgz; # the first wildcard, twice
srcs = */*.a -> $2-$1.b; # reordered
srcs = */*.a -> only-$2.b; # the first one unused

The two styles can’t be mixed in one template. Outside a rename template, $1 is an ordinary variable substitution.

Renaming a package. When -> follows a whole package, with no files selected from it, it renames the package: the name it is installed and imported under. Its contents, version and dependencies don’t change.

deps = @npm:stream-browserify:3.0.0 -> stream; # code importing "stream" gets this package
deps = mylib -> renamedlib;

Choosing a program. Where a reference is run, by fabr run or as a property that takes a program such as a serve target’s tool, selecting from a package looks among the package’s command-line programs as well as its files. @npm:typescript:5.6.3:tsc is the tsc program. A package with one program needs no selection. A package with several, as typescript has tsc and tsserver, must have one named.

An archive can be read as if it were a directory, by continuing a reference into it. A reference that stops at the archive is the archive file itself.

srcs = ./vendor.tgz:*:**; # everything in the archive, below its top-level directory
data = ./vendor.tgz; # the archive, as one file

Tar and zip archives are recognised, and tar compressed with gzip or xz, so .tgz, .tar.gz, .tar.xz and .zip all work. Recognition is by a file’s contents, not its name. An archive inside an archive can be read the same way (outer.tgz:inner.tgz:**).

** doesn’t look inside archives. ./**/*.ts finds .ts files in your tree and treats any archive it passes as a file. To look inside, the reference has to match the archive itself with one of its components:

srcs = ./**/*.ts; # .ts files in the tree
srcs = ./**/*.tgz/**/*.ts; # the .ts files inside every .tgz in the tree

A package version in a reference can end in ? or !, to resolve a conflict between the versions that different parts of a build require. Each takes an exact version, except that a ? may name none, and both can be written wherever dependencies are listed: in a target’s deps or in a catalog.

deps = @npm:aws-param-store-sdkv3:4.0.0
@npm:tslib:2.6.2? @npm:tslib:1.14.1? # both versions of tslib are allowed
@npm:debug? # any versions of debug are allowed
@npm:some-package:2.0.0!; # every requirement on it becomes 2.0.0

? allows a version. A build normally uses one version of each package, and fails when the requirements can’t agree on one. Listing each version involved with a ? lets them all be used: each package gets the version its own requirement accepts, as it would under npm.

  • The entries name the complete set of versions allowed. If a later change makes the build need a different version, it fails again and says which entry to update.
  • An entry is not a dependency. It doesn’t add the package to the target.
  • In a catalog, an ordinary exact entry counts as allowing that version: deps = @npm:tslib:2.6.2 @npm:tslib:1.14.1?;.
  • Without a version, @npm:debug? allows every version of the package: as many as the build requires, now or after later changes.
  • A ? with a version also supplies it for a package that is only ever required as *, which otherwise has no version to select.

! forces a version. Every requirement on the package, from any dependency, is replaced by exactly this version, as npm’s overrides does. Requirements it doesn’t satisfy are overridden without a warning, so use it when a dependency’s requirement is wrong, and ? otherwise.

On a target. Both markers can also end the name of a target that builds a package, with the same meaning: mylib! uses the package mylib builds wherever mylib is required, in place of a registry package of that name, and mylib? allows it alongside one. See Packages built in your project. A marker is the last character of a reference, written unquoted, before or after a <k=v> constraint (mylib!<BUILD_TYPE=release> or mylib<BUILD_TYPE=release>!). On a target, it is read as a marker only where the rest is the exact name of a declared target; anywhere else a ? is a wildcard and a ! part of the name. Quoted, either character is part of the name ("mylib!" names a file). A marker on a reference that selects files of a package (@npm:pkg:1.2.3!:lib/x.js) is an error: files are no package.

Dependencies shows the error that prompts these and how to choose between them.

A property gives a name a value, and ends with ;:

name = value;

At the top level of a build file a property is global: a configuration setting, or a value to reuse. Inside a target it is one of that target’s inputs. The value is a string, a map or a command.

A global property can be set from outside the build file: for one run with -DNAME=value on the command line, or for one reference with a constraint. A private property can’t.

default declares a value to use only when nothing else declares the property:

default TYPESCRIPT = @npm:typescript:5.6.3;

Fabr and its plugins declare their settings this way, which is why a build file can set TYPESCRIPT itself without that being a second declaration of the name.

Properties that stand in for a group of names

Section titled “Properties that stand in for a group of names”

A property can have the name of a group of targets, the part before a / in names such as @fabr-build/js. It then answers every name in that group that nothing declares: the rest of the name is looked up in the property’s value. Fabr’s standard definitions use this to make the packages installed with fabr available by their own names:

default @fabr-build = @fabr-home:@fabr-build;

@fabr-home is the set of packages installed alongside fabr, and @fabr-home:@fabr-build the ones in its @fabr-build scope (fabr ls @fabr-home:@fabr-build lists them; see ls). So @fabr-build/sass-pnp-importer means the installed package, unless your project declares a target of that name, which takes precedence. Declaring a property or a target named @fabr-build yourself replaces the default, like any other default.

A string property is one or more words separated by whitespace. Each word is a reference or a quoted string:

version = 1.4.0;
description = "A small example";
deps = @npm:chai:4.3.6 @npm:@types/chai:4.3.1 @npm:picomatch:2.3.1;

A property that lists references can be used by name where references are expected, which is how a list of dependencies is shared:

TEST_LIBS = @npm:chai:4.3.6 @npm:@types/chai:4.3.1;
js_package mylib {
srcs = src:**/*.ts;
test_deps = TEST_LIBS;
}

A map is a block of key = value; entries. A value is a string, another map, or several maps one after another, which make a list:

metadata = {
description = My package;
license = GPL-3.0-or-later;
repository = { type = git; url = https://example.com/r.git; };
maintainers = { name = ann; } { name = bob; };
}

The ; after a closing } is optional.

Naming another map inside a block includes its entries. Entries are applied in the order written and a later one replaces an earlier one with the same key, so a shared map can be extended or overridden:

COMMON = { license = GPL-3.0-or-later; author = { name = Ann; }; };
metadata = { COMMON; description = This particular package; };

A command is a pipeline, written much as in a shell. The run of a generate or a patched target is one, and so is the inside of a command substitution.

run = my_script -l < src/input.txt | paginate > output.txt;

Each command is something fabr can run: a runnable target, a package with a command-line program, or a program installed on your machine, named through @host. The arguments are words, and a pattern among them is expanded against the target’s input files. A command can’t be written in a map.

Form Meaning
a | b Send a’s stdout to b’s stdin.
< name Read the first command’s stdin from a reference that names one file.
> name Keep stdout as a file called name. Only on the last command, since an earlier one’s stdout goes to the pipe.
2> name Keep stderr as a file called name.
2>&1 Send stderr wherever stdout is going at that point. 1>&2 is the reverse.
&> name Keep both in one file: the same as > name 2>&1.

Redirections apply in the order written, as in a shell: > f 2>&1 sends both streams to f, while 2>&1 > f sends stderr to where stdout was going before, and then stdout to f. Unlike a shell, redirecting the same stream twice is an error.

A stream that isn’t redirected is shown as the step’s output, and included in the error if the command fails.

In a generate target, the files that redirections keep are the target’s result. If the target sets output, that chooses the result instead, from those files and from any files the commands wrote, and a kept file it doesn’t match is left out. A kept file and a written file with the same name is an error. Files that incremental selects are kept for the command’s next run rather than added to the result; see generate.

@host:<command> is the program a shell would run for <command>, found on fabr’s own PATH, and @host:<absolute path> the program at that path:

run = @host:sed -e s/DEBUG/RELEASE/ < config.in > config.out;

Fabr identifies the program by its contents, so a different build of sed rebuilds what used it. It doesn’t track what the program reads or runs in turn, such as a compiler’s headers or its linker, and the program starts with no environment variables except those its target’s env sets. Prefer a tool fabr fetches, such as an npm package, where there is one; use @host for what only your machine provides. The host_searchpath entry has the details.

A property declaration can have a guard: <NAME=pattern> after the property’s name. The declaration then applies only in builds where the setting matches.

js_package watcher {
srcs = src:*.ts;
srcs<TARGET=*-linux-*> = src:linux/**/*.ts;
srcs<TARGET=*-apple-*> = src:macos/**/*.ts;
deps<TARGET=*-apple-*> = @npm:fsevents:2.3.3;
}

A build for Linux gets the common sources and the Linux ones; a build for macOS gets the common sources, the macOS ones and fsevents.

  • A guard’s value is a pattern, matched against the setting’s value. Several settings in one guard must all match: <TARGET=*-linux-*, BUILD_TYPE=release>.
  • Every declaration that matches applies. For a property that lists files, the matching declarations are combined. There is no “most specific guard wins”.
  • A property with a single value can have only one match. Two guarded declarations of version that both match are an error naming both, so write guards for such a property that can’t overlap.
  • When no declaration matches, the property is unset, and the target type’s default applies if it has one.

This is the notation of a constraint in a different place, with a different meaning. After a reference it requires a setting, and takes an exact value. After a property name being declared it tests a setting, and takes a pattern.

A guard can be written once around several declarations:

js_package watcher {
srcs = src:*.ts;
<TARGET=*-apple-*> {
srcs = src:macos/**/*.ts;
deps = @npm:fsevents:2.3.3;
}
}

Global properties can be guarded in the same way. For those, a default declaration is what applies when no guarded declaration matches; a property declared only under guards, none of which match, is an error.

A default declaration can be guarded too, which gives a setting a default that depends on another. The JavaScript support declares JS_BUNDLE_FORMAT this way:

default JS_BUNDLE_FORMAT<JS_TARGET=*-esm*> = esm;
default JS_BUNDLE_FORMAT<JS_TARGET=*-commonjs-browser> = iife;

Write a single-valued setting’s guarded defaults so that exactly one matches each build. Any ordinary declaration of the setting replaces all of them.

A target is declared as <type> <name> { <properties> }:

js_package mylib {
srcs = src:**/*.ts;
deps = @npm:lodash:4.17.21;
tests = src:**/*.test.ts;
}

The type is one that fabr’s core or a loaded plugin defines, or one your own targetdef declares; the reference pages for core and JavaScript list the standard ones with their properties. The name is how the target is referred to, in build files and on the command line.

A target can be declared default, like a property:

default js_script lint_tool { entry = tools/lint.js; }

A default target is used only if no other declaration has its name. A plugin declares the tools it supplies this way, so that a project can replace one by declaring a target with the same name. Two default declarations of one name are still an error.

A target can also be declared private, which only its own build file can see.

Naming a map property alone in a target’s body, as a statement, takes the map’s entries as the target’s properties:

JS_DEFAULTS = { test_deps = chai jest_types; test_framework = jest; };
js_package core { JS_DEFAULTS; srcs = src:**/*.ts; }
js_package cli { JS_DEFAULTS; srcs = src:**/*.ts; test_framework = node; }
  • Later wins. For each property, the last item in the body that supplies it is what the target has, as in a map: cli’s test_framework is node, and in { test_framework = node; JS_DEFAULTS; } it would be jest. A guarded declaration after a splice is chosen as usual, and the splice supplies the property where none of them applies.
  • Entries are read as the target’s properties. Each entry is read with the kind its property has in the target’s type, so test_deps = chai jest_types; names packages. An entry for a property the type doesn’t have is left out, so one map can be shared by targets of different types. An entry whose value doesn’t fit its property (a { … } block for a FILES property) is an error when the target is built, as is a REQUIRED property that the body as spliced doesn’t set.
  • Only a map can be spliced. A target’s name isn’t a map: it stands for what the target builds. A splice can’t be guarded, either with a <…> after it or inside a guard block. A map can’t hold a command, so a COMMAND property such as generate’s run is written in the target itself.

Inside a rule’s block, the rule’s parameter is the target being built as a map of its properties, so pkg; splices all of them (see Splicing the target’s properties).

private before a target or a global property declares a name that only the build file it is written in can see. Use it for a target that exists only to be used by others in the same file, so that it doesn’t add a name to the rest of the project:

private patched glob-promise {
srcs = @npm:glob-promise:1.33.0;
patches = patches/glob.patch;
}
js_package editor {
srcs = src:**/*.ts;
deps = glob-promise;
}
  • Every reference written in the file sees its private names, including those in ${…} substitutions and in guards. There, a private name is used in place of a declaration of the same name in any other build file.
  • Other build files, the command line and the list- commands don’t see a private name. Naming one on the command line is an error that says which file it is private to.
  • -DNAME=value and a constraint set only the public property of that name. A private property keeps the value its file gives it.
  • A reference means what it means in the file it is written in, as a relative path does. So a property SHARED = glob-promise; written in the same file names the private target wherever another file refers to SHARED. A ${SHARED} substitution inserts its text, which names whatever glob-promise is in the file the substitution is written in.
  • A file can’t declare one name both privately and publicly. Only target and property declarations can be private, so private default, private include and the like are errors, and an included file doesn’t see the private names of the file that includes it.

targetdef defines a target type: the properties its targets take. Fabr’s core and its plugins define the standard ones.

targetdef script {
entry = REQUIRED FILES;
deps = FILES;
args = STRING;
}

Building a target needs a rule for its type (see Rule and repository declarations): one a plugin provides, or one written in a .fabr file out of other targets, as Writing your own rules shows. Writing plugins is described in the plugin reference.

Each entry is <property> = <kind>;. A * in place of a property name gives a kind to every property not otherwise listed, for types whose targets take arbitrary keys:

targetdef sync {
* = FILES;
}
Kind The value is
STRING Text.
FILES References, which become a set of files.
PACKAGES References to packages: registry packages and the targets that build one.
MAP A block of key = value; entries; see Map properties.
COMMAND A command pipeline; see Command property.
REWRITE Renaming rules written pattern -> template; see Projection and renaming.
REQUIRED Not a kind but a modifier, written before one: the property must be set.

MAP, COMMAND and REWRITE are checked: a value of the wrong form is an error, as is a missing REQUIRED property. STRING, FILES and PACKAGES are not told apart when a build file is read. They document what a property is for, and the rule that reads the property decides how to treat its value.

FILES and PACKAGES take the same references and differ in what a reference means:

A reference to In a FILES property In a PACKAGES property
a target The files it builds. The package it builds, under the target’s name.
a registry package (@npm:lodash:4.17.21) That package’s own files, at the version written (the lowest version a range allows). Nothing it depends on is fetched. A dependency: its version is chosen together with every other package the target depends on, and its own dependencies come with it.
a file or directory Those files. Those files.

A rule may read a PACKAGES property for its files, but reads a FILES property only for its files, except where a type says otherwise: a js_script’s entry may name a package, and a sync or repository_group makes the files it is given into a package. The JavaScript targets page gives each property’s kind.

A kind can be followed by default and a value, which a target of the type gets when it doesn’t set the property:

targetdef script {
entry = REQUIRED FILES;
args = STRING default --quiet;
outputs = FILES default *.js;
}
  • A default can be anything the property itself could be set to, including references, ${…} substitutions and a { … } block for a MAP.
  • A relative path in a default is relative to the file that defines the type, not to the file declaring the target, so a plugin’s defaults can refer to the plugin’s own files.
  • A ${…} in a default is replaced with the value the setting has for the target being built, so a default can follow ${BUILD_TYPE}.
  • REQUIRED and default can’t be combined, and a * entry can’t have a default.

A targetdef can extend another: extends and the other type’s name come after its own name. The new type has every property of the type it extends, plus those it declares:

targetdef documented_package extends js_package {
docs = FILES;
}
  • Properties. A type can declare again a property of the type it extends, to give it a different default, to make it REQUIRED, or to document it again. It can’t change the property’s kind, or make a REQUIRED property optional.
  • Rules. A target of the new type is built by a rule of its own type where one applies, else by a rule of the type it extends, then of the type that one extends, and so on, before any default rule. A rule of its own type is chosen over one of the type it extends however specific the other is, so a type with only a build rule of its own is still tested and run by the rules of the type it extends.
  • The type extended can be declared in any .fabr file the project reads, a plugin’s included.

A type built on another works through an example, with a rule written in .fabr that builds on the extended type’s rule. A rule written in code can do the same by calling the extended type’s implementation (Which rule builds a target).

rule says how targets of a type are built: under which configuration, for which targets, and by what code.

rule js_package pkg<BUILD_OPERATION=build> = @fabr-build/js:rules/BuildJSPackage;
rule js_package pkg<BUILD_OPERATION=test, pkg.test_framework=mocha> = @example/mocha:rules/TestMocha;
default rule t<BUILD_OPERATION=files> = @fabr-build/core:rules/DefaultFilesRule;
  • After rule comes the target type, then a name for the target. default rule is a rule for every type, used for a type with no rule of its own that applies.
  • The guard, written against the name, says where the rule applies. A key is a property of the build configuration (BUILD_OPERATION), or a string property of the target through its name (pkg.test_framework). The values are glob patterns, as in a guarded property, and a rule with no guard applies everywhere.
  • Of the rules that apply to a target, the one with the most guard keys builds it, and two that are equally specific are an error.
  • The value is the rule’s implementation, an entry of a package: @fabr-build/js:rules/BuildJSPackage is the module rules/BuildJSPackage that @fabr-build/js exports. The package must be fabr’s core or a plugin the project uses, and the name means the copy fabr loaded. An implementation can also be written in .fabr; see Rules written in .fabr.

A rule’s implementation can be written in .fabr, as targets that build its target. The guide Writing your own rules shows how to use one; this is the grammar.

rule js_package pkg<BUILD_OPERATION=lint> = report<BUILD_OPERATION=build> {
generate report { srcs = pkg.srcs; run = checker **/*.ts > lint.txt; }
}
rule version_module v<BUILD_OPERATION=build> = generate gen { run = stamp ${v.version} > version.ts; }
  • = <reference> { <declarations> }: the reference says what the target is, and the block declares the targets and properties it may name, guarded ones included and nothing else. The block’s names are looked up before the file’s, and only within the rule.
  • = <type> <name> { <properties> } is the short form of a block declaring that one target, its name the reference.
  • The parameter (pkg) stands for the target being built: pkg.<property> reads one of its properties, as a value or as ${pkg.<property>}, pkg alone is a reference to the target, and ${pkg} is its name. A bare name starting <parameter>. always means the parameter’s property.
  • pkg; alone in the body of a target of the block splices the parameter, as a map of the target being built’s properties (see Splicing the target’s properties).
  • What follows the = is told apart by what comes after the first name: ; for a package entry, a second name for the short form, and { after a reference for a block.

repository says how repositories of a type are made:

repository npm_repository = @fabr-build/js:NPMRepository;

rule and repository followed by a name always begin these declarations, so neither can be used as a target type. Fabr’s core and its plugins declare the standard rules and repository types in their own .fabr files. An implementation is loaded only when its rule first builds a target, or its repository type first makes a repository.