How-to guides
Task-oriented how-tos. Everything described here is implemented. Installation and the first build are in 62-getting-started.md, the complete option list in 60-cli.md, and the manifest syntax in 12-build-reference.md.
1. Building
dowel build # build every bin / test
dowel build app # by name: <target> or <package>:<target>
dowel build --config=release
- Switch configurations with
--config(debug/release; defaultdebug). Build directories are separated per configuration, so switching never clobbers the other’s outputs - The default backend is ninja. Where ninja is unavailable,
--backend=directruns the steps sequentially with no external generator.--backend=makegenerates aMakefile, and--backend=graphwrites the build description for a tool of your own (14-build-graph.md) -j/--jobsis the parallelism passed to the backendcompile_commands.jsonis written on every build; suppress with--no-compdb
2. Running tests
dowel test # every test target
dowel test app:unit # by name
dowel test --nocapture # pass output through (default shows failures only)
dowel test --failed # rerun only what failed last time
dowel test --fail-fast # stop at the first failure
dowel test --test-jobs=4 # run 4 at a time (default is sequential)
dowel test --no-run # build only; do not run
dowel test --label=fast # only cases tagged `fast`
dowel test app:suite/parse # one case, by the label the output prints
dowel test --no-run # list what would run, without running it
One binary can register several tests, each with its own arguments, timeout, expected verdict, and labels:
[test.suite.cases]
parse = { args = ["parse"], timeout = 10 }
rejects = { args = ["bad"], should_fail = true }
heavy = { args = ["heavy"], labels = ["slow"] }
Or, where the suite already enumerates itself, let the binary list its own cases:
[test.suite.harness]
list = ["--list"] # prints one case name per line
run = ["--run"] # this, then the name, runs one case
Results, --failed, and --label then work per case either way
(12-build-reference.md).
- Pass/fail is the exit status (0 = success). A case killed by a signal
fails,
should_failor not — that declares a nonzero exit, not a crash - The working directory is the package root. A case that needs another one
says so:
golden = { args = ["golden"], cwd = dir("tests/golden") } - The default is sequential because C tests may use shared resources (the same working directory, fixed ports, output files). Results are always displayed in request order
- The verdicts behind
--failedpersist in the build directory; verdicts of targets that were not run are kept
3. Switching configurations and feature flags
Branching on the manifest side uses match / when
(12-build-reference.md section 5). From the CLI:
dowel build --config=release
dowel build --features=zlib,png
dowel build --no-default-features
The domain of feature names is defined by [features] in dowel.toml.
Unknown names fail with a diagnostic — both when referenced from dowel.build
and when passed to --features — with suggestions. Switching --config /
--target does not re-run manifest evaluation (branch resolution is deferred
to the specialization stage).
4. Investigating “why”
Value provenance: where a propagated value came from, with source locations.
$ dowel why app:app includes
include/ Path
← public.includes of target:foo libfoo/dowel.build:18
← deps of target:app app/dowel.build:7
Graphs: the target dependency graph and the action graph, as text / dot / json.
dowel graph # target dependency graph
dowel graph --kind=action # action graph
dowel graph --format=dot | dot -Tsvg -o graph.svg
Rebuild reasons and the actual command lines are in the log:
DOWEL_LOG=debug dowel build # per-stage timing, graph sizes, freshness verdicts
DOWEL_LOG=trace dowel build # dependency edges, the full command line of every action
The per-source breakdown of trace output is in 91-implementation-status.md.
5. Cross compilation and runners
Declare the toolchain per target triple with [toolchain.<triple>] in
dowel.toml, and an execution wrapper with [runner.<triple>] in
dowel.build. dowel build --target=<triple> compiles with the declared
toolchain, and dowel test --target=<triple> launches through the wrapper
transparently. A --target with no declared toolchain is refused before
building (missing-toolchain) — the host compiler is never substituted.
# dowel.toml
[toolchain.riscv64gc-unknown-linux-gnu]
c = "riscv64-linux-gnu-gcc"
ar = "riscv64-linux-gnu-ar" # archives too — do not fall back to the host's ar
For bare-metal work, declare the tools that turn the ELF into something a programmer can write, and the images become part of the build:
# dowel.toml
[toolchain.thumbv7em-none-eabihf]
c = "arm-none-eabi-gcc"
ar = "arm-none-eabi-ar"
objcopy = "arm-none-eabi-objcopy"
# dowel.build
[bin.firmware.artifacts]
bin = { tool = "objcopy", args = ["-O", "binary"] }
hex = { tool = "objcopy", args = ["-O", "ihex"] }
dowel build --target=thumbv7em-none-eabihf now produces firmware.bin and
firmware.hex next to the ELF, re-running the conversion only when the ELF
changed (12-build-reference.md).
qemu:
[runner.riscv64gc-unknown-linux-gnu]
command = "qemu-riscv64"
args = ["-L", "/usr/riscv64-linux-gnu"]
Real hardware over SSH. When the target machine cannot see the build machine’s file system, declare a transfer:
[runner.aarch64-unknown-linux-gnu]
host = "board.local"
remote_dir = "/tmp/dowel"
transfer = ["scp", "-q"]
command = "ssh"
args = ["board.local"]
This expands to the following. Source and destination paths are not written in the manifest; the implementation appends them (ADR-0008).
scp -q <build>/bin/unit_test board.local:/tmp/dowel/unit_test
ssh board.local /tmp/dowel/unit_test
transferandremote_dirare specified together- Pass/fail is the exit status of the launch command; with
ssh, the target machine’s exit status is the verdict - If no runner is declared for a triple that differs from the host, the launch
is refused with a diagnostic beforehand (rather than surfacing as an
Exec format errorreported as a test failure)
6. Writing in an editor
There are three paths; they serve different files.
| Target | Path |
|---|---|
dowel.build / dowel.toml |
dowel lsp — a language server providing diagnostics and hover |
| C sources | clangd, fed by the compile_commands.json that dowel build writes |
| VS Code | the client in editors/vscode/: launches dowel lsp and adds syntax highlighting |
dowel lsp speaks LSP on stdin/stdout; the editor is the one that starts it
(nothing stays resident). Hover shows property types and merge rules, builtin
function signatures, and configuration key domains. Diagnostics cross files:
with dowel.toml and dowel.build open, an unknown feature name, an
undeclared dependency, or a merge conflict with a dependency shows up in the
editor, against unsaved buffer contents. Plan-stage checks (glob expansion,
path resolution, toolchain probing) still belong to dowel check.
7. Managing the cache
Memoized evaluation results live under .dowel/cache/.
dowel cache info # store size
dowel cache gc # collect stores left by older formats
cache info/cache gcdo not read the manifests, so cleanup works even when a manifest is broken- Deleting the store never loses correctness — only the cached speedup
- The writer is limited to one process (
flock); a process that cannot take the lock reads only
8. Using from CI and tools
Output can be machine-readable. stdout carries artifacts and stderr carries progress and logs — always — so piping is safe.
dowel check --message-format=json # one diagnostic per line, with stable codes, locations, fix suggestions
dowel test --message-format=json # one test result per line
dowel build --log-format=json # logs as JSON too
dowel schema dump # the schema and configuration vocabulary, machine-readable
- Exit status: 0 = success (including warnings only); anything else = an
error.
dowel testreturns nonzero if even one test fails - Diagnostic codes (
unknown-propertyand so on) are a compatibility surface - The output of
dowel schema dumpis also intended as context for LLM agents (30-devexp.md section 4)
9. Migrating from CMake
Start from a draft extracted out of the real configuration, then keep checking against the old build until the port is equivalent:
# 1. have CMake emit its model, and draft manifests from it
mkdir -p build/.cmake/api/v1/query && touch build/.cmake/api/v1/query/codemodel-v2
cmake -B build ...
dowel migrate import build # writes UNVERIFIED dowel.toml / dowel.build
# 2. edit the draft (promote public headers, restore conditionals), then
dowel migrate verify build/compile_commands.json
The draft is deliberately conservative — everything private, sources listed explicitly — because it is a snapshot of one configuration and the intent is lost (60-cli.md).
Sources are matched one by one and their compile arguments compared after
normalization (spelling differences like -DX vs -D X, relative vs
absolute -I, and output/depfile flags don’t count). A ported source with
differing arguments fails the run and lists each difference with its
direction; sources you haven’t ported yet are reported but don’t fail —
migration proceeds target by target. --format=json for CI
(60-cli.md).
10. Reading diagnostics
- Human output uses the rustc format: severity, a stable code, location labels, notes, and fix suggestions
- Unknown names (properties, functions, configuration keys, feature names,
matcharms, CLI options) come with edit-distance suggestions - In
--message-format=json, fix suggestions carry a span and a replacement string, and can be applied mechanically - When stuck, close in from both sides:
dowel why(value provenance) andDOWEL_LOG=debug(execution reasons)