Skip to content

RFC 020: offline / locked / reproducible builds (Cargo policy + generated project contract)

  • Status: Implemented
  • Created: 2026-01-21
  • Author(s): Danny Meijer (@dannymeijer)
  • Related: RFC 013 (dependency + lockfile direction), RFC 015 (project lifecycle CLI), RFC 019 (test runner + CLI)
  • Issue: https://github.com/dannys-code-corner/incan/issues/38
  • RFC PR:
  • Written against: v0.1
  • Shipped in: v0.3

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, and incan test:
    • --offline (no network)
    • --locked (must use an existing lockfile)
    • --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/** and target/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 define incan.lock or incan lock/update.
  • RFC 015 (project metadata + lifecycle): this RFC does not define incan.toml schema 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 build and incan run (via cargo build/run on a generated project under target/incan/<name>)
  • incan test (via cargo test on a generated harness project under target/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, and --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.toml schema for projects (RFC 015).
  • Defining incan.lock format, incan lock, or incan update workflows (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.lock used by Cargo to pin transitive crates.
    • Incan lockfile (future): incan.lock (RFC 013 direction), a source-of-truth lock that can materialize Cargo state.

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/--frozen are 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> and incan 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 ... --offline and/or CARGO_NET_OFFLINE=true).
  • --locked
    • Semantics: builds/tests must use an existing lockfile and must not modify it.
    • Implementation requirement: pass Cargo’s --locked to Cargo invocations.
  • --frozen
    • Semantics: equivalent to --offline --locked (mirrors Cargo’s “no network and no lockfile updates”).
    • If implemented, --frozen implies both --offline and --locked and they are treated as set.
  • --cargo-args <ARGS...>
    • Semantics: additional arguments forwarded to the underlying Cargo invocation after policy flags, as an escape hatch.
    • Safety: incan should not attempt to validate arbitrary Cargo args beyond basic well-formedness.

Configuration + precedence (normative)

Policy configuration sources, highest priority first:

  1. CLI flags on the specific command invocation
  2. Environment variables (for CI policy)
  3. Defaults (no policy enforced)

Environment variables (recommended):

  • INCAN_OFFLINE=1 behaves like --offline
  • INCAN_LOCKED=1 behaves like --locked
  • INCAN_FROZEN=1 behaves like --frozen
  • INCAN_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_FROZEN is set, it implies offline + locked, regardless of the other two env vars.
  • CLI flags override env vars. Use --no-offline, --no-locked, or --no-frozen to disable matching environment defaults for one invocation. CI can still enforce policy 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.toml that sets default Cargo policy. If and when we add it, it must follow RFC 015’s incan.toml approach and must not conflict with the CLI/env precedence defined above.

Generated project locations and persistence (normative)

Default output directories

  • incan build <file> and incan run <file> generate under target/incan/<name>/.
  • incan test generates one or more harness projects under target/incan_tests/**.

Generated project naming (normative)

The <name> for a generated project is computed as follows:

  1. Project mode (when incan.toml exists):
  2. Use the [project].name value from incan.toml.
  3. Example: target/incan/my_app/

  4. Single-file mode (no incan.toml):

  5. Use the file's path relative to the current working directory, with path separators replaced by _ and the .incn extension removed.
  6. Examples (the <name> portion is shown in bold):
    • incan run src/main.incntarget/incan/src_main/
    • incan run examples/main.incntarget/incan/examples_main/
    • incan run foo/bar/baz.incntarget/incan/foo_bar_baz/
    • incan run main.incntarget/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.toml
    • src/** (generated backend 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.lock
    • target/** (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.lock as the source-of-truth lockfile, embedding a Cargo lock payload.
  • If and when RFC 013 is implemented, Cargo.lock files inside generated project directories become derived artifacts materialized from incan.lock and 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.toml exists per RFC 015):

    • The authoritative lock artifact is the project-root incan.lock (RFC 013).
    • If incan.lock is missing (or out of date per RFC 013), the command must fail with a clear diagnostic:
      • explain that incan.lock is required for strict modes
      • instruct the user to run incan lock
      • point to the project root (incan.toml location)
    • Incan must materialize the embedded Cargo lock payload from incan.lock into Cargo.lock inside the generated project directory before invoking Cargo.
  • Single-file mode (no incan.toml):

    • The authoritative lock artifact is Cargo.lock inside 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 --locked to generate it
      • point to the generated project path

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 incan forwards Cargo policy flags (--offline/--locked/--frozen)
  • how generated Cargo projects are treated (notably: Cargo.lock handling differs by project vs single-file mode; see above)

If and 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.lock under strict modes.
  • Generated-project lockfile handling may change from “preserve Cargo.lock” to “materialize Cargo.lock from incan.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. That 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 generate Cargo.lock.

Alternatives considered

  • Do nothing: users run cargo --offline/--locked manually 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 and vendoring exist.

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.

Layers affected

  • CLI / tooling: must expose and propagate Cargo policy flags consistently across incan build, incan run, and incan test.
  • Build orchestration: must thread policy through all Cargo subprocess invocation paths rather than only selected commands.
  • Generated project management: must preserve the documented persistence and overwrite behavior for generated Cargo projects under target/incan/** and target/incan_tests/**.
  • Docs / examples: must explain offline, locked, and frozen behavior clearly enough that CI and restricted-environment users can reason about failure modes.

Implementation Plan

Phase 1: RFC lifecycle and policy contract

  • Record the settled design decisions for frozen mode, test harness persistence, and offline-readiness doctor diagnostics.
  • Keep follow-up work linked without broadening this RFC's implementation scope.

Phase 2: CLI and environment policy surface

  • Add --offline, --locked, --frozen, and Cargo-args forwarding to incan build, incan run, and incan test.
  • Add CI-friendly environment inputs for offline, locked, frozen, and Cargo args.
  • Resolve CLI/env precedence so command-line policy wins over environment defaults.

Phase 3: Cargo subprocess propagation

  • Thread the combined policy through generated-project build and run paths.
  • Thread the same policy through incan test preparation and execution paths.
  • Preserve existing generated-project lockfile behavior while making policy forwarding consistent.

Phase 4: Tests and user documentation

  • Add focused tests for policy flag construction, env parsing, Cargo args, and test-runner cache keys.
  • Update the CLI reference and practical tooling docs for offline, locked, frozen, and Cargo args.
  • Add release notes and bump the active development version.

Implementation log

Spec / design

  • Decide that --frozen is implemented now as offline plus locked.
  • Decide that RFC 020 keeps the existing incan test harness persistence strategy; broader harness redesign is tracked separately.
  • Park offline-readiness incan tools doctor diagnostics in #460.
  • Keep issue #38 updated with the implementation plan.

CLI / policy surface

  • Add initial --locked and --frozen flags to incan build, incan run, and incan test.
  • Add --offline to incan build, incan run, and incan test.
  • Add Cargo-args forwarding for incan build, incan run, and incan test.
  • Add INCAN_OFFLINE, INCAN_LOCKED, INCAN_FROZEN, and INCAN_CARGO_ARGS support.
  • Apply CLI-over-env precedence consistently.
  • Add explicit --no-offline, --no-locked, and --no-frozen CLI overrides for env defaults.

Cargo subprocess propagation

  • Preserve/materialize generated Cargo lock payloads through existing lockfile support.
  • Forward offline/locked/frozen policy to all generated-project Cargo subprocesses.
  • Forward Cargo args after policy flags.
  • Include policy inputs in relevant test-runner cache keys.

Tests

  • Add policy flag construction tests.
  • Add env policy parsing tests.
  • Add Cargo args forwarding tests.
  • Add or update test-runner policy/cache tests.

Docs / release

  • Update CLI reference docs.
  • Update practical tooling/troubleshooting docs.
  • Add release notes entry for RFC 020.
  • Bump active dev version from 0.3.0-dev.22 to 0.3.0-dev.23.

Implementation architecture

  • Tooling changes:
    • Add --offline/--locked/--frozen/--cargo-args to build, run, test
    • Thread a CargoPolicy (or equivalent) from CLI parsing to all Cargo subprocess calls
  • Backend changes:
    • Apply policy consistently to every Cargo subprocess path used by incan build, incan run, and incan test
  • Tests to add:
    • CLI parsing tests for new flags
    • A unit/integration test that verifies we pass --offline / --locked through 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)
  • Offline-readiness diagnostics in incan tools doctor (#460)

Design Decisions

  1. --frozen is implemented immediately. It is the strict CI shorthand for offline plus locked policy, matching Cargo's semantics and keeping the public policy surface complete.
  2. RFC 020 keeps the existing incan test generated-harness persistence strategy. Broader harness redesign remains outside this RFC and is tracked by dedicated test-runner issues.
  3. Offline-readiness diagnostics belong in a follow-up extension to incan tools doctor, tracked by #460. RFC 020 only defines policy inputs and Cargo subprocess forwarding.