machdocs
Home Ecosystem Changelog

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
mach    = "^6.0"                       # required in a root manifest: the compiler range

[target.linux]                        # a platform: a fully-spelled tuple
default = true
isa = "x86_64"
os  = "linux"
abi = "sysv64"

[profile.debug]                       # a build variant: requires all five keys
default       = true
opt           = 0                     # 0 (debug pipeline) | 1 | 2 (release pipeline)
debug         = true                  # emit debug info for this profile
simd          = "scalarize"          # SIMD lever: "scalarize" | "require"
vectorize     = true                  # auto-vectorization lever
float_reassoc = false                 # associative float transforms

[artifact.demo]                       # a produced artifact
default = true
kind    = "bin"                       # "bin" | "static" | "shared"
entry   = "root.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    = []                          # category-qualified names demanded directly

[dep.std]                             # a dependency: directory, manifest key, and id match
git     = "https://github.com/briar-systems/mach-std"
version = "^9.0"                    # a release range, or ref = "tag/..." for one exact selection

[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 and $project.version.{major,minor,patch}, and stamped into a Windows executable's version resource. A release tag vX.Y.Z is a release only when this matches it.
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.
machstringThe compiler versions this project builds with, as a version range such as "^6.0". Required in a root manifest. See Compiler range.
Restriction

[project] permits only id, version, src, out, and mach. Any other key, name and description included, is an unknown-key error.

The output directory

Everything a build and its cache write lands under the expanded out, in one layout:

PathHolds
obj/one object per module, each carrying its cache key; it is the object cache, and is read by developers and tooling too
ir/, asm/the human-readable views --emit-ir and --emit-asm write
.cache/compiler-only state, read and written by nothing but the compiler
.cache/steps/one fingerprint stamp per build step
.stage/build step scratch space, one directory per step, reset before the step runs
test/<artifact>/dispatch.othe test dispatcher object of a tested artifact
test/<artifact>/<artifact>the test dispatcher executable
test/<artifact>/log/a failing test's captured output
dep/<id>/a dependency's artifact outputs

Test objects sit in obj/ beside the module objects, as obj/<project>/<module>.test.o. Artifact outputs go wherever their own out names under the directory.

mach build and mach test read obj/ as the object cache by default. Each module has its own key, so an edit rebuilds only that module and the modules whose view of it changed, and a warm build links the same binary a cold one does. An object that is missing or carries another key is rebuilt, never linked stale. --no-cache forces an uncached build.

mach clean removes obj/, ir/, asm/, .cache/, .stage/, test/, and dep/ along with every artifact output, for every declared target and profile, so the build after it is a cold one that reuses nothing. A step output may not name a path inside obj/<project.id>/, .cache/, or .stage/.

Compiler range

mach states which compilers a project builds with. Every command that reads the dependency closure - build, test, check, mach dep verify, and the language server - checks it for the root and for every realized dependency. A compiler outside any of those ranges is refused once, with every unmet requirement and the chain that states it:

error: this is mach 5.2.1, and the dependency closure does not accept it:
    app (mach.toml) requires mach ^5.3
    app -> gfx -> glfw requires mach >=5.4, <6

A root manifest must state mach. One without it is refused, with the line to add. A dependency without it states no constraint. mach init writes the oldest release of the running compiler's major that reads the key: mach = "^6.0" for every 6.x compiler. Dependency resolution also reads it: a release whose mach range excludes the running compiler is never selected.

The compiler's version is the last release it was built from. A build from an unreleased tree reports that release, so a project cannot require an unreleased feature by version.

Targets

Each <name> in [target.<name>] is a selector you pass to -t <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
oslinux, windows, darwin, freestanding
abisysv64, win64, aapcs64, lp64, lp64f, lp64d, ilp32, ilp32f, ilp32d, spirv
ofelf, coff, macho, raw, spv

A value outside its axis 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. riscv64 defaults to rv64gc, while riscv32 defaults to rv32imac and ilp32 (specify rv32imafdc when RV32 hardware float is wanted).

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. $bin.name reads the selected artifact name.

KeyReqMeaning
kindyes"bin", "static", or "shared".
entryyesEntry source, relative to the project src dir. The entry module FQN is <id>.<entry without .mach>, with / turned into ..
outyesThis artifact output path, relative to the expanded project out and rooted there automatically, such as bin/demo or bin/demo{artifact.suffix}.
targetsyesArray of declared target names this artifact builds for, where ["*"] means every declared target.
linkyesArray of [link.X] names this artifact links. [] for none.
needyesArray of category-qualified requirements such as step.generate, artifact.support, and artifact.shader-*. Each glob matches only its named category. [] for none.
subsystemno"console" (default) or "gui", the environment a Windows executable declares it runs under. Refused on targets whose image format has no subsystem.
iconnoProject-root-relative .ico path embedded in a Windows executable PE resources. bin artifacts only.
manifestnoProject-root-relative application-manifest path embedded byte-for-byte in a Windows executable PE resources. bin artifacts only.
defaultnotrue marks the artifact chosen when a command needs one artifact (such as mach test or mach run) and several declared artifacts support the selected target. Exactly one candidate may carry it.

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     = "root.mach"
out       = "bin/game.exe"
targets   = ["*"]
link      = []
need      = []
subsystem = "gui"
icon      = "assets/game.ico"
manifest  = "assets/game.manifest"

Only a PE image carries the subsystem field. A key written on an artifact planned for a target whose format has none (ELF, Mach-O, raw) is refused as unsupported, naming the key, the target, and the format. An omitted key is the console default and is never refused, so an artifact declaring no subsystem still builds everywhere. Resource keys like icon and manifest remain valid in a multi-target artifact, but are inert off Windows where Mach does not read either path.

Profiles

A [profile.<name>] is a build variant. The optimization level, debug toggle, and SIMD levers 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, including in .debug_* sections of COFF objects and PE images). 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.
vectorizeyesAuto-vectorization lever. When true, the release pipeline rewrites provably safe counted loops to 128-bit SIMD on a target with hardware vectors. false skips the pass, changing performance but never semantics.
float_reassocyesPermission to treat floating-point addition and multiplication as associative. 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 computed answer.
defaultnotrue marks the profile a build uses when several are declared and -p is omitted. Exactly one profile may carry it.

