From cd8ef9e287c6c50278d376289009011dbba80f8e Mon Sep 17 00:00:00 2001 From: Bas Zalmstra <4995967+baszalmstra@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:17:50 +0000 Subject: [PATCH] fix(lock): detect host/build dependency changes in `--check`/`--dry-run` --- .../integration_rust/source_package_tests.rs | 73 +++++++++++++++++++ crates/pixi_cli/src/lock.rs | 14 ++-- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/crates/pixi/tests/integration_rust/source_package_tests.rs b/crates/pixi/tests/integration_rust/source_package_tests.rs index 87e603d383..1cf5b812a0 100644 --- a/crates/pixi/tests/integration_rust/source_package_tests.rs +++ b/crates/pixi/tests/integration_rust/source_package_tests.rs @@ -3300,3 +3300,76 @@ bat = { version = "*", when = { package = "python", version = ">=3.10" } } "conditional extra dependency must preserve its `when=` clause, got {test_group:?}" ); } + +/// Regression test for : a host +/// dependency change re-solves the lock file without touching the run +/// environment, so `--check`/`--dry-run` must not rely on `LockFileDiff`. +#[tokio::test] +async fn test_lock_check_detects_host_dependency_change() { + setup_tracing(); + + // Host-only dependency, no run-exports, so its version never reaches the run + // environment -- the case the diff is blind to. + let mut package_database = MockRepoData::default(); + package_database.add_package(Package::build("bar", "1").finish()); + package_database.add_package(Package::build("bar", "2").finish()); + let channel = package_database.into_channel().await.unwrap(); + + let pixi = PixiControl::new() + .unwrap() + .with_backend_override(BackendOverride::from_memory( + PassthroughBackend::instantiator(), + )); + + let source_dir = pixi.workspace_path().join("my-package"); + fs::create_dir_all(&source_dir).unwrap(); + let write_host_dep = |version: &str| { + fs::write( + source_dir.join("pixi.toml"), + format!( + r#" +[package] +name = "my-package" +version = "1.0.0" +[package.build] +backend = {{ name = "in-memory", version = "0.1.0" }} +[package.host-dependencies] +bar = "=={version}" +"# + ), + ) + .unwrap(); + }; + write_host_dep("1"); + pixi.update_manifest(&format!( + r#" +[workspace] +channels = ["{channel}"] +platforms = ["{platform}"] +preview = ["pixi-build"] +[dependencies] +my-package = {{ path = "./my-package" }} +"#, + channel = channel.url(), + platform = Platform::current(), + )) + .unwrap(); + pixi.lock().await.unwrap(); + + // Unchanged manifest: check passes (no false positive). + pixi.lock() + .with_check(true) + .with_dry_run(true) + .await + .unwrap(); + + // Bumped host dependency: check must now fail as out-of-date. + write_host_dep("2"); + let err = pixi + .lock() + .with_check(true) + .with_dry_run(true) + .await + .expect_err("`pixi lock --check --dry-run` must fail when a host dependency changed"); + assert!(format_diagnostic(err.as_ref()).contains("not up-to-date")); +} diff --git a/crates/pixi_cli/src/lock.rs b/crates/pixi_cli/src/lock.rs index 459e4f2354..8a548342e4 100644 --- a/crates/pixi_cli/src/lock.rs +++ b/crates/pixi_cli/src/lock.rs @@ -81,7 +81,9 @@ pub async fn execute(args: Args) -> miette::Result<()> { let json = serde_json::to_string_pretty(&json_diff).expect("failed to convert to json"); println!("{json}"); } else if args.dry_run { - if !diff.is_empty() { + // `diff` compares resolved packages by location, so it misses host/build + // dependency changes; rely on the re-solve signal instead. + if lock_updated { eprintln!( "{}Dry-run: lock file would be updated (not written to disk)", console::style(console::Emoji("i ", "i ")).blue() @@ -91,7 +93,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { .context("failed to print lock file diff")?; } else { eprintln!( - "{}Dry-run:lock file would not change", + "{}Dry-run: lock file would not change", console::style(console::Emoji("i ", "i ")).blue() ); } @@ -110,10 +112,10 @@ pub async fn execute(args: Args) -> miette::Result<()> { ); } - // Return with a non-zero exit code if `--check` has been passed and the lock - // file has been updated - if args.check && !diff.is_empty() { - std::process::exit(1); + // `--check`: fail if the lock file is out of date. Keyed on `lock_updated` + // (not `diff`) so the exit status matches the messages above. + if args.check && lock_updated { + miette::bail!("lock file not up-to-date with the workspace"); } Ok(())