Target / platform-specific dependencies
Cabin supports declaring a dependency only for a particular host
platform via [target.'cfg(...)'.<kind>] tables in cabin.toml.
The shape mirrors Cargo's syntax so the manifest stays familiar,
but the supported predicates and evaluation context are pinned
down to a small, deterministic set.
This document is the canonical specification. The behavior
described here is what the manifest parser, the workspace loader,
the resolver, the feature resolver, the fetch / build pipeline,
the canonical package metadata, the local and sparse-HTTP index
loaders, and the cabin metadata JSON view all agree on.
Manifest syntax
A target-conditional dependency table is a TOML table whose key is
the literal string target followed by a TOML key that is a
quoted cfg(...) expression. The dependency-kind sub-tables
inside are the same dependency kinds:
[target.'cfg(os = "linux")'.dependencies]
fmt = ">=10"
[target.'cfg(arch = "x86_64")'.dev-dependencies]
gtest = "^1.14"
[target.'cfg(env = "musl")'.dependencies]
zlib = { version = ">=1.2", system = true }
Each dependency entry inside a conditional table accepts the same
fields as the top-level kind table (per-edge features,
optional, default-features for Cabin package deps; system =
true with version for system deps). Every declared system
dependency is required.
The same [target.'cfg(...)'] machinery also applies to the
[toolchain] and [profile] tables introduced for explicit
toolchain selection and conditional build flags:
[target.'cfg(os = "linux")'.toolchain]
ar = "llvm-ar-18"
[target.'cfg(os = "linux")'.profile]
defines = ["USE_EPOLL"]
Conditional [toolchain] tables follow the same workspace-root-
only rule as the unconditional [toolchain] table. Conditional
[profile] tables are per-package, like the rest of [profile].
Full protocol in toolchains.md.
workspace = true is not allowed inside a conditional table.
Workspace inheritance only flows through the unconditional
[workspace.<kind>-dependencies] tables; mixing the two would
allow a single workspace key to silently mean different things
on different hosts. Use a workspace dep without a condition, or
declare the dep directly inside the conditional table.
Supported cfg grammar
A predicate is one of:
<key> = "<value>"— a key/value test;all(<expr>, <expr>, …)— every nested predicate must hold (zero arguments is allowed and is always true);any(<expr>, <expr>, …)— at least one nested predicate must hold (zero arguments is allowed and is always false);not(<expr>)— exactly one nested predicate, negated.
Keys are bare identifiers; values are double-quoted strings.
Unknown keys, unquoted values, missing parentheses, and wrong
arity in not(...) are rejected at parse time with a clear
error that names the offending input.
The supported keys are fixed:
| Key | Source | Examples |
|---|---|---|
os |
std::env::consts::OS |
"linux", "macos", "windows" |
arch |
std::env::consts::ARCH |
"x86_64", "aarch64" |
family |
std::env::consts::FAMILY |
"unix", "windows" |
env |
derived from the host triple ("" when the triple has no env) |
"gnu", "musl", "msvc" |
abi |
derived from the host triple ("" when the triple has no abi) |
"eabihf", "sim" |
target |
the full host triple | "aarch64-apple-darwin" |
Only those six keys are accepted. Adding more requires a spec-level decision because it widens the public manifest grammar and the canonical metadata schema.
Evaluation context
Cabin evaluates predicates against the host platform, derived
once via cabin_core::TargetPlatform::current(). There is no
cross-compilation in this step, so the evaluation context is
deterministic for a given machine and is reported back to the
user under target_platform in cabin metadata so dependency
filtering is auditable.
The evaluation rules are:
key = "value"matches when the host's value forkeyequalsvalueexactly (string equality, case-sensitive).all/anyshort-circuit and recurse.notinverts.- Unknown keys never reach evaluation — the parser rejects them.
A dependency whose predicate fails on the host is filtered out before it reaches:
- the workspace closure walker
(
cabin_workspace::collect_closure_versioned_deps_filtered); - the resolver input (no constraint reaches the solver);
- the feature resolver (
cabin_feature::resolve_featuresskips it when computingdep:entries); - the artifact fetch path;
- the build planner;
- the canonical metadata document and the
cabin metadataJSON view's "active" flag.
The condition is preserved on Dependency::condition and
DependencyEdge::condition so the JSON view can still surface it
as target plus an active: false marker; the dependency
itself does not participate in resolution or build.
Round-trip through publishing
cabin package and cabin publish carry the predicate through
the canonical PackageMetadata document
under a target field on each dependency table entry, and the
local-file plus sparse-HTTP index loaders parse the same field
back into IndexPackageDependency::condition /
IndexSystemDependency::condition. Older metadata that omits
target continues to load — the field is optional in every
schema.
The on-disk encoding of a Condition is its canonical
inner-expression form (os = "linux",
all(os = "linux", arch = "x86_64"), …). The wrapping
cfg(...) is implicit because the field name already carries
that meaning in the schema.
Lockfile
The lockfile records the resolved closure for the host platform,
exactly as before. Because filtering happens before resolution,
the lockfile always describes a valid, host-evaluated set; it is
not partitioned per-platform. If the host platform changes, the
closure may change and the lockfile is updated by re-running
cabin update or by removing and regenerating it. Cross-platform
lockfile partitioning is deliberately out of scope for this step.
CLI behavior
cabin metadata reports two new pieces of information:
- a top-level
target_platformblock listing the values used to evaluate predicates (os,arch,family,env,abi,target); - per-dependency
target(when present) andactiveflags so consumers can decide whether to surface or filter inactive declarations without re-evaluating the predicate.
cabin resolve, cabin fetch, cabin build, and
cabin update apply the host-platform filter implicitly. They do
not gain new flags — cross-compilation lives outside this step.
Errors
Errors are rendered without referring to internal step numbers. Examples (the precise wording lives in the manifest / index error types):
- "invalid
cfg(...)expression in[target.'…'.dependencies]: unknown keyhost_endian" - "
cfg(...)expressions do not acceptworkspace = true— declare the dependency in[workspace.dependencies]and usedep = { workspace = true }outside anytarget.cfgtable" - "expected double-quoted string after
os ="
The CLI surfaces them through the same channel as ordinary
manifest errors: cabin metadata, cabin resolve, and
cabin build all stop early and report the offending line.