Skip to content
Merged
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
1 change: 0 additions & 1 deletion crates/pixi/tests/integration_rust/add_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,6 @@ index-url = "{index_url}"
index: None,
},
env_markers: MarkerTree::default(),
exclude_newer: None,
}
);
}
Expand Down
118 changes: 118 additions & 0 deletions crates/pixi/tests/integration_rust/build_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,124 @@ my-package = {{ path = "./my-package" }}
assert!(rendered.contains("foo"), "{rendered}");
}

#[tokio::test]
async fn test_source_dependency_honors_exclude_newer_overrides_for_host_and_build_dependencies() {
setup_tracing();

let mut package_database = MockRepoData::default();
package_database.add_package(
Package::build("foo", "1")
.with_timestamp("2026-01-10T00:00:00Z".parse().unwrap())
.with_materialize(true)
.finish(),
);
package_database.add_package(
Package::build("bar", "1")
.with_timestamp("2026-01-10T00:00:00Z".parse().unwrap())
.with_materialize(true)
.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();
fs::write(
source_dir.join("pixi.toml"),
r#"
[package]
name = "my-package"
version = "1.0.0"

[package.build]
backend = { name = "in-memory", version = "0.1.0" }

[package.host-dependencies]
bar = "*"

[package.build-dependencies]
foo = "*"
"#,
)
.unwrap();

let cutoff = "2025-01-01T00:00:00Z";
let override_cutoff = "2026-12-31T00:00:00Z";

let manifest_with_build_override_only = format!(
r#"
[workspace]
channels = ["{channel}"]
platforms = ["{platform}"]
preview = ["pixi-build"]
exclude-newer = "{cutoff}"

[dependencies]
my-package = {{ path = "./my-package" }}

[exclude-newer]
foo = "{override_cutoff}"
"#,
channel = channel.url(),
platform = Platform::current(),
cutoff = cutoff,
override_cutoff = override_cutoff,
);
pixi.update_manifest(&manifest_with_build_override_only)
.unwrap();

let err = pixi
.install()
.await
.expect_err("host dependency should still be excluded until it is overridden too");
let rendered = format_diagnostic(err.as_ref());
assert!(
rendered.contains("while trying to solve the host environment"),
"{rendered}"
);
assert!(rendered.contains("bar"), "{rendered}");

let manifest_with_both_overrides = format!(
r#"
[workspace]
channels = ["{channel}"]
platforms = ["{platform}"]
preview = ["pixi-build"]
exclude-newer = "{cutoff}"

[dependencies]
my-package = {{ path = "./my-package" }}

[exclude-newer]
foo = "{override_cutoff}"
bar = "{override_cutoff}"
"#,
channel = channel.url(),
platform = Platform::current(),
cutoff = cutoff,
override_cutoff = override_cutoff,
);
pixi.update_manifest(&manifest_with_both_overrides).unwrap();

let err = pixi
.install()
.await
.expect_err("timestamp-less source package should remain the only blocker");
let rendered = format_diagnostic(err.as_ref());
assert!(
rendered.contains("my-package 1.0.0 is excluded"),
"{rendered}"
);
assert!(rendered.contains("package has no timestamp"), "{rendered}");
assert!(!rendered.contains("bar 1 is excluded"), "{rendered}");
assert!(!rendered.contains("foo 1 is excluded"), "{rendered}");
}

/// Test that demonstrates using PassthroughBackend with PixiControl
/// to test build operations without requiring actual backend processes.
#[tokio::test]
Expand Down
9 changes: 6 additions & 3 deletions crates/pixi/tests/integration_rust/install_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1554,7 +1554,10 @@ async fn test_exclude_newer_per_package_dependency_override() {
exclude-newer = "2015-12-02T02:07:43Z"

[dependencies]
foo = {{ version = "*", exclude-newer = "0d" }}
foo = "*"

[exclude-newer]
foo = "0d"
"#,
channel = channel.url(),
platform = Platform::current()
Expand Down Expand Up @@ -1605,8 +1608,8 @@ async fn test_exclude_newer_per_package_constraint_override() {
[dependencies]
foo = "*"

[constraints]
bar = {{ exclude-newer = "0d" }}
[exclude-newer]
bar = "0d"
"#,
channel = channel.url(),
platform = Platform::current()
Expand Down
10 changes: 8 additions & 2 deletions crates/pixi/tests/integration_rust/pypi_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,10 @@ async fn test_exclude_newer_per_package_pypi_index_override() {
python = "==3.12.0"

[pypi-dependencies]
foo = {{ version = "*", index = "{explicit_idx_url}", exclude-newer = "0d" }}
foo = {{ version = "*", index = "{explicit_idx_url}" }}

[pypi-exclude-newer]
foo = "0d"

[pypi-options]
index-url = "{default_idx_url}"
Expand Down Expand Up @@ -693,7 +696,10 @@ async fn test_exclude_newer_dependency_override_pypi_index_override() {
index-url = "{default_idx_url}"

[pypi-options.dependency-overrides]
foo = {{ version = ">=2.0.0", index = "{explicit_idx_url}", exclude-newer = "0d" }}
foo = {{ version = ">=2.0.0", index = "{explicit_idx_url}" }}

[pypi-exclude-newer]
foo = "0d"
"#,
platform = platform,
channel_url = channel.url(),
Expand Down
1 change: 0 additions & 1 deletion crates/pixi_command_dispatcher/src/build/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ pub fn from_binary_spec_v1(spec: BinaryPackageSpec) -> pixi_spec::BinarySpec {
license,
md5,
sha256,
exclude_newer: None,
})),
}
}
1 change: 0 additions & 1 deletion crates/pixi_command_dispatcher/src/build/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,6 @@ fn filter_match_specs<T: From<BinarySpec> + Clone + Hash + Eq + PartialEq>(
md5,
sha256,
license,
exclude_newer: None,
})),
};

