diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 06783f642c6c..5339d3aa82fe 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -472,8 +472,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { } /// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted. - pub async fn requires_dist(&self, project_root: &Path) -> Result { - self.builder.requires_dist(project_root).await + pub async fn requires_dist(&self, project_root: &Path) -> Result, Error> { + self.builder.source_tree_requires_dist(project_root).await } /// Stream a wheel from a URL, unzipping it into the cache as it's downloaded. diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 8df51913f75b..7c36bb2c814a 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -360,21 +360,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Ok(metadata) } - /// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted. - pub(crate) async fn requires_dist(&self, project_root: &Path) -> Result { - let requires_dist = read_requires_dist(project_root).await?; - let requires_dist = RequiresDist::from_project_maybe_workspace( - requires_dist, - project_root, - None, - self.build_context.locations(), - self.build_context.sources(), - self.build_context.bounds(), - ) - .await?; - Ok(requires_dist) - } - /// Build a source distribution from a remote URL. async fn url<'data>( &self, @@ -1250,6 +1235,48 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Ok(pointer) } + /// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted. + pub(crate) async fn source_tree_requires_dist( + &self, + project_root: &Path, + ) -> Result, Error> { + // Attempt to read static metadata from the `pyproject.toml`. + match read_requires_dist(project_root).await { + Ok(requires_dist) => { + debug!( + "Found static `requires-dist` for: {}", + project_root.display() + ); + let requires_dist = RequiresDist::from_project_maybe_workspace( + requires_dist, + project_root, + None, + self.build_context.locations(), + self.build_context.sources(), + self.build_context.bounds(), + ) + .await?; + Ok(Some(requires_dist)) + } + Err( + err @ (Error::MissingPyprojectToml + | Error::PyprojectToml( + uv_pypi_types::MetadataError::Pep508Error(_) + | uv_pypi_types::MetadataError::DynamicField(_) + | uv_pypi_types::MetadataError::FieldNotFound(_) + | uv_pypi_types::MetadataError::PoetrySyntax, + )), + ) => { + debug!( + "No static `requires-dist` available for: {} ({err:?})", + project_root.display() + ); + Ok(None) + } + Err(err) => Err(err), + } + } + /// Build a source distribution from a Git repository. async fn git( &self, diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index a6f2e2aafa22..569c658b6200 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -191,8 +191,12 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> { ) })?; - // If the path is a `pyproject.toml`, attempt to extract the requirements statically. - if let Ok(metadata) = self.database.requires_dist(source_tree).await { + // If the path is a `pyproject.toml`, attempt to extract the requirements statically. The + // distribution database will do this too, but we can be even more aggressive here since we + // _only_ need the requirements. So, for example, even if the version is dynamic, we can + // still extract the requirements without performing a build, unlike in the database where + // we typically construct a "complete" metadata object. + if let Some(metadata) = self.database.requires_dist(source_tree).await? { return Ok(metadata); } diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 198bea7195c4..a132e7a48768 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -293,6 +293,37 @@ dependencies = [ Ok(()) } +#[test] +fn compile_pyproject_toml_eager_validation() -> Result<()> { + let context = TestContext::new("3.12"); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + dynamic = ["version"] + requires-python = ">=3.10" + dependencies = ["anyio==4.7.0"] + + [tool.uv.sources] + anyio = { workspace = true } + "#})?; + + // This should fail without attempting to build the package. + uv_snapshot!(context + .pip_compile() + .arg("pyproject.toml"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to parse entry: `anyio` + Caused by: `anyio` references a workspace in `tool.uv.sources` (e.g., `anyio = { workspace = true }`), but is not a workspace member + "###); + + Ok(()) +} + /// Resolve a package from a `requirements.in` file, with a `constraints.txt` file. #[test] fn compile_constraints_txt() -> Result<()> {