Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions crates/pixi/tests/integration_rust/source_package_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/prefix-dev/pixi/issues/6445>: 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"));
}
14 changes: 8 additions & 6 deletions crates/pixi_cli/src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
);
}
Expand All @@ -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(())
Expand Down
Loading