diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs index dea83c8b2..d0573b1bd 100644 --- a/crates/uv-distribution-types/src/lib.rs +++ b/crates/uv-distribution-types/src/lib.rs @@ -639,6 +639,7 @@ impl SourceDist { } } + /// Returns the [`Version`] of the distribution, if it is known. pub fn version(&self) -> Option<&Version> { match self { Self::Registry(source_dist) => Some(&source_dist.version), @@ -646,7 +647,7 @@ impl SourceDist { } } - /// Return true if the distribution is editable. + /// Returns `true` if the distribution is editable. pub fn is_editable(&self) -> bool { match self { Self::Directory(DirectorySourceDist { editable, .. }) => *editable, @@ -654,7 +655,15 @@ impl SourceDist { } } - /// Return true if the distribution refers to a local file or directory. + /// Returns `true` if the distribution is virtual. + pub fn is_virtual(&self) -> bool { + match self { + Self::Directory(DirectorySourceDist { r#virtual, .. }) => *r#virtual, + _ => false, + } + } + + /// Returns `true` if the distribution refers to a local file or directory. pub fn is_local(&self) -> bool { matches!(self, Self::Directory(_) | Self::Path(_)) } @@ -668,7 +677,7 @@ impl SourceDist { } } - /// Return the source tree of the distribution, if available. + /// Returns the source tree of the distribution, if available. pub fn source_tree(&self) -> Option<&Path> { match self { Self::Directory(dist) => Some(&dist.install_path), diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index e1b5ba3b5..4fe07c00e 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -2196,8 +2196,11 @@ impl Package { }; } - if !no_build { - if let Some(sdist) = self.to_source_dist(workspace_root)? { + if let Some(sdist) = self.to_source_dist(workspace_root)? { + // Even with `--no-build`, allow virtual packages. (In the future, we may want to allow + // any local source tree, or at least editable source trees, which we allow in + // `uv pip`.) + if !no_build || sdist.is_virtual() { return Ok(Dist::Source(sdist)); } } diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 315731c6e..5bae24810 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -23005,14 +23005,16 @@ fn lock_no_build_static_metadata() -> Result<()> { "###); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--no-build").arg("--frozen"), @r###" - success: false - exit_code: 2 + uv_snapshot!(context.filters(), context.sync().arg("--no-build").arg("--frozen"), @r" + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - error: Distribution `dummy==0.1.0 @ virtual+.` can't be installed because it is marked as `--no-build` but has no binary distribution - "###); + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "); Ok(()) } diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 18ac85d3d..bd180899a 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -3544,6 +3544,133 @@ fn no_install_project_no_build() -> Result<()> { Ok(()) } +#[test] +fn virtual_no_build() -> Result<()> { + let context = TestContext::new("3.12"); + + 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" + dependencies = ["anyio==3.7.0"] + "#, + )?; + + // Generate a lockfile. + context.lock().assert().success(); + + // Clear the cache. + fs_err::remove_dir_all(&context.cache_dir)?; + + // `--no-build` should not raise an error, since we don't install virtual projects. + uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + sniffio==1.3.1 + "); + + Ok(()) +} + +#[test] +fn virtual_no_build_dynamic_cached() -> Result<()> { + let context = TestContext::new("3.12"); + + 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" + dynamic = ["dependencies"] + + [tool.setuptools.dynamic] + dependencies = {file = ["requirements.txt"]} + "#, + )?; + + context + .temp_dir + .child("requirements.txt") + .write_str("anyio==3.7.0")?; + + // Generate a lockfile. + context.lock().assert().success(); + + // `--no-build` should not raise an error, since we don't build or install the project (given + // that it's virtual and the metadata is cached). + uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + sniffio==1.3.1 + "); + + Ok(()) +} + +#[test] +fn virtual_no_build_dynamic_no_cache() -> Result<()> { + let context = TestContext::new("3.12"); + + 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" + dynamic = ["dependencies"] + + [tool.setuptools.dynamic] + dependencies = {file = ["requirements.txt"]} + "#, + )?; + + context + .temp_dir + .child("requirements.txt") + .write_str("anyio==3.7.0")?; + + // Generate a lockfile. + context.lock().assert().success(); + + // Clear the cache. + fs_err::remove_dir_all(&context.cache_dir)?; + + // `--no-build` should raise an error, since we need to build the project. + uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to generate package metadata for `project==0.1.0 @ virtual+.` + Caused by: Building source distributions for `project` is disabled + "); + + Ok(()) +} + /// Convert from a package to a virtual project. #[test] fn convert_to_virtual() -> Result<()> { @@ -4815,25 +4942,25 @@ fn no_build_error() -> Result<()> { error: Distribution `django-allauth==0.51.0 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-build` but has no binary distribution "###); - uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- Resolved 19 packages in [TIME] - error: Distribution `project==0.1.0 @ virtual+.` can't be installed because it is marked as `--no-build` but has no binary distribution - "###); + error: Distribution `django-allauth==0.51.0 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-build` but has no binary distribution + "); - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BUILD", "1"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BUILD", "1"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- Resolved 19 packages in [TIME] - error: Distribution `project==0.1.0 @ virtual+.` can't be installed because it is marked as `--no-build` but has no binary distribution - "###); + error: Distribution `django-allauth==0.51.0 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-build` but has no binary distribution + "); uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BUILD_PACKAGE", "django-allauth"), @r###" success: false