Expand Down
36 changes: 36 additions & 0 deletions crates/pixi_command_dispatcher/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,42 @@ pub async fn instantiate_backend_with_compatible_api_version_respects_exclude_ne
assert!(rendered.contains("backend-with-compatible-api-version"));
}

#[tokio::test]
pub async fn instantiate_backend_with_compatible_api_version_honors_exclude_newer_overrides() {
let backend_name = PackageName::new_unchecked("backend-with-compatible-api-version");
let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.unwrap();
let channel_dir = root_dir.join("tests/data/channels/channels/backend_channel_1");
let channel_url = Url::from_directory_path(channel_dir).unwrap().into();
let allowed_cutoff = "2026-12-31T00:00:00Z".parse().unwrap();

let dispatcher = CommandDispatcher::builder()
.with_cache_dirs(default_cache_dirs())
.with_executor(Executor::Serial)
.finish();

dispatcher
.instantiate_tool_environment(InstantiateToolEnvironmentSpec {
exclude_newer: Some(
ResolvedExcludeNewer::from_datetime("2025-01-01T00:00:00Z".parse().unwrap())
.with_package_cutoff(backend_name.clone(), allowed_cutoff)
.with_package_cutoff(
PackageName::new_unchecked("pixi-build-api-version"),
allowed_cutoff,
),
),
..InstantiateToolEnvironmentSpec::new(
backend_name,
PixiSpec::Version(VersionSpec::Any),
Vec::from([channel_url]),
)
})
.await
.expect("backend should instantiate when backend packages are explicitly overridden");
}

/// When two identical tool env instantiations are queued concurrently and the
/// operation fails, the dispatcher sends the failure to one waiter and cancels
/// the others. This verifies cancellation without network access.
Expand Down
2 changes: 1 addition & 1 deletion crates/pixi_core/src/environment/conda_prefix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ impl CondaPrefixUpdaterBuilder<'_> {
let variant_config = self.group.workspace().variants(self.platform)?;
let exclude_newer = self
.group
.exclude_newer_config_resolved(Some(self.platform))
.exclude_newer_config_resolved()
.into_diagnostic()?;

