Writing your own rules
Every target has a target type, declared with
targetdef, and a rule says how targets of one
type are built under one operation: how a js_package is built, or how it is tested. The standard
rules come with fabr and its plugins, written in code. You can also write a rule in your own
.fabr files, out of targets you could declare yourself: run a tool over a target’s sources,
generate a file from its properties, or build it with another target’s rule. The target type can
be one a plugin provides or one you declare, and the operation one it already has or a new one.
A rule that needs more than targets can express, such as reading a tool’s report, is written in
TypeScript as part of a plugin, which your project can build itself as a fabr_plugin target;
see Writing a plugin.
You don’t call a rule: fabr applies it to every target of its type whose operation and guard it matches, as it does the standard rules, so each target gets its own copy of the rule’s targets, built from that target’s properties.
| To… | See |
|---|---|
| build a target in one step | A rule of one step |
| chain several steps | A rule of several steps |
add an operation, such as lint |
A rule for another operation |
| read the target’s own properties | Reading the target’s properties |
| pass all of them to a step | Splicing the target’s properties |
| declare a type that builds on an existing one | A type built on another |
| choose what the target is | What the target is |
| build a step under another operation or configuration | Configuration |
| replace a plugin’s rule | Choosing between rules |
| fix an error | Troubleshooting |
A rule of one step
Section titled “A rule of one step”This rule says how a version_module is built: by generating a TypeScript module from the
target’s version.
script stamp { entry = tools:stamp.sh; }
targetdef version_module { # The version to record. version = REQUIRED STRING;}
rule version_module v<BUILD_OPERATION=build> = generate gen { run = stamp ${v.version} > version.ts;}
version_module app_version { version = 1.4.0; }
js_package app { srcs = src:**/*.ts app_version:version.ts; }rule version_module v<BUILD_OPERATION=build>says the rule builds aversion_moduleunder thebuildoperation, and calls the target it buildsv.= generate gen { … }says the target is built as thegeneratewritten after the=, one whose properties are written as any target’s are.${v.version}is theversionof whicheverversion_moduleis being built.
tools/stamp.sh prints export const VERSION = "$1";, so app compiles a version.ts holding
1.4.0.
A rule of several steps
Section titled “A rule of several steps”A rule can be made of several targets, declared in a block after the =, with the name of the one
the target is written before it. This rule generates a TypeScript client from an API description
and compiles it, with any hand-written sources, into a package:
script gen_client { entry = tools:gen-client.sh; }
targetdef api_client { # The API description the client is generated from. spec = REQUIRED FILES; # Hand-written sources compiled with the generated client. srcs = FILES; # Packages the client's code imports. deps = PACKAGES;}
rule api_client c<BUILD_OPERATION=build> = client { generate code { srcs = c.spec; run = gen_client *.json > index.ts; } sources = code:index.ts c.srcs; js_package client { srcs = sources; deps = c.deps; }}
api_client weather { spec = api:weather.json; srcs = api:retry.ts;}
js_package app { srcs = src:**/*.ts; deps = weather;}coderuns the generator over the target’sspec.sourcesis a property of the block: the filecodewrote, named ascode:index.tsas any reference names a file another target built, and the target’s ownsrcs. A property of the block is written as a global is, and read the same way.clientcompilessourcesagainst the target’sdeps.= clientmakes eachapi_clientthe packageclientbuilds. That package is named after theapi_client(weather), soapp’s sources import it asimport { … } from "weather".
The one-step rule above is the short form of this: = generate gen { … } means
= gen { generate gen { … } }.
fabr build app builds the client first, reporting its steps as Building code for weather and
Compiling client for weather.
A rule for another operation
Section titled “A rule for another operation”A rule can also give a target type an operation of its own. This one gives every js_package a
lint operation that runs a checker over the package’s sources:
script checker { entry = tools:check.sh; }
rule js_package pkg<BUILD_OPERATION=lint> = report<BUILD_OPERATION=build> { generate report { srcs = pkg.srcs; run = checker **/*.ts > lint.txt; }}
js_package app { srcs = src:**/*.ts; }The targets in a block are built under the rule’s own operation, lint here, and generate has a
rule for build and none for lint. So report<BUILD_OPERATION=build> says the linted target is
report, built under build; Configuration has the details. This is also why
the rule can’t be written in the short form.
The operation’s name is a command, as build and test are, and a <…> constraint asks for the
operation where another command is given, quoted for the shell:
fabr lint appfabr cat 'app<BUILD_OPERATION=lint>:lint.txt'Every js_package now has its own report, built from its own sources, and
fabr list-targetdefs lists lint among the operations of js_package.
Reading the target’s properties
Section titled “Reading the target’s properties”Inside the block, the name after the rule’s type (pkg, v) stands for the target being built.
Its properties are read through it:
| Written in the block | Means |
|---|---|
srcs = pkg.srcs; |
The target’s srcs. The property it is written in decides how it is read: as files, as packages, or as a string. |
srcs = pkg.srcs extra.ts; |
The target’s values, then the block’s own. |
srcs = pkg.srcs:**/*.ts; |
Only the files the target’s srcs names that match **/*.ts. |
run = stamp ${v.version} > version.ts; |
The target’s string value, substituted as any ${…} is. |
run = stamp ${v} > version.ts; |
The target’s name (app_version). |
defines = pkg.defines; |
The target’s MAP property, as one map property naming another is read. A COMMAND property is read the same way. |
srcs = pkg.srcs<BUILD_TYPE=release>; |
The target’s srcs as it declares them for that configuration, with what they name built in it. |
deps = pkg<BUILD_OPERATION=build>; |
The target itself, built under another operation. |
A value the target wrote means what it means where the target is declared: a relative path is
relative to the target’s own file, and a name is the name in that file, whatever the block declares.
A guarded property of the target (srcs<TARGET=*-linux-*> = …) is chosen by the configuration the
target is being built in.
A name in the block that starts with the parameter and a . always reads the parameter’s property,
so a file named that way is written with a ./ (./pkg.json). Where the rule has a type, every
property read this way must be one the type declares, and fabr reports one that isn’t when it reads
your files.
Splicing the target’s properties
Section titled “Splicing the target’s properties”To give a target of the block every property of the target being built, write the parameter alone in its body, as a statement. The parameter stands for the target as a map of its properties, and splicing a map into a target takes its entries as the target’s properties:
rule js_package pkg<BUILD_OPERATION=release> = released<BUILD_OPERATION=build> { js_package released { pkg; version = 1.0.0; }}fabr release app then builds app as a js_package at version 1.0.0: released has each
property as app has it, as though you had written srcs = pkg.srcs; deps = pkg.deps; and so on
yourself. (released is built under build, by js_package’s own rule; under release it would
be this rule again.)
- The order matters, as in a map:
version = 1.0.0;comes afterpkg;, so it replacesapp’sversion. Written beforepkg;, it would be used only whereapphas noversionof its own. - A property
apphas a value for is taken, whetherappsets it or gets it from its type’s default. Oneapphas no value for at all is left to the block target’s own default. - A property of
appthat the block target’s type doesn’t have is left out.
A type built on another
Section titled “A type built on another”A new target type can extend an existing one: it has all of that type’s properties, adds its own, and is built by that type’s rules wherever it has no rule of its own. A rule for the new type then only needs to cover what is different, and it can splice the target into a target of the existing type to have that part built as before.
For example, a documented_package is a js_package that ships its documentation in its
package, under docs/:
plugin @fabr-build/js;
# A package that ships its documentation beside its code.targetdef documented_package extends js_package { # Documentation files, shipped under docs/ in the package. docs = FILES;}
rule documented_package pkg<BUILD_OPERATION=build> = code { js_package code { pkg; resources = pkg.resources pkg.docs:** -> docs/**; }}
documented_package greeter { srcs = src:**/*.ts; exports = src:index.ts; tests = src:**/*.test.ts; test_deps = @npm:@types/node:22.10.0; docs = docs:*.md;}greeteris written like anyjs_package, with one more property,docs.- Its own rule is for
buildonly.codeis ajs_packagewith all ofgreeter’s properties, exceptresources, which it sets togreeter’s resources plus its documentation, renamed underdocs/.codeis the rule’s result, so the package it builds isgreeter. - For every other operation,
greeteris ajs_package:fabr test greeterruns its tests withjs_package’s test rule, andfabr runandfabr depswork as they do for any package.
$ fabr ls greeterdocs/guide.mdindex.d.tsindex.jsindex.js.mappackage.json$ fabr test greeter✓ Testing greeter (727ms)greeter: 1 test passedfabr list-operations lists documented_package under each operation it has a rule for, its own
and js_package’s.
What the target is
Section titled “What the target is”The reference between the = and the block says what the target is:
- One whole target (
report,report<BUILD_OPERATION=build>) makes the target that one: a package if it builds a package, and files otherwise. What depends on the target gets what that one builds, and what it requires. - Anything else makes the target the files it names: a selection such as
report:lint.txt, or a property of the block listing several targets (contents = classes:** docs:**;with= contents { … }).
The block’s targets and properties belong to the rule: they are looked up before your files’ names, and nothing outside the rule can name them. A block declares only targets and properties, guarded ones included.
A target in the block is built under the name of the target the rule is building, as that target’s
own work, so whatever its rule names after its target is named after the target being built (see
Packages are named after the target). The block’s name for a target is
how the block refers to it, and how fabr’s progress output and errors name its work, as in
Building report for app.
Since every target in the block is a package of the target’s name, and a property read as packages
takes one package of each name, a target of the block can list only one other target of the block
as a package (or the target itself under other constraints, pkg<BUILD_OPERATION=build>): two
fail with '<name>' is required here as two different packages. Give one of them a name of its
own with -> (deps = api -> app-api impl;), or, where you want their files rather than the
packages, name them for their files (classes:classes/**).
Package versions
Section titled “Package versions”The result, and every target of the block it reaches through what each target requires, resolve
their packages together, as the parts of one target do: what they build against as one set of
versions, and what they install or run as another, each target’s requirements joining the set
its own rule puts them in. A target is reached when another one names it among what it requires,
for its package (deps = api) or only for its files (resources = classes:classes/**). So in
Java’s war_package rule, the deps that classes compiles against are one set, the jars libs
lays into WEB-INF/lib the other, and a package any reached target adds beside w.deps raises
its version for the whole of its set. Where only one set asks for a later version of a package,
the two can hold different versions of it, as a Java project’s compile and runtime classpaths can (Compile and runtime classpaths).
A target of the block that the result doesn’t reach this way, such as a program another target
runs as a command, has its versions chosen for it alone, as any tool’s are. A target in the block whose own
type is built by a rule written in .fabr is reached like any other, and the targets of its rule’s
block join the same resolution.
Versions that conflict are an error for a target in the block that uses them together, unless its
type collects its packages permissively, as java_program does.
What fabr deps shows
Section titled “What fabr deps shows”fabr deps on a target a rule builds shows that resolution from the target’s side: a section for
each of the target’s own properties the block reads (deps, provided_deps), holding what it
resolved to, and a section required by <type> rule for what the block adds itself. The block’s
own targets aren’t shown; what they pass on is in those sections. Where the result is another
target of your project, outside the block, it’s shown under the rule’s section, as what the target
is made from. Where the result is the target itself under another operation, as in
= pkg<BUILD_OPERATION=build> { … }, run fabr deps on the target under that operation to see
what it requires.
Configuration
Section titled “Configuration”A target in the block is built in the configuration the rule applies in, as any target a reference
names is: under the rule’s own operation, and with the same settings. A rule for an operation other
than build must therefore ask for the operation its steps have a rule for, as the lint rule does
with report<BUILD_OPERATION=build>. The short form has nowhere to write that, so it suits a rule
whose operation its one target’s type has a rule for, such as a build rule.
A <…> constraint on a reference in the block applies to that target alone: the lint rule’s report
is built under build while the js_package it reports on is still being linted.
Choosing between rules
Section titled “Choosing between rules”A rule written in .fabr is chosen as any rule is. Each rule’s guard (the <…> after its
parameter) says where it applies, and of the rules that apply to a target, the one with the most
guard keys builds it. Two equally specific rules are an error.
To build some of a plugin’s targets your own way, give your rule a guard with more keys than the plugin’s rule, such as a property of the target:
rule js_package pkg<BUILD_OPERATION=build, pkg.flavour=legacy> = … { … }A rule with the same guard as a plugin’s is an error rather than a replacement.
Troubleshooting
Section titled “Troubleshooting”Cannot perform 'lint' on 'report': no rule matches target type 'generate'
A target in the rule’s block is built under the rule’s own operation, and its type has no rule for
that operation. Ask for one it has a rule for on the result: write
= report<BUILD_OPERATION=build> { generate report { … } } instead of the short form
= generate report { … }. The help: line’s fabr build report doesn’t apply: report is a name
only the rule can use.
'pkg.sources' reads a property 'js_package' does not declare
The block reads a property of the target that its type doesn’t have. Check the property’s name
against fabr list-targetdefs.
'rule …' is applied inside its own application
A target the rule’s block declares, or one built from it, is of a type and configuration the same rule matches, so the rule would be applied to it again without end. Give that target a type or a configuration the rule doesn’t match, or narrow the rule’s guard.
'g.whom' has no value
A ${…} in the block reads a property the target doesn’t set, or that its type doesn’t have. Set
it on the target, or give it a default in the targetdef.