Clause
← All documentation

TOML projects

A project manifest selects ordered groups of Erlang sources and configures each target independently. All CLI actions accept projects. See the working example.

Manifest (schema 1)

schema_version = 1

[[targets]]
name = "app"
sources = ["main.erl", "src/workers/*.erl", "shared/**/*.erl"]
source_dirs = ["src/support"]
output = "build/app"
entry = "main:main"
build_dir = "build"

[targets.options]
source_search_paths = ["src", "generated"]
include_dirs = ["include", "vendor/include"]
defines = ["DEBUG", "LIMIT=100", 'LABEL="demo"']
enable_features = ["maybe_expr"]
disable_features = ["compr_assign"]
ignore_dirs = ["src/legacy", "deps/*/test"]
ignore_modules = ["telemetry", "*_SUITE"]
keep_exports = false
shell = false
cache_inference_in_dirs = ["deps/*"]

[targets.options.applications]
my_dependency = "vendor/my_dependency"

CLI and target selection

clau [options] <source.erl>...
clau [options] --project <path> [--target <name>]...
clau --new-project <filename>

Each selected target is a separate compilation batch with its own preprocessing sessions. Planning (decode, select, discover, check output collisions) finishes before any source runs; execution then processes targets/files in order and latches failures. Payload output goes to stdout in target/file order without banners (except the --print-inputs target lines); context and diagnostics go to stderr. A target with diagnostics is named once, on a <manifest> [target <name>]: line before its first one; the diagnostics themselves (error: file:line:col: message) carry no tool or target prefix.

./build/debug/bin/clau --parse-check --project examples/project/project.toml
./build/debug/bin/clau --print-ast --project examples/project/project.toml --target tests --target app

Executables

clau --project FILE [--target T]... (no --emit, no check/print action) links every selected target that has an output or entry key, or that CLI -o/--entry addresses, into one executable per target:

./build/debug/bin/clau --project tests/fixtures/linking/project/project.toml

Creating a project

--new-project <filename> writes one annotated manifest and nothing else.

Build directory

Each project target has a build directory where Clause keeps its own temporary files and its caches: the target's build_dir, relative to the manifest, default <manifest-dir>/build. --build-dir DIR (invocation-relative) replaces it for every selected target. Positional inputs get none unless --build-dir is given.

Build caches

Results worth keeping between runs live below <build_dir>/cache; --no-cache neither reads nor writes them. They are the parse transform loader's replies and the host OTP's sticky module list, so a warm build starts no host erl for modules whose transform inputs did not change, each batch's compile outcome, and the inference results of modules in configured directories.

Inference cache

Library modules (OTP's stdlib, kernel, compiler and syntax_tools sources found through source_search_paths, Clause's own library) rarely change, so modules in configured directories keep their type inference results in the build cache, per module, and later compiles reuse them instead of inferring them again.

Paths and discovery

Directory index

Lookups by name do not probe every candidate directory. One index per clau invocation, shared by all targets, keeps the entry names of each searched directory: listed once, non-recursively, when a lookup first needs it, or recorded by the recursive walks that expand ** in sources, source_dirs, source_search_paths, include_dirs and transform_paths, so no directory is listed twice. It answers the misses of the module search (the library, then source_search_paths), transform sources, literal sources found through source_search_paths, and include resolution (the including file's directory, the working directory, include directories in order, then include_lib applications); a candidate the index holds is still opened or checked as before, so precedence, ignore_dirs and error reports do not change. Include names with directory parts (sub/x.hrl) are looked up in the directory they name; names with .. steps, absolute names, relative directories and listings that fail are probed as before.

Ignored sources

ignore_dirs and ignore_modules keep sources out of a target without removing them from the tree, e.g. vendored tests, legacy code or modules that only matter under OTP. An ignored source is silently skipped: it is never read, parsed, compiled or linked, so it may even hold syntax errors.

[targets.options]
source_search_paths = ["deps/**"]
ignore_dirs = ["deps/*/test", "src/legacy"]
ignore_modules = ["telemetry", "*_SUITE"]

Effective options

SettingManifest and CLI combination
include_dirsManifest order, after CLI -I (last CLI -I searched first); an entry with wildcards (deps/*/include, apps/**/include) expands in place to the existing directories it matches, sorted by UTF-8 path bytes (** also matches none, directory symlinks are skipped); an unmatched pattern is an error
source_search_pathsFallback roots for literal sources and the modules the target names but does not list; patterns expand like include_dirs (unmatched: unmatched source search path pattern)
definesManifest first, then CLI; duplicates across both are errors
applicationsManifest map; CLI entries replace matching names
Feature listsManifest settings, then ordered CLI changes; CLI wins
outputPer target, executable targets only; CLI -o overrides for a single selected target
entryPer target; CLI --entry overrides for a single selected target
cache_inference_in_dirsImplicit OTP patterns, then the manifest's, then CLI --cache-inference-in-dirs (inference cache)

Build dependency

Compiler-private toml++ 3.4.0 (MIT), header-only with exceptions, unreleased syntax disabled. Archive SHA-256 8517f65938a4faae9ccf8ebb36631a38c1cadfb5efa85d9a72e15b9e97d25155.

The build cache uses the SQLite 3.54.0 amalgamation (public domain), compiled into the compiler's cache library only (clause_sqlite, without the project's warning flags). CMake downloads https://www.sqlite.org/2026/sqlite-amalgamation-3540000.zip into thirdparty/ on every host and verifies its SHA-256 68e913b0fe8ec6b4e5f391c594a28872884e80d6cd042a1e50c957be46b5aa4d; -DCLAUSE_SQLITE_ROOT=<directory with sqlite3.c and sqlite3.h> takes precedence and disables the download.

Limits

Manifest size 1 MiB; at most 1,024 targets and 100,000 TOML nodes. Each source expansion allows 100,000 visited entries, 128 directory levels and 16,000,000 wildcard transitions. Exhaustion is an explicit error; these are work bounds, not I/O deadlines.