Ok(CondaPrefixUpdater::new(
Expand Down
4 changes: 2 additions & 2 deletions crates/pixi_core/src/lock_file/satisfiability/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@ fn verify_exclude_newer(
environment: &Environment<'_>,
locked_environment: &rattler_lock::Environment<'_>,
) -> Result<(), ExcludeNewerMismatch> {
for (platform, packages) in locked_environment.conda_packages_by_platform() {
for (_platform, packages) in locked_environment.conda_packages_by_platform() {
let Some(exclude_newer) = environment
.exclude_newer_config(Some(platform))
.exclude_newer_config()
.expect("environment channels were already validated")
else {
continue;
Expand Down
8 changes: 3 additions & 5 deletions crates/pixi_core/src/lock_file/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -783,8 +783,7 @@ impl<'p> LockFileDerivedData<'p> {
{
let pypi_indexes = self.locked_env(environment)?.pypi_indexes().cloned();
let index_strategy = environment.pypi_options().index_strategy.clone();
let pypi_exclude_newer = environment
.pypi_exclude_newer_config_resolved(Some(environment.best_platform()));
let pypi_exclude_newer = environment.pypi_exclude_newer_config_resolved();
let skip_wheel_filename_check =
environment.pypi_options().skip_wheel_filename_check;

Expand Down Expand Up @@ -2265,7 +2264,7 @@ async fn spawn_solve_conda_environment_task(
// Get the channel configuration
let channel_config = group.workspace().channel_config();
let exclude_newer = group
.exclude_newer_config_resolved(Some(platform))
.exclude_newer_config_resolved()
.map_err(SolveCondaEnvironmentError::from)
.map_err(CommandDispatcherError::Failed)?;

Expand Down Expand Up @@ -2625,8 +2624,7 @@ async fn spawn_solve_pypi_task<'p>(
));
}

let exclude_newer =
to_exclude_newer(&grouped_environment.pypi_exclude_newer_config_resolved(Some(platform)));
let exclude_newer = to_exclude_newer(&grouped_environment.pypi_exclude_newer_config_resolved());

// Get the system requirements for this environment
let system_requirements = grouped_environment.system_requirements();
Expand Down
13 changes: 7 additions & 6 deletions crates/pixi_core/src/workspace/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,7 @@ mod tests {
.unwrap();

let env = workspace.environment("combined").unwrap();
let config = env.exclude_newer_config(None).unwrap().unwrap();
let config = env.exclude_newer_config().unwrap().unwrap();
let package = PackageName::new_unchecked("polars");

assert_eq!(
Expand Down Expand Up @@ -898,7 +898,7 @@ mod tests {
.unwrap();

let env = workspace.environment("combined").unwrap();
let config = env.exclude_newer_config(None).unwrap().unwrap();
let config = env.exclude_newer_config().unwrap().unwrap();
let package = PackageName::new_unchecked("polars");

assert_eq!(
Expand Down Expand Up @@ -940,17 +940,18 @@ mod tests {
exclude-newer = "2015-12-02T02:07:43Z"

[dependencies]
polars = { version = "*", exclude-newer = "2017-12-02T02:07:43Z" }
polars = "*"
numpy = "*"

[constraints]
openssl = { exclude-newer = "2018-12-02T02:07:43Z" }
[exclude-newer]
polars = "2017-12-02T02:07:43Z"
openssl = "2018-12-02T02:07:43Z"
"#,
)
.unwrap();

let env = workspace.environment("default").unwrap();
let config = env.exclude_newer_config(None).unwrap().unwrap();
let config = env.exclude_newer_config().unwrap().unwrap();
let polars = PackageName::new_unchecked("polars");
let numpy = PackageName::new_unchecked("numpy");
let openssl = PackageName::new_unchecked("openssl");
Expand Down
Loading
Loading