Build profiles
A profile is a named preset of compile-time settings — debug information, optimization level, assertions — that Cabin applies to a build. Profiles formalize the long-standing distinction between "debug" and "release" builds and let projects declare their own presets without having to drop into a raw compiler-flag system.
This document is the canonical specification. The behavior
described here is what the manifest parser (cabin-manifest),
the typed model and resolver (cabin-core::profile), the build
planner (cabin-build), the CLI (cabin), the canonical
package metadata (cabin-package), and the local / sparse-HTTP
index loaders (cabin-index, cabin-index-http) all agree on.
Built-in profiles
Cabin always provides two profiles, even when the manifest has no
[profile.*] tables:
| Profile | debug |
opt-level |
assertions |
C compile flags | C++ compile flags |
|---|---|---|---|---|---|
dev |
true |
0 |
true |
-std=c11 -O0 -g |
-std=c++17 -O0 -g |
release |
false |
3 |
false |
-std=c11 -O3 -DNDEBUG |
-std=c++17 -O3 -DNDEBUG |
dev is the default. It is also the profile a bare
cabin build and a bare cabin metadata invocation produce.
CLI selection
cabin build and cabin metadata accept --profile <name>:
cabin build --profile dev
cabin build --profile release
cabin build --profile relwithdebinfo
cabin build also keeps the long-standing --release flag as a
compatibility alias for --profile release. Passing both
flags together is rejected:
$ cabin build --release --profile release
error: the argument '--release' cannot be used with '--profile <NAME>'
cabin resolve, cabin update, cabin fetch, cabin package,
and cabin publish deliberately do not accept a profile flag.
Profiles are local build configuration; they have no effect on
dependency resolution, the lockfile, or the on-disk archive.
A .cabin/config.toml file may also pin a default profile via
[build] profile = "<name>". The CLI flag still wins when
present; see config.md for the full discovery and
precedence ladder.
Manifest syntax
Manifests may declare custom profiles or override built-in
defaults under top-level [profile.<name>] tables:
[profile.dev]
opt-level = 1 # override built-in: a faster dev cycle
[profile.release]
debug = true # override built-in: keep debug info on
[profile.relwithdebinfo] # custom: must declare `inherits`
inherits = "release"
debug = true
Supported fields
| Field | Type | Notes |
|---|---|---|
inherits |
string (profile name) | Required on custom profiles; rejected on dev / release. |
debug |
true / false |
Whether -g is added to C and C++ compile commands. |
opt-level |
0 / 1 / 2 / 3 / "s" / "z" |
Maps directly onto -O0 … -O3 / -Os / -Oz. |
assertions |
true / false |
When false, -DNDEBUG is added to C and C++ compile commands. |
The schema is closed: any other key is rejected with a clear
error. Specifically, capability-style fields such as
link-libs, compiler, toolchain, target, cfg, env,
rustflags, linker, ar, stdlib, and sanitizer are
not accepted here — toolchain selection lives under
[toolchain], and capability probing is out of scope.
The array flag fields cflags, cxxflags, ldflags,
defines, and include-dirs are written directly on the
[profile.<name>] table. See
Inheritance and array flags below for the merge semantics
across an inherits chain.
Inheritance
- Built-in profiles (
dev,release) have implicit defaults and are always selectable, even without a manifest entry. - A
[profile.dev]or[profile.release]entry overrides the matching built-in's defaults field-by-field. Unspecified fields keep their defaults. - Custom profiles must declare
inherits, which must point to a built-in or another custom profile. - Inheritance is acyclic; cycles are rejected with a clear error.
- Final field values are resolved root-first: each layer in the chain overrides anything an ancestor set, and missing fields keep their inherited value.
Inheritance and array flags
A [profile.<name>] table can contribute array flag
fields: cflags, cxxflags, ldflags, defines,
and include-dirs. These compose differently from the scalar
fields above:
- Scalars replace across the inherits chain
(
opt-level,debug,assertions). The leaf wins; an unset leaf field keeps its inherited value. - Array flag fields append, root-first, across the inherits chain. Each ancestor's values come first, in the order the user wrote them; the selected profile's values come last.
The full effective order of array-flag layers, top to bottom in the resulting argv, is:
- The package's top-level
[profile]block. - Each matching
[target.'cfg(...)'.profile]block, in manifest order. - The profile inherits chain, root → selected — each step's
[profile.<name>]flags appended after the previous step's.
So with
[profile]
cxxflags = ["-Wall"]
[profile.release]
cxxflags = ["-O3"]
[profile.profiling]
inherits = "release"
cxxflags = ["-pg"]
selecting profiling resolves to
cxxflags = ["-Wall", "-O3", "-pg"]
This is Cabin adopting cargo-config-style array layering for
its profile flag arrays. Cargo's own profile tables do not
expose user-facing array fields; the closest analog is the
rustflags layering inside .cargo/config.toml. Cabin
profile flag arrays append across ancestors so a leaf profile
can extend its parent without re-stating every flag.
Practical caveat. Because arrays append, parent and leaf
flags coexist on every compile / link command. Mutually
exclusive compiler or codegen flags placed in shared parent
profiles will conflict with leaf overrides — -O0 vs -O3,
-fno-rtti vs -frtti, -flto vs -fno-lto, incompatible
-std= / /std: flags. Cabin does not arbitrate; the
compiler's own last-wins or conflict behavior decides.
Reserve shared parent profiles for non-conflicting policy
flags (warnings, sanitizer-friendly debug-info knobs); keep
leaf-specific optimization / codegen choices in the leaf
profile itself.
Workspace scope
Only the workspace root manifest's [profile.*] tables apply.
Member or path-dep manifests that declare profile tables are
rejected with the error
profile tables may only appear in the workspace root manifest,
so a single workspace key cannot mean different things in
different members.
Build directories
Build outputs are profile-aware:
<build-dir>/<profile>/build.ninja
<build-dir>/<profile>/compile_commands.json
<build-dir>/<profile>/packages/<package>/<target>/...
Two effects:
devandreleasebuilds never overwrite each other.- A custom profile gets its own deterministic output tree.
Profile names are validated up front (ASCII alphanumerics, _,
-, .; non-empty; not . / ..; not starting with .) so a
malformed name is rejected at parse time instead of slipping into
filesystem layout.
Build configuration fingerprint
BuildConfiguration::fingerprint is a SHA-256 of every input
that affects build output: enabled features and the resolved
profile (its name, debug, opt-level, assertions). Switching
profiles changes
the fingerprint by design — a future cache layer would key on
the same value.
cabin metadata
cabin metadata reports a top-level profiles block:
{
"profiles": {
"selected": {
"name": "relwithdebinfo",
"debug": true,
"opt_level": "3",
"assertions": false,
"source": "custom",
"inherits_chain": ["release", "relwithdebinfo"]
},
"available": ["dev", "release", "relwithdebinfo"],
"definitions": {
"relwithdebinfo": { "inherits": "release", "debug": true }
}
}
}
The available array is sorted alphabetically; definitions
keys iterate alphabetically; the selected.inherits_chain is
deterministic (root first). Pass --profile <name> to compute
the metadata view as if that profile were selected — useful for
CI that wants to dump every profile's resolved fields without
re-reading the manifest.
What profiles do not do
- They do not affect dependency resolution.
cabin resolveand the lockfile are profile-independent. - They do not enable or disable optional dependencies or
gate features. Those remain orthogonal axes (see
features.md). - They do not introduce target-specific profile tables
(
[target.'cfg(...)'.profile.*]) or profile-specific dep tables ([profile.<name>.dependencies]).
Limitations
- Cross-compilation is out of scope, so the build planner evaluates profiles against the host toolchain.
- Toolchain selection and capability probing are explicitly out of scope for profile tables.
- Profile names cannot escape the build root; invalid names are rejected at parse time rather than sanitized silently.