Manifest
mach.toml declares a project. It separates the orthogonal axes of a build - what is produced ([artifact.*]), where it runs ([target.*]), how it is compiled ([profile.*]), and what it links or must run first ([link.*], [step.*]) - and the build engine takes their product. Nothing is inferred from another key.
A root manifest is parsed strictly and totally: every required key must be present, and an unknown key is an error rather than a silent no-op. A dependency's manifest is read permissively - only the keys the consumer needs are consulted - so a library cannot impose its build choices on you.
The schema
[project]
id = "demo" # required: identifier; root of every module path
version = "0.1.0" # required
src = "src" # required: source dir, project-root-relative
out = "out/{target.name}/{profile.name}" # required: output-path template root
[target.linux] # a platform: a fully-spelled tuple
isa = "x86_64"
os = "linux"
abi = "sysv64"
[profile.debug] # a build variant
opt = 0 # 0 (debug pipeline) | 1 | 2 (release pipeline)
debug = true # emit debug info for this profile
simd = "scalarize" # SIMD lever: "scalarize" | "require"
[artifact.demo] # a produced artifact
kind = "bin" # "bin" | "static" | "shared"
entry = "main.mach" # entry source, relative to src
out = "bin/demo" # output path, relative to the project out
targets = ["*"] # which declared targets build it ("*" = all)
link = [] # [link.X] names this artifact links
need = [] # [step.X] names this artifact demands directly
[dep.std] # a dependency
git = "https://github.com/briar-systems/mach-std"
ref = "branch/main"
[project]
| Key | Type | Meaning |
|---|---|---|
id | string | Root segment of every module path the project exposes: a file at <src>/foo/bar.mach is reachable as <id>.foo.bar. Must be a plain identifier - letters, digits, _, - - since it names the dependency store and keys step stamp files. Read by $project.id. |
version | string | Project version. Read by $project.version; the source of truth a tag/<version> acquisition checks. |
src | string | Source root, project-root-relative. Module paths resolve under it. |
out | string | The output-path template root, referenced as {project.out} by artifact out, step paths, and cmds. |
mach | string | Optional. The minimum compiler version the project needs, as semver, validated when the manifest is read. |
A project using a feature added last week otherwise fails on an older toolchain with an ordinary parse error pointing at the feature rather than at the version. mach is checked for the root project and for every dependency, so a dependency that outgrows the running compiler says so in its own name rather than failing somewhere in its source.
[project]
mach = "4.15.0"
[project] is exactly these keys. name, description, and any other key are unknown-key errors in a root manifest.
Targets
Each <name> in [target.<name>] is a selector you pass to --target <name>. A target is a fully-spelled platform tuple; nothing is inferred from another key.
| Key | Req | Meaning |
|---|---|---|
isa | yes | Instruction-set architecture. Read by $project.target.arch. |
os | yes | Operating system. Read by $project.target.os. |
abi | yes | Application binary interface. Read by $project.target.abi. |
of | no | Object-format override; defers to the os's format when omitted. |
base | no | Load-address override (integer). Overrides the os's default base virtual address; defers to it (0 for freestanding) when omitted. |
platform | no | Open platform tag (string), surfaced to comptime as $mach.build.platform (empty when unset). A support library keys its backend on it; the compiler treats it as opaque. |
native is a reserved name - declaring [target.native] is an error, because native resolves to whichever declared target matches the host.
Accepted tuple values
| Axis | Values |
|---|---|
isa | x86_64, aarch64, riscv64, riscv32, spirv, mos6502 |
os | linux, windows, darwin, freestanding |
abi | sysv64, win64, aapcs64, lp64, lp64f, lp64d, ilp32, ilp32f, ilp32d, spirv, mos6502 |
of | elf, coff, macho, raw, spv |
A value outside its axis's set is a strict-parse error, so a typo is caught rather than silently never matching. mach info targets prints the tuples this binary can actually build; it is derived from the same declarations composition reads, so it never advertises a tuple that would fail to resolve.
lp64 means soft float: arguments ride integer registers even where the hardware has an FPU. lp64f passes single-precision floats in float registers and lp64d passes both single and double, which is what a Linux riscv64 toolchain means by its default. ilp32, ilp32f, and ilp32d are the same three on 32-bit RISC-V. Passing lp64 where lp64d was meant is an ABI mismatch against every C object on the system, not a performance choice.
Each os has a default object format - linux to elf, windows to coff, darwin to macho, freestanding to raw - and of names a different one. An os accepts only the formats it can load, so an override the os cannot enter is refused. The default is a function of the whole tuple, not the os alone: a spirv target resolves to spv regardless of the os it names.
A freestanding target can select of = "elf" on every architecture, and gets an image with no loader construct in it: no header segment, since nothing parses a program header at run time on bare metal.
[target.metal]
isa = "x86_64"
os = "freestanding" # os default object format is "raw"
abi = "sysv64"
of = "elf" # override: emit an ELF image instead
--pie, dynamic linking, and a shared library are each refused by name on a loaderless os: a position-independent executable exists to be relocated by a program loader, a dynamically-linked one to have its imports resolved by one, and os = "freestanding" has none.
Finished-module targets
A spirv target's object output is a complete, self-contained module rather than a link input. The build delivers the module tree - one <out>/obj/<fqn-as-path>.spv per module - and runs no link phase, so a default build and --emit obj produce the same files.
[target.gpu]
isa = "spirv"
os = "freestanding"
abi = "spirv"
# no `of`: the finished-module format resolves on its own
The artifact's out template and -o name a linked binary, which such a target has none of; the module tree is delivered instead. A static or shared artifact kind, and mach test, are refused by name.
Platform targets (bare metal)
A bare-metal platform is not its own os. It is os = "freestanding" plus two optional keys: a base load-address override, and an open platform tag a support library keys its backend on.
[target.bmos]
isa = "x86_64"
os = "freestanding"
abi = "sysv64"
of = "raw"
base = 0xFFFF800000000000
platform = "bmos"
Artifacts
Every artifact is declared explicitly and named by its table key. $project.name reads the selected artifact's name.
| Key | Req | Meaning |
|---|---|---|
kind | yes | "bin", "static", or "shared". |
entry | yes | Entry source, relative to the project src dir. The entry module's FQN is <id>.<entry without .mach>, with / turned into .. |
out | yes | This artifact's output path, relative to the expanded project out and rooted there automatically - write bin/demo, not {project.out}/bin/demo. An executable extension, where wanted, is written literally here. |
targets | yes | Array of declared target names this artifact builds for; ["*"] means every declared target. |
link | yes | Array of [link.X] names this artifact links. [] for none. |
need | yes | Array of [step.X] names this artifact demands directly, for step outputs that are not themselves link inputs. [] for none. |
subsystem | no | "console" (default) or "gui" - the environment a windows executable declares it runs under. |
icon | no | Project-root-relative .ico path embedded in a Windows executable's PE resources. bin artifacts only. |
manifest | no | Project-root-relative application-manifest path embedded byte-for-byte in a Windows executable's PE resources. bin artifacts only. |
binlinks an executable at the resolvedoutpath.staticmaterialises a realararchive at the resolvedoutpath - the per-module objects with an archive symbol index, the deliverable a consumer links as a.a.sharedis reserved for a shared-library deliverable; its emission is a later phase.
Per-target extension or per-target entry is not a per-cell exception table - it is a second artifact stanza, so the condition stays visible like everything else.
Windows subsystem and resources
A PE executable records in its optional header which environment it wants, and the Windows loader honours it: "console" gets a console window attached to the process, "gui" does not. A graphical application sets "gui" to stop an empty console from opening behind it on launch. --subsystem console|gui overrides the key for one invocation.
[artifact.game]
kind = "bin"
entry = "main.mach"
out = "bin/game.exe"
targets = ["*"]
link = []
need = []
subsystem = "gui"
icon = "assets/game.ico"
manifest = "assets/game.manifest"
These keys take no os filter and are not an error on a linux or darwin target. Only the PE writer consumes them, so on any other target they are accepted and inert - the manifest stays one declaration read by every build, rather than a per-platform file.
Profiles
A [profile.<name>] is a build variant. The optimization level and the debug-emission toggle live here because they are variant concerns.
| Key | Req | Meaning |
|---|---|---|
opt | yes | Optimization level: 0 selects the debug pipeline (the always-on passes only), 1 and 2 select the release pipeline. Any other integer is a manifest error. |
debug | yes | Emit debug info (DWARF on ELF / Mach-O, CodeView on COFF). Gates emission only, never the optimizer, so a release profile can keep symbols with debug = true. |
simd | yes | SIMD scalarization lever. "scalarize" builds for a target without hardware SIMD by emitting a defined unrolled scalar expansion of each vector operator; "require" makes a build for an incapable target a hard error naming the offending operator. |
vectorize | no | Auto-vectorization lever, default true. When true, the release pipeline rewrites provably-safe counted loops to 128-bit SIMD on a target with hardware vectors; false skips the pass. It only ever subtracts, so it changes performance and never semantics. |
float_reassoc | no | Permission to treat floating-point addition and multiplication as associative, default false. It lets the vectorizer reduce an f32 / f64 accumulator through lane-count partial sums, which changes the result. The only profile key that can change a program's computed answer. |
Emission of the human-readable IR and assembly side-artifacts is not a profile concern - it is controlled only by the --emit-ir / --emit-asm CLI flags.
The simd, vectorize, and float_reassoc levers are always the consumer's. A dependency's [profile.*] is parsed permissively and never read to build the consumer, so a library's values are inert. There is no ecosystem fork and no dual API.
Link requirements
A [link.<name>] is a named external link requirement. Artifacts reference entries by name in their link = [...]; an entry whose filters do not match the build cell is skipped. An entry with export = true also applies to any project that links this project's modules, so a platform link requirement lives once - in the manifest that needs it - and cascades to consumers.
| Key | Req | Meaning |
|---|---|---|
source | yes | "system" (a system library resolved by name), "framework" (a macOS framework), or "local" (a file on disk). |
name | shape | Library / framework name - required for "system" and "framework", forbidden for "local". |
path | shape | File path - required for "local", forbidden otherwise. A template. |
library | no | Stable logical name used by #[library("...")]; defaults to the table name. |
symbols | no | Array of symbol names this dependency provides, attributing imports that have no ext declaration to decorate. |
os / isa / abi | yes | Filter axes: a canonical value, "*" (any), an array of values, or [] (none). An entry applies to a cell when all three match. |
export | yes | true cascades this entry to consumers; false keeps it to this project's own builds. |
[link.kernel32]
source = "system"
name = "kernel32.dll"
library = "kernel32"
symbols = ["Sleep", "CreateFileW", "CloseHandle"]
os = "windows"
isa = "*"
abi = "*"
export = true
library decouples source attribution from platform loader spelling. Give mutually exclusive platform entries the same logical value when they provide the same API; one unconditional #[library("glfw")] can then bind against libglfw.so.3 on Linux, an install name on Darwin, and glfw3.dll on Windows.
symbols names the symbols the dependency provides. On a two-level-namespace format (PE, Mach-O) every import must identify its provider, and #[library] can only attribute a symbol your Mach source declares. A vendored static archive leaves its own undefined references with no declaration to decorate, so the entry that provides them claims them. A symbol may be claimed only once per link.
Whether an input links statically or dynamically follows the resolved file: a loose .o / .obj or static .a / .lib links statically; ELF .so, Mach-O .dylib, and PE .dll inputs are recorded using their format's canonical loader name.
Build steps
A [step.<name>] is a command, make-recipe style, that produces files a build consumes - typically a local link input, such as a vendored-C object.
| Key | Req | Meaning |
|---|---|---|
cmd | yes | One command string, run through the platform shell (sh -c on posix, cmd.exe /C on windows) from the project root. Templates expand in it. |
in | yes | Declared input file list. Accepts globs (*, **), expanded sorted for a stable fingerprint; a glob that matches nothing is a hard error. |
out | yes | Declared output file list. Concrete paths only - a glob here is an error, since the demand match and cache key expand out verbatim. |
need | yes | Array of other [step.X] names this step must run after. Cycles error. [] for none. |
Steps carry no filters and never run automatically. A step runs only when demanded: by a selected [link.X] whose local path matches the step's out, by another step's need, or by an artifact's need. Because a step has no filter of its own, the condition for running it lives in the link entry that demands it.
A step is cached by content: its in contents plus its expanded cmd fingerprint the step. An unchanged step whose outputs still exist is skipped. Every step process inherits the active build cell's target tuple as MACH_TARGET_ISA, MACH_TARGET_OS, and MACH_TARGET_ABI.
Dependencies
Each [dep.<alias>] names a dependency materialised under dep/<alias>/. The build resolves a dependency purely by vendor layout: it reads that directory's own mach.toml for its [project].id and src. A module path whose head matches a dep's id resolves into that dep's tree. A stanza declares exactly one source key.
| Key | Meaning |
|---|---|
git | Git URL to clone into dep/<alias>/. |
path | Local path to another project tree, resolved relative to this manifest; never fetched. mach dep pull materialises it at the vendor location as a relative symlink. |
ref | Git ref to check out (with git): tag/<name>, branch/<name>, a bare tag or branch, or a commit SHA. An absent ref means the remote default branch. |
A git dep is pinned to a resolved commit in mach.lock; a path dep has no pinned content, so it carries no lock entry. mach dep performs only plain git operations, so a checkout you also commit as a submodule composes naturally; mach never invokes git submodule.
A registry-style version = key is reserved and rejected. Cloning, lockfile handling, and transitive resolution are covered in Dependencies.
Bare project-id imports
A module path whose head segment matches a declared dependency's [project].id resolves into that dependency's source tree. The alias under dep/ is a directory name; the id is what source code writes.
Path templates
Paths and cmds expand over a closed, final set of six variables:
| Variable | Expands to |
|---|---|
{project.out} | the root project's expanded [project].out, in every manifest of the closure |
{target.name} | the resolved target name (never the literal native) |
{target.isa} | the resolved target's isa, e.g. x86_64 |
{target.os} | the resolved target's os, e.g. linux |
{target.abi} | the resolved target's abi, e.g. sysv64 |
{profile.name} | the selected profile name |
An artifact's out is relative to the expanded project out and is rooted there automatically. Step out lists and local link paths are not auto-rooted: they name {project.out} explicitly, which is what homes a dependency's build products into the consumer's output tree rather than the dependency's checkout.
There are no {name} / {ext} or bare {target} / {profile} aliases. An unresolvable {...} reference, or an unterminated {, is a strict-parse error. {project.out} is not available inside [project].out itself.
The build matrix
A build cell is one artifact times one target times one profile.
mach build <path>builds every declared artifact whosetargetsincludes the selected target, for the default profile.--all-targetscrosses every artifact with every target in itstargets.mach run <path>andmach test <path>build exactly one artifact; with several declared and no narrowing flag, they ask you to pick one, naming every candidate.- A cell whose artifact does not list the cell's target is a cell the manifest never declared, so enumerating skips it. Naming that pair is a different act and is refused by name, because you asked for a cell that does not exist.
- Every cell is attempted; a failure does not abandon the ones after it. Each cell's diagnostics are reported under its own heading, and every cell that succeeded leaves its artifact on disk.
-ois accepted exactly when the selection resolves to a single build cell, and refused otherwise, naming the cells it resolved to.
Artifacts cannot share an output path: a manifest whose expanded out templates collide is rejected before the build starts.
native target resolution
native resolves the host's (isa, os) against the declared targets only, never a synthesized tuple. Exactly one host match is chosen; several matching tuples is an ambiguity error naming the candidates; no match warns and falls back to the first declared target, so a cross-only project still builds on a foreign host.
Annotated example
A project that cross-compiles to linux and windows, links a system library on each, and vendors a C object through a build step. Note where the conditions live: [step.miniz] carries no filter of its own, so it runs only because [link.miniz] demands it - and that entry is gated to os = "linux", since the host cc emits an ELF object. On the windows cell the entry filters out, the step is never demanded, and it never runs. To vendor C for the windows cell too, add a second step whose cmd cross-compiles (it can branch on MACH_TARGET_OS) and a second link entry gated to os = "windows".
[project]
id = "demo"
version = "0.1.0"
src = "src"
out = "out/{target.name}/{profile.name}"
[target.linux]
isa = "x86_64"
os = "linux"
abi = "sysv64"
[target.windows]
isa = "x86_64"
os = "windows"
abi = "win64"
[profile.debug]
opt = 0
debug = true
simd = "scalarize"
[profile.release]
opt = 2
debug = false
simd = "scalarize"
[step.miniz] # builds the vendored C object the link entry below demands
cmd = "cc -c vendor/miniz.c -o {project.out}/miniz.o"
in = ["vendor/miniz.c"]
out = ["{project.out}/miniz.o"]
need = []
[link.miniz]
source = "local"
path = "{project.out}/miniz.o" # matches step.miniz's out, so it demands that step
os = "linux" # host `cc` emits an ELF object, so this entry is linux-only
isa = "*"
abi = "*"
export = false
[link.kernel32]
source = "system"
name = "kernel32.dll"
os = "windows" # skipped on the linux cell, never a per-platform file
isa = "*"
abi = "*"
export = true
[artifact.demo]
kind = "bin"
entry = "main.mach"
out = "bin/demo"
targets = ["*"]
link = ["miniz", "kernel32"]
need = []
[dep.mach-std]
git = "https://github.com/briar-systems/mach-std"
ref = "branch/main"
See also
- CLI - the flags that select a target, profile, and artifact
- Dependencies -
mach dep, the lockfile, and transitive resolution - Project layout - how
srcmaps to module paths - Decorators -
#[library], which resolves against a[link.X]identity