machdocs
Home GitHub

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.

Totality

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]

KeyTypeMeaning
idstringRoot 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.
versionstringProject version. Read by $project.version; the source of truth a tag/<version> acquisition checks.
srcstringSource root, project-root-relative. Module paths resolve under it.
outstringThe output-path template root, referenced as {project.out} by artifact out, step paths, and cmds.
machstringOptional. 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"
Restriction

[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.

KeyReqMeaning
isayesInstruction-set architecture. Read by $project.target.arch.
osyesOperating system. Read by $project.target.os.
abiyesApplication binary interface. Read by $project.target.abi.
ofnoObject-format override; defers to the os's format when omitted.
basenoLoad-address override (integer). Overrides the os's default base virtual address; defers to it (0 for freestanding) when omitted.
platformnoOpen 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.
Restriction

native is a reserved name - declaring [target.native] is an error, because native resolves to whichever declared target matches the host.

Accepted tuple values

AxisValues
isax86_64, aarch64, riscv64, riscv32, spirv, mos6502
oslinux, windows, darwin, freestanding
abisysv64, win64, aapcs64, lp64, lp64f, lp64d, ilp32, ilp32f, ilp32d, spirv, mos6502
ofelf, 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.

RISC-V is a family of ABIs

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.

KeyReqMeaning
kindyes"bin", "static", or "shared".
entryyesEntry source, relative to the project src dir. The entry module's FQN is <id>.<entry without .mach>, with / turned into ..
outyesThis 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.
targetsyesArray of declared target names this artifact builds for; ["*"] means every declared target.
linkyesArray of [link.X] names this artifact links. [] for none.
needyesArray of [step.X] names this artifact demands directly, for step outputs that are not themselves link inputs. [] for none.
subsystemno"console" (default) or "gui" - the environment a windows executable declares it runs under.
iconnoProject-root-relative .ico path embedded in a Windows executable's PE resources. bin artifacts only.
manifestnoProject-root-relative application-manifest path embedded byte-for-byte in a Windows executable's PE resources. bin artifacts only.

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.

KeyReqMeaning
optyesOptimization 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.
debugyesEmit 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.
simdyesSIMD 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.
vectorizenoAuto-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_reassocnoPermission 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.

Note

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.

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.

KeyReqMeaning
sourceyes"system" (a system library resolved by name), "framework" (a macOS framework), or "local" (a file on disk).
nameshapeLibrary / framework name - required for "system" and "framework", forbidden for "local".
pathshapeFile path - required for "local", forbidden otherwise. A template.
librarynoStable logical name used by #[library("...")]; defaults to the table name.
symbolsnoArray of symbol names this dependency provides, attributing imports that have no ext declaration to decorate.
os / isa / abiyesFilter axes: a canonical value, "*" (any), an array of values, or [] (none). An entry applies to a cell when all three match.
exportyestrue 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.

KeyReqMeaning
cmdyesOne command string, run through the platform shell (sh -c on posix, cmd.exe /C on windows) from the project root. Templates expand in it.
inyesDeclared input file list. Accepts globs (*, **), expanded sorted for a stable fingerprint; a glob that matches nothing is a hard error.
outyesDeclared output file list. Concrete paths only - a glob here is an error, since the demand match and cache key expand out verbatim.
needyesArray 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.

KeyMeaning
gitGit URL to clone into dep/<alias>/.
pathLocal 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.
refGit 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.

Restriction

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:

VariableExpands 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.

Restriction

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.

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 src maps to module paths
  • Decorators - #[library], which resolves against a [link.X] identity