Skip to content

Lockfile graduation phase 2: wire writer in up + build feature pipeline (PR-4b) #32

Description

@pofallon

Follow-up to PR-4 (lockfile graduation, schema + CLI surface). PR-4 lands the user-visible flag surface (`--no-lockfile`, `--frozen-lockfile`), the schema parity fix (`dependsOn` rename, trailing newline), the upstream-aligned frozen error messages, and a helper (`LockfileFeature::from_resolved`) — but the actual writer is not yet invoked in any code path.

Scope

After feature resolution in `up`, build a `Lockfile` from the resolved features and write it to `get_lockfile_path(config_path)` unless `--no-lockfile` is set. With `--frozen-lockfile`, fail (exit 1) if the freshly-built lockfile differs from the existing one. Same for `build` (once `build` actually installs features — see also TODO at `crates/deacon/src/commands/build/mod.rs:1285`).

Plumbing

The data is already in scope:

  • `crates/deacon/src/commands/up/features_build.rs:378-382` — `downloaded_features: HashMap<String, DownloadedFeature>` (each has `digest: String` in `sha256:` form)
  • `DownloadedFeature.metadata.version` and `metadata.depends_on` provide the rest
  • `FeatureRef.registry` + `FeatureRef.repository()` complete the upstream `resolved` format

Helper to use (added in PR-4): `deacon_core::lockfile::LockfileFeature::from_resolved`.

Suggested integration

  1. Extend `StagedFeatures` (`features_build.rs:38`) with `lockfile: Lockfile`.
  2. In `stage_features` (around L382), build the `Lockfile` from `feature_refs + downloaded_features`:
    ```rust
    let mut lockfile = Lockfile { features: HashMap::new() };
    for (id, feature_ref) in &feature_refs {
    let downloaded = downloaded_features.get(id).unwrap();
    let deps: Option<Vec> = ...; // from downloaded.metadata.depends_on keys, sorted
    let entry = LockfileFeature::from_resolved(
    &feature_ref.registry,
    &feature_ref.repository(),
    &downloaded.digest,
    downloaded.metadata.version.clone().ok_or(...)?,
    deps,
    );
    lockfile.features.insert(id.clone(), entry);
    }
    ```
  3. Thread `lockfile` back through `FeatureBuildOutput` to `up/mod.rs`.
  4. In `up/mod.rs`, after a successful build:
    • If `--frozen-lockfile`: compare against existing on-disk lockfile via byte-comparison (write to in-memory string with the same canonical formatter, then compare). Fail with `"Lockfile does not match."` if differs.
    • Else if not `--no-lockfile`: `write_lockfile(&get_lockfile_path(&config_path), &lockfile, true)`.
    • On `EROFS`/`EACCES`: emit WARN and continue (read-only workspaces).

Tests

  • Spec parity: byte-identical output to upstream `devcontainer build` against the same features (record fixture).
  • `--no-lockfile`: no file written.
  • `--frozen-lockfile`: existing matching file → success; existing mismatched file → fail with upstream-aligned message; missing file → fail with `"Lockfile does not exist."`.
  • Default: lockfile written, sorted by key, trailing newline.
  • Concurrent `up` invocations: file-lock via `fs2` (or similar) to avoid races.

Also out of scope for PR-4b

  • `build` command lockfile wiring requires `build` to actually install features first (currently a TODO at `crates/deacon/src/commands/build/mod.rs:1285`). Either split that into a PR-4c or fold into PR-4b.

Refs

  • PR-4 (lockfile graduation, phase 1)
  • `docs/ROADMAP_TO_1.0.md` Tier 1 item "Lockfile graduation"
  • Upstream: `devcontainers/cli` `src/spec-configuration/lockfile.ts` `generateLockfile` + `writeLockfile` (PR #1212)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions