RFC 020: Offline / Locked / Reproducible Builds (Cargo Policy + Generated Project Contract)¶
Status: Planned
Created: 2026-01-21
Author(s): Danny Meijer (@danny-meijer)
Issue: https://github.com/dannys-code-corner/incan/issues/38
Related: RFC 013 (dependency + lockfile direction), RFC 015 (project lifecycle CLI), RFC 019 (test runner + CLI)
Summary¶
Define a first-class, user-facing Cargo policy contract for Incan that supports enterprise/restricted environments:
- Cargo policy flags on
incan build,incan run, andincan test:--offline(no network)--locked(must use an existing lockfile)- (optional)
--frozen(implies offline + locked; mirrors Cargo)
- A precedence model for policy (CLI flags + CI-friendly env vars; project config is explicitly out of scope here).
- A generated-project persistence contract for
target/incan/**andtarget/incan_tests/**: what is regenerated vs preserved, and where artifacts live.
This RFC intentionally avoids overlap with:
- RFC 013 (dependency specification +
incan.lock): this RFC does not defineincan.lockorincan lock/update. - RFC 015 (project metadata + lifecycle): this RFC does not define
incan.tomlschema or project layout/init/new. - RFC 019 (test runner + CLI): this RFC does not define test discovery/selection/reporting flags; it only adds Cargo
policy flags that constrain the underlying Cargo subprocess invocations used by
incan test.
Motivation¶
In enterprise or restricted environments, adopting a compiler/toolchain depends on predictable, enforceable policy:
- no unexpected network access during CI or local builds
- deterministic dependency resolution (lockfile enforcement)
- a stable answer to “where are outputs” and “what does the tool overwrite”
Today, Incan uses Cargo under the hood and may trigger network activity (e.g. “Updating crates.io index”) during:
incan buildandincan run(viacargo build/runon a generated project undertarget/incan/<name>)incan test(viacargo teston a generated harness project undertarget/incan_tests/...)
Users can work around this by cd-ing into generated Cargo projects and running cargo --offline --locked ...
themselves, but that undermines the promise of a coherent toolchain and makes CI policy inconsistent.
Goals¶
- Provide an official way to run builds/tests with Cargo policies (
--offline,--locked, optionally--frozen). - Make CI reproducibility easy via explicit, composable flags and CI-friendly env vars.
- Specify the generated-project contract (default locations; what is overwritten vs preserved; where artifacts land).
- Keep this change non-breaking by default (policy is opt-in).
Non-Goals (this RFC)¶
- Replacing Cargo’s resolver or implementing a new dependency solver.
- Designing the
incan.tomlschema for projects (RFC 015). - Defining
incan.lockformat,incan lock, orincan updateworkflows (RFC 013). - Providing a full vendoring/mirroring solution (but we sketch the direction).
Terminology¶
- Generated project: the Cargo project emitted by Incan as an intermediate build artifact.
- Workspace root: the directory where the user runs
incan ...(or the nearest project root per future RFC 015). - Policy: constraints applied to underlying Cargo invocation(s) (offline/locked/frozen, plus advanced cargo args).
- Lockfile:
- Cargo lockfile:
Cargo.lockused by Cargo to pin transitive crates. - Incan lockfile (future):
incan.lock(RFC 013 direction), a source-of-truth lock that can materialize Cargo state.
- Cargo lockfile:
Guide-level explanation (how users think about it)¶
The mental model¶
Incan orchestrates Cargo. You can tell Incan to enforce the same policies you’d enforce with Cargo:
- Offline mode means: “If Cargo would need the network, fail instead.”
- Locked mode means: “Do not change the lockfile; if it’s missing or needs changes, fail instead.”
CLI examples¶
Build without network:
incan build src/main.incn --offline
Build and require an existing lockfile:
incan build src/main.incn --locked
Strict CI mode (recommended):
incan build src/main.incn --frozen
incan test tests/ --frozen
Run without network (useful in locked-down dev shells):
incan run src/main.incn --offline
Advanced: pass extra Cargo flags (escape hatch):
incan build src/main.incn --cargo-args "--features" "my_feature" "--no-default-features"
What happens to generated files¶
Incan generates a Cargo project under target/ and reuses it across runs. In general:
- Incan overwrites generated source and manifests (
Cargo.toml,src/**) to reflect current Incan sources. - Cargo manages build outputs (
target/**inside the generated project). - Lockfiles are preserved across Incan runs so that
--locked/--frozenare meaningful.
The precise contract is specified below.
Reference-level explanation (precise rules)¶
CLI surface (normative; additive)¶
Add Cargo policy flags to the following commands:
incan build <FILE> [OUTPUT_DIR]incan run <FILE>andincan run -c "<CODE>"incan test [PATH](in addition to the runner/selection surface defined by RFC 019)
Flags:
--offline- Semantics: underlying Cargo invocations must not access the network.
- Implementation requirement: pass Cargo’s offline semantics through (e.g.
cargo ... --offlineand/orCARGO_NET_OFFLINE=true).
--locked- Semantics: builds/tests must use an existing lockfile and must not modify it.
- Implementation requirement: pass Cargo’s
--lockedto Cargo invocations.
--frozen- Semantics: equivalent to
--offline --locked(mirrors Cargo’s “no network and no lockfile updates”). - If implemented,
--frozenimplies both--offlineand--lockedand they are treated as set.
- Semantics: equivalent to
--cargo-args <ARGS...>- Semantics: additional arguments forwarded to the underlying Cargo invocation after policy flags, as an escape hatch.
- Safety:
incanshould not attempt to validate arbitrary Cargo args beyond basic well-formedness.
Configuration + precedence (normative)¶
Policy configuration sources, highest priority first:
- CLI flags on the specific command invocation
- Environment variables (for CI policy)
- Defaults (no policy enforced)
Environment variables (recommended):
INCAN_OFFLINE=1behaves like--offlineINCAN_LOCKED=1behaves like--lockedINCAN_FROZEN=1behaves like--frozenINCAN_CARGO_ARGS="..."optional; see parsing rules below
--cargo-args and INCAN_CARGO_ARGS parsing (normative)¶
CLI flag (--cargo-args):
The CLI flag accepts multiple arguments that are forwarded to Cargo. Use one of these forms:
# Multiple separate arguments
incan build src/main.incn --cargo-args "--features" "my_feature" "--no-default-features"
# Or use a separator (recommended for clarity)
incan build src/main.incn -- --features my_feature --no-default-features
If -- is present, all arguments after it are treated as Cargo args (and --cargo-args is not needed).
Environment variable (INCAN_CARGO_ARGS):
The environment variable is split on whitespace. Quoting is not supported to avoid cross-platform shell escaping issues.
# Simple whitespace-separated args
INCAN_CARGO_ARGS="--features my_feature --no-default-features"
# NOT supported (quotes are literal, not parsed):
INCAN_CARGO_ARGS='--features "my feature"' # broken: passes literal quote chars
For arguments containing spaces, use the CLI flag instead of the environment variable.
Notes:
- If
INCAN_FROZENis set, it implies offline + locked, regardless of the other two env vars. - CLI flags override env vars (CI can still enforce by not letting users override flags; that is outside this RFC).
Out of scope (to avoid overlap with RFC 015):
- A project-level config key in
incan.tomlthat sets default Cargo policy. If/when we add it, it must follow RFC 015’sincan.tomlapproach and must not conflict with the CLI/env precedence defined above.
Generated project locations and persistence (normative)¶
Default output directories¶
incan build <file>andincan run <file>generate undertarget/incan/<name>/.incan testgenerates one or more harness projects undertarget/incan_tests/**.
Generated project naming (normative)¶
The <name> for a generated project is computed as follows:
- Project mode (when
incan.tomlexists): - Use the
[project].namevalue fromincan.toml. -
Example:
target/incan/my_app/ -
Single-file mode (no
incan.toml): - Use the file's path relative to the current working directory, with path separators replaced by
_and the.incnextension removed. - Examples (the
<name>portion is shown in bold):incan run src/main.incn→target/incan/src_main/incan run examples/main.incn→target/incan/examples_main/incan run foo/bar/baz.incn→target/incan/foo_bar_baz/incan run main.incn→target/incan/main/
Rationale: this prevents collisions between files with the same basename in different directories, while keeping names readable and predictable.
Regeneration rules¶
Within a generated project directory, files are classified as:
- Generated (owned by Incan): may be overwritten on each run.
Cargo.tomlsrc/**(generated Rust code)- Any other files explicitly emitted by Incan in the future
- Preserved (owned by Cargo / user tooling): Incan must not delete or overwrite them by default.
Cargo.locktarget/**(Cargo build artifacts).cargo/**(if present; e.g. registry overrides, config)
Rationale: preserving Cargo.lock is required for meaningful --locked behavior across invocations
when Cargo.lock is the only lock artifact in play.
Future compatibility note (RFC 013):
- RFC 013 introduces a project-root
incan.lockas the source-of-truth lockfile, embedding a Cargo lock payload. - If/when RFC 013 is implemented,
Cargo.lockfiles inside generated project directories become derived artifacts materialized fromincan.lockand may be overwritten deterministically. - In that world, the “preserved
Cargo.lock” rule is superseded by RFC 013’s lock materialization contract.
Lockfile requirements for policy modes¶
When --locked or --frozen is set:
-
Project mode (when
incan.tomlexists per RFC 015):- The authoritative lock artifact is the project-root
incan.lock(RFC 013). - If
incan.lockis missing (or out of date per RFC 013), the command must fail with a clear diagnostic:- explain that
incan.lockis required for strict modes - instruct the user to run
incan lock - point to the project root (
incan.tomllocation)
- explain that
- Incan must materialize the embedded Cargo lock payload from
incan.lockintoCargo.lockinside the generated project directory before invoking Cargo.
- The authoritative lock artifact is the project-root
-
Single-file mode (no
incan.toml):- The authoritative lock artifact is
Cargo.lockinside the generated project directory. - If the relevant generated project has no
Cargo.lock, the command must fail with a clear diagnostic:- explain that a lockfile is required
- recommend running once without
--lockedto generate it - point to the generated project path
- The authoritative lock artifact is
When --offline or --frozen is set:
- If Cargo would require network access (registry index, git fetch, etc.), the command must fail and surface Cargo’s error.
- Incan should add a short prefix that clarifies that this failure was expected under offline policy.
Relationship to RFC 013 (incan.lock) (informative, non-normative)¶
RFC 013 proposes an Incan lockfile (incan.lock) as a reproducible source-of-truth for Rust dependencies.
This RFC does not define incan.lock. It only defines:
- how
incanforwards Cargo policy flags (--offline/--locked/--frozen) - how generated Cargo projects are treated (notably:
Cargo.lockhandling differs by project vs single-file mode; see above)
If/when RFC 013 is implemented, its incan.lock workflow must compose with this RFC’s guarantees (in particular: policy
flags still constrain Cargo subprocesses, and generated-project persistence rules remain true unless superseded by a newer
RFC).
In practice, that means:
- The same CLI flags (
--offline/--locked/--frozen) constrain Cargo subprocesses exactly as specified here. - RFC 013 additionally defines Incan-level lock freshness requirements for
incan.lockunder strict modes. - Generated-project lockfile handling may change from “preserve
Cargo.lock” to “materializeCargo.lockfromincan.lock” once RFC 013 is implemented.
Design details¶
Policy application: which Cargo invocations are affected¶
The policy flags apply to all Cargo subprocesses invoked by incan for that command, including:
incan build:cargo build ...incan run:cargo run ...incan test:cargo test ...
Policy does not mean “no Rust dependencies”¶
Offline/locked policy constrains resolution and fetching, not whether crates are used. If a project depends on crates not already available in Cargo’s local cache (or a vendor directory), offline builds will fail—this is intended and is part of the contract.
“Escape hatch” stability¶
--cargo-args is intentionally a thin pass-through and is not guaranteed stable across major versions beyond:
- existence of the flag
- “args are forwarded to Cargo”
The stable, recommended surface is --offline/--locked/--frozen.
Compatibility / migration¶
- This RFC is additive: existing invocations continue to work without changes.
- Users who want determinism can start adding
--locked(or--frozen) in CI immediately. - Users may need an initial “priming” run (without
--offline) to populate Cargo caches and generateCargo.lock.
Alternatives considered¶
- Do nothing: users run
cargo --offline/--lockedmanually in generated directories.- Rejected: inconsistent CI policy, brittle, undermines toolchain contract.
- Environment variables only:
- Rejected: discoverability is worse; flags are the primary, user-facing contract (env vars remain useful for CI).
- Make offline/locked the default:
- Rejected (for now): would be breaking for new users and for first-time builds; revisit once
incan lock+ vendoring exist.
- Rejected (for now): would be breaking for new users and for first-time builds; revisit once
Drawbacks¶
- More CLI surface area and more combinations to test.
- Offline mode can be confusing without a vendoring/mirroring story (addressed via phased plan and clear diagnostics).
- The generated-project contract commits us to a persistence model that later RFCs must respect.
Implementation plan¶
- Tooling changes:
- Add
--offline/--locked/--frozen/--cargo-argstobuild,run,test - Thread a
CargoPolicy(or equivalent) from CLI parsing to all Cargo subprocess calls
- Add
- Backend changes:
- Apply policy to:
ProjectGenerator::build()ProjectGenerator::run()- the
incan testCargo invocation
- Apply policy to:
- Tests to add:
- CLI parsing tests for new flags
- A unit/integration test that verifies we pass
--offline/--lockedthrough to Cargo command construction
Follow-ups (explicitly out of scope here)¶
- Project-level defaults in
incan.toml(RFC 015) - Incan-level lockfile workflows (
incan.lock,incan lock,incan update) (RFC 013) - Vendoring/mirroring strategy to make offline-from-clean-machine reliable (likely a follow-up RFC)
Unresolved questions¶
- Should
--frozenbe implemented immediately, or deferred untilincan.lockexists (RFC 013)? - For
incan test, what is the preferred harness caching strategy undertarget/incan_tests/**(per-test vs per-run)? - Do we want an
incan doctorcheck that validates offline readiness (Cargo cache present, vendor dir present, etc.) as part of RFC 015, or as a follow-up RFC?