CLI
The mach binary is the compiler driver. It dispatches on the first argument to a small set of commands - build, run, test, and more - that compile, execute, and manage a project rooted at a mach.toml.
Invocation
mach <command> [options]
The compiler dispatches on argv[1]. With no command, or an unknown one, it prints usage and exits non-zero. The project commands - build, run, test, and doc - take the project root as a required positional and walk up from it until a mach.toml is found. A bare invocation with no path is a user error.
Flags are matched exactly: --flag value (the value follows in the next argument) or a bare --flag toggle.
The combined --flag=value form and bundled short flags are not recognized. Each flag and its value are separate arguments.
Commands
| Command | Summary |
|---|---|
build | compile the project to objects and (for a kind = "bin" artifact) a linked binary |
run | build, then execute the produced binary |
test | build the test binary and run the collected tests |
clean | remove the project's build output |
dep | manage git-backed dependencies (clone, lock, vendor) |
init | scaffold a new project |
doc | generate Markdown reference docs from source doc-comments |
info | print compiler version, build host, and registered target capabilities |
help | print usage; mach help <command> for detail |
Global flags
Read by build, run, test, and doc, which share one config parser. Passing a verbosity flag and --quiet together is a parse error.
| Flag | Value | Effect |
|---|---|---|
-v | - | verbose readout: build phase timing, or per-test lines under mach test |
-vv | - | -v plus per-module / per-file (or passing-test) detail |
--quiet, -q | - | suppress non-error output |
--target <name> | target name | select a [target.<name>] entry |
--profile <name> | profile name | select a [profile.<name>] variant; absent, the first declared |
--bin <name> | artifact name | narrow the build to one bin-kind artifact |
--lib <name> | artifact name | narrow to one library artifact (mutually exclusive with --bin) |
-o <path> | path | override the linked-binary path; accepted only when the selection resolves to a single build cell |
--all-targets | - | build every declared target rather than the manifest default |
-g | - | emit debug info for this build, overriding the profile's debug key |
--pie | - | emit a position-independent (ET_DYN) executable |
--subsystem <kind> | console, gui | the environment a windows executable declares; overrides the artifact's subsystem key, inert off windows |
--emit-asm | - | emit per-module .s assembly text beside each object |
--emit-ir | - | emit per-module .ir SSA text beside each object |
--verify-ir | - | run the IR verifier after each optimisation pass |
IR and assembly emission is a CLI concern only - there is no profile key for it. There are no --no-emit-* inverses, no --release flag (use -O1 / -O2 or --profile), no --color flag, and no --verbose long form.
mach dep and mach init do not use the shared config parser; they read only their own flags.
build
mach build <path> [options]
Compiles the project rooted at <path> (for example mach build .). With no --bin or --lib, it builds every declared artifact for the default target and profile. Every reachable module is driven through sema, lower, optimise, and codegen to one relocatable object under the manifest's obj template. For a kind = "bin" artifact the objects are linked into the resolved out path; for a static artifact they are archived into an ar archive there, and with --emit obj the objects are the deliverable and nothing is linked.
| Flag | Value | Effect |
|---|---|---|
-O0 | - | force the debug pipeline, overriding the profile |
-O1, -O2 | - | select the release pipeline (both select the same one today) |
--emit <kind> | obj, exe | obj stops at the objects; exe (default) links a binary |
--jobs <n> | count | codegen worker threads (default: host CPUs; 1 serialises) |
--explain | - | print the resolved build plan and exit without building |
-L <dir> | dir | add a search directory for -l-resolved inputs; repeatable |
-l <name> | name | link a named object, archive, or shared library; repeatable |
Plus the global flags. An -O<n> flag overrides the selected profile's opt for this invocation; absent one, the profile decides. A bare .o / .a / .so / .dylib / .dll argument, or any /-bearing path, is linked verbatim.
Link inputs
ext fun declarations are forward references resolved at link time by external precompiled code - a loose .o object, a static .a archive, or a shared .so library. Inputs come from the command line and from the manifest's merged libs overlay; both sets are linked. An input that resolves to no existing file is a hard error, so a typo never silently drops a dependency.
- Explicit input path - a bare (non-flag) argument that contains a
/, ends in.oor.a, or names a.sois treated as an input path. The first non-flag positional afterbuildis the project root and is skipped; the rest are link inputs. A relative path is tried verbatim against the working directory first, then rooted at the project root. -l <name>- each-L <dir>is searched forlib<name>.o,<name>.o,lib<name>.a, then<name>.a; if none hit, the same candidates are tried against the working directory. Only if no static object or archive is found does resolution fall back to a sharedlib<name>.so(the-Ldirs, then the target OS's default and common system library directories).-L <dir>- adds a search directory for the resolution above. Both-Land-lmay be repeated.
How an input resolves decides static vs dynamic linking. A loose .o or static .a is a static input merged into the executable (an archive contributes every member object). A shared .so is a dynamic dependency: its DT_SONAME is recorded and undefined ext symbols are bound at load time through an emitted PLT. A static definition always wins over a dynamic import of the same name. -l prefers a static candidate, so the .so fallback applies only when none exists (the common case for system libraries like libc). Manifest libs resolve before CLI inputs, giving a deterministic link order.
Dynamic linking is implemented for the ELF (Linux) and PE (Windows) targets. The Mach-O (Darwin) import path is not yet implemented.
run
mach run <path> [options] [-- args...]
Executes the binary mach build already produced for <path> - a post-build convenience, not a rebuild. The build selection flags (--target, --profile, --bin / --lib) resolve which artifact to run; the build-only -O0 / -O1 / -O2 are accepted but have no effect, and --emit is rejected. Arguments after a -- separator are forwarded to the child as its argv; the child's exit code becomes this command's exit code.
mach run does not build. Run mach build first, or the binary it executes will be the one from your last build.
| Flag | Value | Effect |
|---|---|---|
--runner <cmd> | command | execute the binary as <cmd> <binary> <args...> instead of directly |
--runner names a host-side launcher for binaries the host cannot exec directly, for example mach run . --target windows --runner wine. <cmd> is a single command name or path (no shell-style word splitting); a bare name is resolved on PATH. Without the flag the binary is exec'd directly, and a launch failure reports exit 127 when execve rejects the binary, with no auto-detection.
test
mach test <path> [options]
Builds one dispatcher executable covering every test declaration (under the tests template) and runs each test as its own process (<exe> <idx>), reporting a per-module roll-up that expands failures. A crashing test reports its signal and the run continues. Only the current project's own tests run by default. A test build always links executables, even for a library target.
| Flag | Value | Effect |
|---|---|---|
--filter <pattern> | pattern | run only tests whose name contains <pattern> |
--include-deps | - | also run tests declared in dependency modules |
--list | - | list the collected tests and exit |
--format <kind> | human, json | human (default), or a json NDJSON event stream for tooling |
--jobs <n> | count | run up to n test processes at once, and size the build's codegen workers |
--runner <cmd> | command | launch every test as <cmd> <exe> <idx> instead of exec'ing it directly |
Plus the build and global flags. --runner has the same semantics as on run. Without it, a test the host cannot launch reports a per-test failure - FAIL(exit 127) when execve rejects the binary, FAIL(spawn) when the spawn itself fails.
clean
mach clean <path>
Removes the project's build output: the output directory trees declared by the manifest's out / obj / ir / asm templates. <path> is a project directory or a manifest file. It is idempotent and takes no options. Exit codes are its own: 0 ok, 1 no project or a bad manifest, 2 an IO failure.
dep
mach dep <action> [args]
Manages the project's dependency tree. A dependency has exactly one source form: a git URL plus a ref, acquired into the dep directory with plain git operations, or a path to another project tree, materialised as a relative symlink so the build resolves it by the same vendor layout.
| Action | Args | Effect |
|---|---|---|
pull | - | realise the manifest: clone missing git deps (transitively), link path deps, re-resolve a changed ref, repair drift, write mach.lock. Idempotent. |
update | <name> or --all | the only lock-advancer: re-resolve branch refs to current remote tips. Tag/commit refs are a no-op. Never edits the manifest. |
add | <name> --git <url> [--ref <ref>] or --path <dir> | append a [dep.<name>] stanza, then pull. |
remove | <name> [--purge] | drop the entry from mach.toml and mach.lock; --purge also deletes the vendored checkout. |
list | - | print each entry with its source form, ref, locked commit, and state (synced, missing, drifted, path). |
mach build never requires git or the network: a project whose dep tree is present builds on a bare machine. Only the network-shaped commands (pull, update, add) use git, discovered on PATH and invoked with an allowlisted environment. Git's absence is a clean error naming the operation that needed it. The same name required from two different sources or refs is a hard error naming both requirers; there is no version resolution.
Lockfile
The manifest is intent; mach.lock is the record of resolving it. After a pull, mach dep writes a TOML file recording each git dep's url, ref, and resolved commit (path deps have no lock entry).
# generated by `mach dep pull`; do not edit by hand.
version = 1
[dep.mach-std]
url = "https://github.com/briar-systems/mach-std"
ref = "branch/dev"
commit = "6b78ae1e8c3c9cc45e4ab4b916fd191d61e76aff"
pull honours the lock except where the manifest ref was edited, where it re-resolves loudly. A checked-out commit that differs from the lock is drift, repaired and reported, never silent. The writer is idempotent: an up-to-date lock is left untouched. Commit mach.lock to pin builds.
init
mach init [dir] [options]
Scaffolds a new project in [dir] (default: the current directory). Writes a complete mach.toml with a [project] block, [target.*] platforms for linux/windows/darwin, one artifact, debug and release profiles, a [dep.mach-std] dependency, a starter source file, and a cloned mach-std. Every collision is checked before any file is written, so a refused init leaves nothing behind.
| Flag | Value | Effect |
|---|---|---|
--name <name> | name | project id (default: the directory base name) |
--force | - | scaffold even when mach.toml, src/main.mach, or src/lib.mach already exists |
--lib | - | library layout: write src/lib.mach and scaffold a kind = "static" artifact instead of a kind = "bin" one |
The first non-flag argument is the target directory. A default binary scaffold links and runs from mach build . without further manifest edits.
doc, info, and help
mach doc <path> loads the module graph and generates Markdown reference docs from source doc-comments - one page per module plus an index. Each pub declaration is paired with the run of # comment lines immediately preceding it. --out <dir> sets the output directory (default doc/api); --target <name> selects a target for module discovery. The hand-written language material is never touched.
mach info prints an at-a-glance identity of the binary: its version, the host it was built for, and the registered capability surface. It needs no project. The output is line-oriented and stable for scripts.
mach
host: linux/x86_64
isa: x86_64 aarch64 riscv64 riscv32 spirv mos6502
os: linux darwin windows freestanding
abi: sysv64 win64 aapcs64 lp64 lp64f lp64d ilp32 ilp32f ilp32d spirv mos6502
object: elf coff macho raw spv
mach info targets prints the concrete (os, isa) tuples this binary can build, one per line. It is derived from the same joint capability declarations composition reads, so it never advertises a tuple that would fail to resolve.
The capability lines are read from the binary's target registries, so they report exactly what this build can target. mach info --version prints the version string alone on one line, for tooling.
mach help [command] prints the top-level usage summary, or - with a known command - that command's detail page. An unknown command prints usage and exits non-zero.
Exit codes
The commands share a stable convention for scripting:
| Code | Meaning |
|---|---|
0 | success (for test, all tests passed) |
1 | user error: missing project path, no mach.toml, unknown target, compile errors, an unresolvable link input (for test, any test failed) |
2 | internal error |
mach run instead returns the child's exit code on a successful build, falling back to 1 on a build/user error and 2 internally.
See also
- Manifest - the
mach.tomlreference for targets, profiles, and libs - Dependencies - git and path deps managed by
mach dep - Testing - the
testdeclaration behindmach test - Project layout - the directory shape
mach initscaffolds