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
12 changes: 6 additions & 6 deletions crates/uv-resolver/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct Options {
pub fork_strategy: ForkStrategy,
pub exclude_newer: ExcludeNewer,
pub index_strategy: IndexStrategy,
pub required_environments: SupportedEnvironments,
pub artifact_environments: SupportedEnvironments,
pub flexibility: Flexibility,
pub build_options: BuildOptions,
pub torch_backend: Option<TorchStrategy>,
Expand All @@ -29,7 +29,7 @@ pub struct OptionsBuilder {
fork_strategy: ForkStrategy,
exclude_newer: ExcludeNewer,
index_strategy: IndexStrategy,
required_environments: SupportedEnvironments,
artifact_environments: SupportedEnvironments,
flexibility: Flexibility,
build_options: BuildOptions,
torch_backend: Option<TorchStrategy>,
Expand Down Expand Up @@ -83,10 +83,10 @@ impl OptionsBuilder {
self
}

/// Sets the required platforms.
/// Sets the environments that require artifact coverage.
#[must_use]
pub fn required_environments(mut self, required_environments: SupportedEnvironments) -> Self {
self.required_environments = required_environments;
pub fn artifact_environments(mut self, artifact_environments: SupportedEnvironments) -> Self {
self.artifact_environments = artifact_environments;
self
}

Expand Down Expand Up @@ -120,7 +120,7 @@ impl OptionsBuilder {
fork_strategy: self.fork_strategy,
exclude_newer: self.exclude_newer,
index_strategy: self.index_strategy,
required_environments: self.required_environments,
artifact_environments: self.artifact_environments,
flexibility: self.flexibility,
build_options: self.build_options,
torch_backend: self.torch_backend,
Expand Down
15 changes: 9 additions & 6 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1193,12 +1193,14 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
BuiltDist::Path(dist) => &dist.filename,
};

// If the wheel does _not_ cover a required platform, it's incompatible.
if env.marker_environment().is_none() && !self.options.required_environments.is_empty()
// If the wheel does _not_ cover an environment that requires artifact coverage, it's
// incompatible.
if env.marker_environment().is_none() && !self.options.artifact_environments.is_empty()
{
let wheel_marker = implied_markers(filename);
// If the user explicitly marked a platform as required, ensure it has coverage.
for environment_marker in self.options.required_environments.iter().copied() {
// If the caller marked an environment as requiring artifact coverage, ensure it
// has coverage.
for environment_marker in self.options.artifact_environments.iter().copied() {
// If the platform is part of the current environment...
if env.included_by_marker(environment_marker)
&& !find_environments(id, pubgrub).is_disjoint(environment_marker)
Expand Down Expand Up @@ -1455,8 +1457,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
return Ok(None);
}

// If the user explicitly marked a platform as required, ensure it has coverage.
for marker in self.options.required_environments.iter().copied() {
// If the caller marked an environment as requiring artifact coverage, ensure it has
// coverage.
for marker in self.options.artifact_environments.iter().copied() {
// If the platform is part of the current environment...
if env.included_by_marker(marker) {
// But isn't supported by the distribution...
Expand Down
7 changes: 7 additions & 0 deletions crates/uv/src/commands/pip/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,12 @@ pub(crate) async fn pip_compile(
PythonRequirement::from_interpreter(&interpreter)
};

let artifact_environments = if universal {
environments.clone()
} else {
SupportedEnvironments::default()
};

// Determine the environment for the resolution.
let (tags, resolver_env) = if universal {
(
Expand Down Expand Up @@ -554,6 +560,7 @@ pub(crate) async fn pip_compile(
.index_strategy(index_strategy)
.torch_backend(torch_backend)
.build_options(build_options.clone())
.artifact_environments(artifact_environments)
.build();

// Resolve the requirements.
Expand Down
24 changes: 13 additions & 11 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,14 +715,24 @@ async fn do_lock(
}
};

let lock_supported_environments = environments.cloned().unwrap_or_default();
let lock_required_environments = required_environments.cloned().unwrap_or_default();
let artifact_environments = SupportedEnvironments::from_markers(
lock_supported_environments
.iter()
.copied()
.chain(lock_required_environments.iter().copied())
.collect(),
);

let options = OptionsBuilder::new()
.resolution_mode(*resolution)
.prerelease_mode(*prerelease)
.fork_strategy(*fork_strategy)
.exclude_newer(exclude_newer.clone())
.index_strategy(*index_strategy)
.build_options(build_options.clone())
.required_environments(required_environments.cloned().unwrap_or_default())
.artifact_environments(artifact_environments.clone())
.build();
let hasher = HashStrategy::Generate(HashGeneration::Url);

Expand Down Expand Up @@ -978,19 +988,11 @@ async fn do_lock(
let lock = Lock::from_resolution(
&resolution,
target.install_path(),
environments
.cloned()
.map(SupportedEnvironments::into_markers)
.unwrap_or_default(),
lock_supported_environments.clone().into_markers(),
)?
.with_manifest(manifest)
.with_conflicts(conflicts)
.with_required_environments(
required_environments
.cloned()
.map(SupportedEnvironments::into_markers)
.unwrap_or_default(),
);
.with_required_environments(lock_required_environments.into_markers());

if previous.as_ref().is_some_and(|previous| *previous == lock) {
Ok(LockResult::Unchanged(lock))
Expand Down
132 changes: 80 additions & 52 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33995,7 +33995,7 @@ fn lock_unsupported_wheel_url_requires_python() -> Result<()> {
}

#[test]
fn lock_unsupported_wheel_url_required_platform() -> Result<()> {
fn lock_unsupported_wheel_url_supported_platform() -> Result<()> {
let context = uv_test::test_context!("3.11");

let pyproject_toml = context.temp_dir.child("pyproject.toml");
Expand All @@ -34008,95 +34008,123 @@ fn lock_unsupported_wheel_url_required_platform() -> Result<()> {
dependencies = ["numpy @ https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl"]

[tool.uv]
required-environments = ["sys_platform == 'win32'"]
environments = ["sys_platform == 'win32'"]
"#,
)?;

uv_snapshot!(context.filters(), context.lock(), @"
let filters: Vec<_> = context
.filters()
.into_iter()
.chain([(
// This hint is only shown when the current platform doesn't match the target.
r"\n\n\s+hint: The resolution failed for an environment that is not the current one[^\n]*",
"",
)])
.collect();

uv_snapshot!(filters, context.lock(), @"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
× No solution found when resolving dependencies:
× No solution found when resolving dependencies for split (markers: sys_platform == 'win32'):
╰─▶ Because only numpy==2.3.5 is available and numpy==2.3.5 has no Windows-compatible wheels, we can conclude that all versions of numpy cannot be used.
And because your project depends on numpy, we can conclude that your project's requirements are unsatisfiable.
");

Ok(())
}

// TODO(charlie): This produces an empty entry in the lockfile (`pywin32` has no wheels for the
// set of supported environments, and no source distribution).
#[test]
fn lock_supported_environment_wheel_only_package_can_produce_empty_entry() -> Result<()> {
let context = uv_test::test_context!("3.12").with_exclude_newer("2025-01-30T00:00:00Z");
fn lock_unsupported_wheel_url_required_platform() -> Result<()> {
let context = uv_test::test_context!("3.11");

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = "~=3.12.0"
dependencies = ["pywin32"]
requires-python = ">=3.11"
dependencies = ["numpy @ https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl"]

[tool.uv]
environments = ["sys_platform == 'linux' and platform_machine == 'x86_64'"]
required-environments = ["sys_platform == 'win32'"]
"#,
)?;

uv_snapshot!(
context.filters(),
context.lock(),
@"
success: true
exit_code: 0
uv_snapshot!(context.filters(), context.lock(), @"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
Resolved 2 packages in [TIME]
"
);

let lock = context.read("uv.lock");
× No solution found when resolving dependencies:
╰─▶ Because only numpy==2.3.5 is available and numpy==2.3.5 has no Windows-compatible wheels, we can conclude that all versions of numpy cannot be used.
And because your project depends on numpy, we can conclude that your project's requirements are unsatisfiable.
");

insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 3
requires-python = "==3.12.*"
resolution-markers = [
"platform_machine == 'x86_64' and sys_platform == 'linux'",
]
supported-markers = [
"platform_machine == 'x86_64' and sys_platform == 'linux'",
]
Ok(())
}

[options]
exclude-newer = "2025-01-30T00:00:00Z"
#[test]
fn lock_supported_environment_wheel_only_package_requires_compatible_wheels() -> Result<()> {
let context = uv_test::test_context!("3.12").with_exclude_newer("2025-01-30T00:00:00Z");

[[package]]
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "pywin32", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
]
requires-python = "~=3.12.0"
dependencies = ["pywin32"]

[package.metadata]
requires-dist = [{ name = "pywin32" }]
[tool.uv]
environments = ["sys_platform == 'linux'"]
"#,
)?;

[[package]]
name = "pywin32"
version = "308"
source = { registry = "https://pypi.org/simple" }
"#
);
});
let filters: Vec<_> = context
.filters()
.into_iter()
.chain([(
// This hint is only shown when the current platform doesn't match the target.
r"\n\n\s+hint: The resolution failed for an environment that is not the current one[^\n]*",
"",
)])
.collect();

uv_snapshot!(filters, context.lock(), @"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
× No solution found when resolving dependencies for split (markers: sys_platform == 'linux'):
╰─▶ Because only the following versions of pywin32 are available:
pywin32==222
pywin32==223
pywin32==224
pywin32==225
pywin32==226
pywin32==227
pywin32==228
pywin32==300
pywin32==301
pywin32==302
pywin32==303
pywin32==304
pywin32==305
pywin32==306
pywin32==307
pywin32==308
and pywin32<=305 has no wheels with a matching Python version tag (e.g., `cp312`), we can conclude that pywin32<=305 cannot be used.
And because pywin32>=306 has no Linux-compatible wheels and your project depends on pywin32, we can conclude that your project's requirements are unsatisfiable.

hint: Wheels are available for `pywin32` (v305) with the following Python ABI tags: `cp36m`, `cp37m`, `cp38`, `cp39`, `cp310`, `cp311`
");

Ok(())
}
Expand Down
Loading