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
- Extend `StagedFeatures` (`features_build.rs:38`) with `lockfile: Lockfile`.
- 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);
}
```
- Thread `lockfile` back through `FeatureBuildOutput` to `up/mod.rs`.
- 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)
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:
Helper to use (added in PR-4): `deacon_core::lockfile::LockfileFeature::from_resolved`.
Suggested integration
```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);
}
```
Tests
Also out of scope for PR-4b
Refs