All five keys (opt, debug, simd, vectorize, float_reassoc) are required in a declared root profile. Emission of the human-readable IR and assembly side-artifacts is not a profile concern. It is controlled only by the --emit-ir and --emit-asm CLI flags.

Note

The simd, vectorize, and float_reassoc levers are always the consumer's. A dependency [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 = [...], and 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, while ELF .so, Mach-O .dylib, and PE .dll inputs are recorded using their format 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
argvyesNonempty array of strings, spawned directly with no shell. argv[0] names the executable, by path or resolved on the planner PATH. Templates expand in every element. To invoke a shell, write ["sh", "-c", "..."].
envnoTable of string values added to the step process environment.
inyesDeclared input file list. Accepts globs (*, **), expanded sorted for a stable fingerprint. A glob that matches nothing is an error.
outyesDeclared output file list. Concrete paths only: a glob here is an error, since demand matching and caching expand out verbatim.
needyesArray of step.<name> requirements or step.<pattern> globs this step must run after. Steps may require only steps. Cycles error. [] for none.
timeoutnoDuration string ("30ms", "30s", "5m", "1h") after which the step process group is terminated and the build fails. Omit for an unbounded step.

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 declared inputs, resolved executable, expanded arguments, and effective environment contribute to its fingerprint. An unchanged step whose outputs still exist is skipped. The fingerprint of the last successful run is kept as a stamp in {project.out}/.cache/steps/, and a step's outputs under {project.out} are written into scratch space in {project.out}/.stage/<name>/ and published only once the step succeeds. A declared out inside .cache/ or .stage/ fails at manifest load. Every step process inherits the active build cell target tuple as MACH_TARGET_ISA, MACH_TARGET_OS, and MACH_TARGET_ABI.

Dependencies

Each [dep.<id>] names a dependency realized under dep/<id>/. The key, the directory, and the dependency's own [project].id are one name. 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 id resolves into that dep tree. A stanza declares exactly one source, git or path, and a git stanza exactly one selector, version or ref.

KeyMeaning
gitGit URL. The dependency is a git submodule at dep/<id>/, pinned by the root repository's gitlink. Requires version or ref.
versionA version range over the dependency's vX.Y.Z release tags, such as "^5.3.0". Valid only with git. Resolved by mach dep add and update, never by a build.
refExact selector (with git): branch/<name>, tag/<name>, or commit/<full-object-id>. Any other spelling is rejected. Cannot be combined with version.
pathLocal project tree, resolved relative to this manifest and never fetched. mach dep copies its files into dep/<id>/. Forbids version and ref.

Pins are the gitlinks committed in the root repository, and there is no lock file. mach dep pull <path> realizes the whole transitive closure flat at dep/<id>/, one level deep. Builds never fetch or resolve. They check offline that every checkout is clean at its pin, that each version-selected pin is a release inside every requirer's range, and that every mach range in the closure accepts the compiler.

Note

The range grammar, how resolution picks releases, root narrowing and overriding, and everything a build verifies 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 directory under dep/ and the id match what source code writes.

Path templates

Paths and argv arguments expand over a closed, final set of eight variables:

VariableExpands to
{project.out}the root project expanded [project].out, in every manifest of the closure
{target.name}the resolved target name (never the literal native)
{target.isa}the resolved target isa, e.g. x86_64
{target.os}the resolved target os, e.g. linux
{target.abi}the resolved target abi, e.g. sysv64
{profile.name}the selected profile name
{artifact.suffix}the conventional filename suffix of the artifact being named, for its kind on the selected target (such as .exe, .a, .so). Available only in an artifact's own out.
{artifact.<id>.out}the output path of a required artifact, relative to the project root exactly as {project.out} is

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, and {artifact.<id>.out} is not available inside an artifact's own out.

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. With no match native is an error, raised before any step runs: a declared target that does not match the host is built only when -t names it, so a cross-only project with hosted targets selects its target with -t.

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 argv 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"
mach    = "^6.0"
src     = "src"
out     = "out/{target.name}/{profile.name}"

[target.linux]
isa     = "x86_64"
os      = "linux"
abi     = "sysv64"
default = true

[target.windows]
isa = "x86_64"
os  = "windows"
abi = "win64"

[profile.debug]
opt           = 0
debug         = true
simd          = "scalarize"
vectorize     = false
float_reassoc = false
default       = true

[profile.release]
opt           = 2
debug         = false
simd          = "scalarize"
vectorize     = true
float_reassoc = false

[step.miniz]                            # builds the vendored c object the link entry below demands
argv = ["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   = "root.mach"
out     = "bin/demo{artifact.suffix}"
targets = ["*"]
link    = ["miniz", "kernel32"]
need    = []

[dep.std]
git     = "https://github.com/briar-systems/mach-std"
version = "^9.0"

See also

  • CLI - the flags that select a target, profile, and artifact
  • Dependencies - version ranges, release resolution, mach dep, and gitlink pins
  • Project layout - how src maps to module paths
  • Decorators - #[library], which resolves against a [link.X] identity