cabin.lock Reference
Cabin uses a deterministic on-disk lockfile that records the package versions chosen by the
resolver. Artifact fetching reuses the same schema: the recorded checksum selects which
packaging revision of the locked version to materialize, and
verifies the fetched source archive in cabin fetch and cabin build.
The lockfile is generated - Cabin owns its content. Tracking it in version control is recommended so collaborators and CI converge on the same resolution and on the same archive bytes.
Workflow
# First resolve: writes cabin.lock next to the root manifest.
cabin resolve --manifest-path app/cabin.toml --index-path index
# Subsequent resolves prefer the locked versions even if newer ones
# exist (as long as constraints still allow them).
cabin resolve --manifest-path app/cabin.toml --index-path index
# Refresh the lockfile to the newest compatible versions.
cabin update --manifest-path app/cabin.toml --index-path index
# Refresh one package.
cabin update --package fmt --manifest-path app/cabin.toml --index-path index
# CI mode: require an up-to-date lockfile, never write.
cabin resolve --locked --manifest-path app/cabin.toml --index-path index
--locked and --frozen both forbid writing cabin.lock. The distinction is intent: --frozen
additionally forbids any other state-writing side effect. It must not populate the artifact cache.
Already-cached, already-extracted artifacts may be reused.
File location
cabin.lock lives next to the root manifest:
cabin resolve --manifest-path app/cabin.tomlwritesapp/cabin.lock.- For workspace roots the lockfile sits beside the workspace root manifest, not inside any member directory.
- The lockfile is not placed inside
--build-dir.
Format
# This file is automatically generated by Cabin.
# Do not edit it manually.
version = 1
[[package]]
name = "fmt"
version = "10.2.1"
source = "index"
checksum = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
[[package]]
name = "spdlog"
version = "1.13.0"
source = "index"
checksum = "sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
dependencies = ["fmt"]
| Field | Where | Required | Description |
|---|---|---|---|
version | top level | yes | Schema version. Cabin supports only 1. |
[[package]] | repeated | yes per package | One entry per resolved registry package. Sorted alphabetically by name. |
name | package | yes | Package name. |
version | package | yes | Resolved SemVer version. |
source | package | yes | Where the package came from. Cabin supports "index" plus local provenance records for patches and vendoring. |
checksum | package | no | sha256:<hex> digest of the source archive’s bytes, and thereby the packaging-revision pin - the revision id is this digest’s leading 16 hex characters. In --locked mode the resolver fails on lockfile-vs-index disagreement; in cabin fetch / cabin build the same value selects the revision to fetch and verifies the archive’s content as it is copied into the cache. |
dependencies | package | no (only emitted when non-empty) | The package’s transitive dependency names, sorted alphabetically. |
Local path packages are deliberately not recorded in cabin.lock; they are resolved
structurally on every load by cabin-workspace.
[target.'cfg(...)'.<kind>] predicates are evaluated before resolution against the host
platform, so the lockfile records the host-evaluated closure exactly as if the non-matching entries
had never been declared. The target field is therefore not stored on locked packages - re-running
on a host whose predicates re-evaluate to a different set updates the lockfile via the ordinary
cabin update path. See target-dependencies.md.
The format is strict:
- the parser uses
deny_unknown_fields; a typo or unsupported key is a clear error; version = 1is the only accepted schema version;- duplicate
[[package]]entries (same name) are rejected; - invalid SemVer in any
versionfield is rejected; - unknown
sourcevalues are rejected.
The writer guarantees:
- a stable header comment;
- alphabetical ordering of
[[package]]entries by name (then by version); - alphabetical ordering of each entry’s
dependencieslist; - byte-identical output for the same in-memory
Lockfile; - atomic replacement of an existing
cabin.lock: the new bytes land in a sibling temporary file and only rename onto the destination after a successful write, so an interrupted run leaves the previous lockfile intact.
[[patch]] and [[source-replacement]] arrays
When the active local-policy layer (manifest [patch] table plus config patches and source
replacements) is non-empty, the lockfile carries deterministic top-level arrays that record exactly
the policy applied:
[[patch]]
package = "fmt"
version = "10.2.1"
kind = "path"
provenance = "manifest"
path = "../fmt"
[[source-replacement]]
original = "https://example.com/index"
original-kind = "index-url"
replacement = "../mirror"
replacement-kind = "index-path"
provenance = "user-config"
Both arrays are optional: old lockfiles that omit them parse unchanged, and Cabin only writes them
when at least one entry is active. Under --locked, if the recorded arrays differ from the active
policy, resolution surfaces a stale-lockfile error (--locked cannot be used because active patch / source-replacement policy differs from <lockfile>). --no-patches empties both arrays for the
current invocation.
The patch-state staleness check is independent of whether the resolver itself runs. Even when the
active patches cover every versioned dependency (so the resolver has nothing to do and prints (no versioned dependencies)), --locked still compares the recorded arrays against the active policy:
adding or removing a patch since the lockfile was written makes the lockfile stale, and --locked
will refuse rather than silently succeed. Comparison uses the same canonical (sorted) arrays the
renderer emits, so it is deterministic.
Full protocol in patch-overrides.md.
Packaging revisions
A registry version can be published more than once - a corrected recipe for the same upstream
release lands as a new packaging revision of it, never as
replaced bytes. There is no separate lockfile field for this: checksum is the pin, because a
revision id is its digest’s first 16 hex characters.
- Plain
cabin resolvekeeps a recorded checksum as long as the index still lists that revision. Someone else’s respin therefore never churns your lockfile: re-resolving after one rewrites nothing. cabin update(andcabin update --package <name>) is the deliberate move to the version’s current revision, exactly as it is the deliberate move to a newer version.- A first resolution, or a package whose pinned revision the index no longer lists, locks the version’s current revision.
--locked,--frozen,--offline, and a vendored index all reproduce the pinned revision exactly, including a revision that has since been superseded: published revisions stay fetchable. A pin no revision in the index carries is an error naming both the pinned and the current checksum - never a silent substitution of different bytes.- In
--locked/--frozenmode a locked entry with nochecksumis a hard error when the index entry does carry revisions - it pins no revision, so it cannot be reproduced.
Version pins only
Beyond the version choices, the revision pins above, and the local override policy, cabin.lock
records nothing else. No language standards, no toolchain information, and no build fingerprints
are written. Resolution inputs are manifest-declared values only, so one lockfile stays shareable
across platforms and toolchains.
The post-resolution standard-compatibility check (see
language-standards.md) validates manifest-declared standards over the
resolved graph on every command that consumes it from the lockfile - cabin build / check /
run / test - exactly as on fresh resolution. The check reads manifests only: it probes no
toolchain, reads nothing from the environment, and never modifies cabin.lock. Because the
lockfile pins versions and not standards, a violation against a dependency whose resolved version
came out of the lockfile usually means a standard declaration changed in a manifest after the
lockfile was generated; the error says so and suggests cabin update to re-resolve, alongside
the usual remedies. A selection the resolver re-resolved past a stale pin carries no such note.
Commands
cabin resolve
Default mode. Reads cabin.lock if it exists. Resolves with the locked versions as preferences
(newest compatible if the locked version no longer satisfies). Writes the new lockfile if it
differs from the existing one.
cabin resolve --locked
Reads cabin.lock and requires it to be current:
- fails if the lockfile does not exist;
- restricts each candidate set during resolution to the locked version, so any deviation surfaces a precise error (missing package, missing version, yanked, constraint violation, checksum mismatch);
- never writes the lockfile.
Use this in CI to verify that contributors didn’t forget to commit their lockfile updates.
cabin resolve --frozen
Same lockfile-strict semantics as --locked plus a stricter promise: no generated state should be
written. For cabin fetch and cabin build, this also means the artifact cache must not be
populated - already-cached and already-extracted artifacts may still be reused, but a cache miss in
--frozen is a clear error rather than an excuse to copy.
cabin update
Re-resolves everything, ignoring the locked map. Writes the new lockfile.
cabin update --package <name>
Drops one entry from the locked map and re-resolves with the rest still preferred. Useful for refreshing one dependency without touching the rest of the graph.
--package validates that <name> is a versioned dependency of the root package - otherwise it
errors.
Yanked versions
The resolver always excludes yanked candidates in PreferLocked / UpdateAll / UpdatePackage.
In Locked mode (--locked / --frozen), if the locked version is yanked, the resolver fails with
a clear error so the user is forced to run cabin update.
Pre-release versions
Pre-release versions (1.0.0-alpha, 2.0.0-rc.1, …) are excluded from candidate selection by
default, mirroring semver::VersionReq::matches. A pre-release version is admitted only when one
of the comparators making up the requirement names the same major.minor.patch with a non-empty pre
tag - for example fmt = "=1.0.0-alpha" (singleton opt-in) or fmt = ">=1.0.0-alpha, <1.0.0"
(range that explicitly opens the 1.0.0 pre-release window). Wide constraints such as fmt = ">=1.0.0, <2.0.0" never pick a pre-release even if one is the only candidate published in the
index. A locked-in pre-release survives a follow-up cabin resolve so existing lockfile pins do
not silently churn.
Resolver diagnostics
Dependency resolution failures are rendered through Cabin’s miette-based diagnostics layer. Every
variant of ResolveError carries the stable diagnostic code cabin::resolver::error along with
per-variant help text. Locked-mode errors remain specific so users can tell whether to update the
lockfile, fix constraints, or investigate a checksum mismatch; conflict failures embed a
human-readable explanation derived from PubGrub’s reporter output.
Limitations
The following are not part of the current lockfile contract:
- network registry access (HTTP / sparse index client)
- registry authentication
- a registry server
- package publishing (
cabin package,cabin publish) - OCI / GHCR or other remote-archive transports
- a binary artifact cache or remote build cache
- alternative
sourcevalues incabin.locksuch aspathorgit - locking of local path packages
- workspace-level
cabin.lockfor workspaces without a root[package] - a separate
cabin generate-lockfilecommand (cabin resolveandcabin fetchare the producers) - a public JSON Schema document for
cabin.lock