From 4fd62290d4617c53574e2c9dfbdfc06a005579bc Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 10 Jun 2026 16:51:18 +0200 Subject: [PATCH 1/5] Fix stop-discovery-at regression Adding the `stop_discovery_at.is_none()` at https://github.com/astral-sh/uv/blob/53d3f5443441ff5ef17b66a0d68cf7091dd5a091/crates/uv-workspace/src/workspace.rs#L1734-L1754 in https://github.com/astral-sh/uv/pull/18311 inadvertently caused a regression when `stop_discovery_at` was missing during workspace discovery. This PR fixes that. --- crates/uv-build-frontend/src/lib.rs | 7 ++ crates/uv-dispatch/src/lib.rs | 2 + .../src/metadata/build_requires.rs | 15 +-- crates/uv-distribution/src/source/mod.rs | 32 +++++- crates/uv-types/src/traits.rs | 1 + crates/uv/src/commands/build_frontend.rs | 2 + crates/uv/tests/pip_install/pip_install.rs | 103 ++++++++++++++++++ 7 files changed, 153 insertions(+), 9 deletions(-) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 9690c88222910..443b661a21da3 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -289,6 +289,7 @@ impl SourceBuild { source: &Path, subdirectory: Option<&Path>, install_path: &Path, + stop_discovery_at: Option<&Path>, fallback_package_name: Option<&PackageName>, fallback_package_version: Option<&Version>, interpreter: &Interpreter, @@ -322,6 +323,7 @@ impl SourceBuild { fallback_package_name, locations, &no_sources, + stop_discovery_at, build_context.cache(), workspace_cache, credentials_cache, @@ -458,6 +460,7 @@ impl SourceBuild { version_id, locations, no_sources, + stop_discovery_at, workspace_cache, build_stack, build_kind, @@ -574,6 +577,7 @@ impl SourceBuild { package_name: Option<&PackageName>, locations: &IndexLocations, no_sources: &NoSources, + stop_discovery_at: Option<&Path>, cache: &Cache, workspace_cache: &WorkspaceCache, credentials_cache: &CredentialsCache, @@ -655,6 +659,7 @@ impl SourceBuild { locations, no_sources, true, + stop_discovery_at, cache, workspace_cache, credentials_cache, @@ -998,6 +1003,7 @@ async fn create_pep517_build_environment( version_id: Option<&str>, locations: &IndexLocations, no_sources: NoSources, + stop_discovery_at: Option<&Path>, workspace_cache: &WorkspaceCache, build_stack: &BuildStack, build_kind: BuildKind, @@ -1104,6 +1110,7 @@ async fn create_pep517_build_environment( build_context .source_tree_editable_policy() .workspace_member_editable(None), + stop_discovery_at, build_context.cache(), workspace_cache, credentials_cache, diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 8882be80256ff..a33a58b0c0071 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -491,6 +491,7 @@ impl BuildContext for BuildDispatch<'_> { source: &'data Path, subdirectory: Option<&'data Path>, install_path: &'data Path, + stop_discovery_at: Option<&'data Path>, version_id: Option<&'data str>, dist: Option<&'data SourceDist>, sources: &'data NoSources, @@ -554,6 +555,7 @@ impl BuildContext for BuildDispatch<'_> { source, subdirectory, install_path, + stop_discovery_at, dist_name, dist_version, self.interpreter, diff --git a/crates/uv-distribution/src/metadata/build_requires.rs b/crates/uv-distribution/src/metadata/build_requires.rs index de0efd1410e00..531c48a5f16b6 100644 --- a/crates/uv-distribution/src/metadata/build_requires.rs +++ b/crates/uv-distribution/src/metadata/build_requires.rs @@ -44,17 +44,18 @@ impl BuildRequires { locations: &IndexLocations, sources: &NoSources, editable: bool, + stop_discovery_at: Option<&Path>, cache: &Cache, workspace_cache: &WorkspaceCache, credentials_cache: &CredentialsCache, ) -> Result { - let discovery = if sources.all() { - DiscoveryOptions { - members: MemberDiscovery::None, - ..Default::default() - } - } else { - DiscoveryOptions::default() + let discovery = DiscoveryOptions { + stop_discovery_at: stop_discovery_at.map(Path::to_path_buf), + members: if sources.all() { + MemberDiscovery::None + } else { + MemberDiscovery::default() + }, }; let Some(project_workspace) = ProjectWorkspace::from_maybe_project_root( install_path, diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index fba1032f1c658..13afd9c9cd63e 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -3010,12 +3010,26 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } else { debug!("Reusing existing build environment for: {source}"); + let install_path = subdirectory.map(|subdirectory| source_root.join(subdirectory)); + let install_path = install_path.as_deref().unwrap_or(source_root); + + let stop_discovery_at = if matches!( + source, + BuildableSource::Dist(SourceDist::GitDirectory(_)) + | BuildableSource::Url(SourceUrl::GitDirectory(_)) + ) { + source_root.parent() + } else { + None + }; + let builder = self .build_context .setup_build( source_root, subdirectory, - source_root, + install_path, + stop_discovery_at, Some(&source.to_string()), source.as_dist(), &no_sources, @@ -3134,13 +3148,27 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { BuildKind::Wheel }; + let install_path = subdirectory.map(|subdirectory| source_root.join(subdirectory)); + let install_path = install_path.as_deref().unwrap_or(source_root); + + let stop_discovery_at = if matches!( + source, + BuildableSource::Dist(SourceDist::GitDirectory(_)) + | BuildableSource::Url(SourceUrl::GitDirectory(_)) + ) { + source_root.parent() + } else { + None + }; + // Set up the builder. let mut builder = self .build_context .setup_build( source_root, subdirectory, - source_root, + install_path, + stop_discovery_at, Some(&source.to_string()), source.as_dist(), &no_sources, diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index d27784146fa28..aa08aa1e37ec4 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -174,6 +174,7 @@ pub trait BuildContext { source: &'a Path, subdirectory: Option<&'a Path>, install_path: &'a Path, + stop_discovery_at: Option<&'a Path>, version_id: Option<&'a str>, dist: Option<&'a SourceDist>, sources: &'a NoSources, diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 530c0e8f643c3..2c21b6fb4992f 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -1016,6 +1016,7 @@ async fn build_sdist( source_tree, subdirectory, source.path(), + None, version_id, dist, sources, @@ -1122,6 +1123,7 @@ async fn build_wheel( source_tree, subdirectory, source.path(), + None, version_id, dist, &sources, diff --git a/crates/uv/tests/pip_install/pip_install.rs b/crates/uv/tests/pip_install/pip_install.rs index 9ca15aab09829..ba82c856d013f 100644 --- a/crates/uv/tests/pip_install/pip_install.rs +++ b/crates/uv/tests/pip_install/pip_install.rs @@ -2382,6 +2382,109 @@ fn install_git_public_https() { context.assert_installed("uv_public_pypackage", "0.1.0"); } +/// Install a package from a Git workspace subdirectory with a workspace build requirement. +#[test] +#[cfg(feature = "test-git")] +fn install_git_workspace_build_requirement() -> Result<()> { + let context = uv_test::test_context!(DEFAULT_PYTHON_VERSION); + + let repository = context.temp_dir.child("repository"); + repository.child("pyproject.toml").write_str(indoc! {r#" + [tool.uv.workspace] + members = ["packages/*"] + + [tool.uv.sources] + uv-test-stop-discovery-at-build-dependency = { workspace = true } + "#})?; + + repository + .child("packages/build-dependency/pyproject.toml") + .write_str(indoc! {r#" + [project] + name = "uv-test-stop-discovery-at-build-dependency" + version = "0.1.0" + requires-python = ">=3.12" + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#})?; + repository + .child( + "packages/build-dependency/src/uv_test_stop_discovery_at_build_dependency/__init__.py", + ) + .touch()?; + + repository + .child("packages/project/pyproject.toml") + .write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + + [build-system] + requires = ["hatchling", "uv-test-stop-discovery-at-build-dependency"] + build-backend = "hatchling.build" + "#})?; + repository + .child("packages/project/src/project/__init__.py") + .write_str(r#"__version__ = "0.1.0""#)?; + + Command::new("git") + .arg("init") + .arg(repository.path()) + .assert() + .success(); + Command::new("git") + .arg("-C") + .arg(repository.path()) + .arg("add") + .arg(".") + .assert() + .success(); + Command::new("git") + .arg("-C") + .arg(repository.path()) + .arg("-c") + .arg("user.name=ferris") + .arg("-c") + .arg("user.email=ferris@example.com") + .arg("commit") + .arg("-m") + .arg("Initial commit") + .env("GIT_AUTHOR_DATE", "2000-01-01T00:00:00Z") + .env("GIT_COMMITTER_DATE", "2000-01-01T00:00:00Z") + .assert() + .success(); + + let repository_url = Url::from_directory_path(repository.path()) + .map_err(|()| anyhow!("failed to convert repository path to file URL"))?; + let repository_url = repository_url.as_str().trim_end_matches('/'); + + let mut filters = context.filters(); + filters.push((r"@[0-9a-f]{40}", "@[COMMIT]")); + uv_snapshot!(filters, context + .pip_install() + .arg(format!( + "project @ git+{repository_url}#subdirectory=packages/project" + )), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + project==0.1.0 (from git+file://[TEMP_DIR]/repository@[COMMIT]#subdirectory=packages/project) + "); + + context.assert_installed("project", "0.1.0"); + + Ok(()) +} + /// Install a package from a public GitHub repository, omitting the `git+` prefix #[test] #[cfg(feature = "test-git")] From 93f1150397cc3cae0cc79b74d391dca64444b36a Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 11 Jun 2026 14:58:28 +0200 Subject: [PATCH 2/5] Improve code quality --- crates/uv-distribution/src/source/mod.rs | 54 +++++++++++++----------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 13afd9c9cd63e..51e5b0c6e457c 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -2909,6 +2909,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Ok(hashes) } + /// For Git directories, we check them out into the cache, so we need to avoid workspac + /// discovery that goes outside the cache. + fn stop_discovery_at(&self, source: &BuildableSource<'_>, source_root: &Path) -> Option<&Path> { + if matches!( + source, + BuildableSource::Dist(SourceDist::GitDirectory(_)) + | BuildableSource::Url(SourceUrl::GitDirectory(_)) + ) { + Some(source_root) + } else { + None + } + } + /// Build a source distribution, storing the built wheel in the cache. /// /// Returns the un-normalized disk filename, the parsed, normalized filename and the metadata @@ -2990,6 +3004,14 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { BuildKind::Wheel }; + let install_path = if let Some(subdirectory) = subdirectory { + source_root.join(subdirectory) + } else { + source_root.to_path_buf() + }; + + let stop_discovery_at = self.stop_discovery_at(source, source_root): + let build_key = BuildKey { base_python: base_python.into_boxed_path(), source_root: source_root.to_path_buf().into_boxed_path(), @@ -3010,25 +3032,12 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } else { debug!("Reusing existing build environment for: {source}"); - let install_path = subdirectory.map(|subdirectory| source_root.join(subdirectory)); - let install_path = install_path.as_deref().unwrap_or(source_root); - - let stop_discovery_at = if matches!( - source, - BuildableSource::Dist(SourceDist::GitDirectory(_)) - | BuildableSource::Url(SourceUrl::GitDirectory(_)) - ) { - source_root.parent() - } else { - None - }; - let builder = self .build_context .setup_build( source_root, subdirectory, - install_path, + &install_path, stop_discovery_at, Some(&source.to_string()), source.as_dist(), @@ -3148,26 +3157,21 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { BuildKind::Wheel }; - let install_path = subdirectory.map(|subdirectory| source_root.join(subdirectory)); - let install_path = install_path.as_deref().unwrap_or(source_root); - - let stop_discovery_at = if matches!( - source, - BuildableSource::Dist(SourceDist::GitDirectory(_)) - | BuildableSource::Url(SourceUrl::GitDirectory(_)) - ) { - source_root.parent() + let install_path = if let Some(subdirectory) = subdirectory { + source_root.join(subdirectory) } else { - None + source_root.to_path_buf() }; + let stop_discovery_at = self.stop_discovery_at(source, source_root); + // Set up the builder. let mut builder = self .build_context .setup_build( source_root, subdirectory, - install_path, + &install_path, stop_discovery_at, Some(&source.to_string()), source.as_dist(), From 1671a8b0da2f0ef8868c8595d4439674e82bd42d Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 11 Jun 2026 15:19:09 +0200 Subject: [PATCH 3/5] . --- crates/uv-distribution/src/source/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 51e5b0c6e457c..7ca9413529690 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -2909,7 +2909,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Ok(hashes) } - /// For Git directories, we check them out into the cache, so we need to avoid workspac + /// For Git directories, we check them out into the cache, so we need to avoid workspace /// discovery that goes outside the cache. fn stop_discovery_at(&self, source: &BuildableSource<'_>, source_root: &Path) -> Option<&Path> { if matches!( From cba9b33d14400c49d4e3bdca67fdf659c706b6be Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 11 Jun 2026 15:26:36 +0200 Subject: [PATCH 4/5] clippy --- crates/uv-distribution/src/source/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 7ca9413529690..f651ddd821f8f 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -2911,7 +2911,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { /// For Git directories, we check them out into the cache, so we need to avoid workspace /// discovery that goes outside the cache. - fn stop_discovery_at(&self, source: &BuildableSource<'_>, source_root: &Path) -> Option<&Path> { + fn stop_discovery_at<'path>( + source: &BuildableSource<'_>, + source_root: &'path Path, + ) -> Option<&'path Path> { if matches!( source, BuildableSource::Dist(SourceDist::GitDirectory(_)) @@ -3010,7 +3013,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source_root.to_path_buf() }; - let stop_discovery_at = self.stop_discovery_at(source, source_root): + let stop_discovery_at = Self::stop_discovery_at(source, source_root); let build_key = BuildKey { base_python: base_python.into_boxed_path(), @@ -3163,7 +3166,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source_root.to_path_buf() }; - let stop_discovery_at = self.stop_discovery_at(source, source_root); + let stop_discovery_at = Self::stop_discovery_at(source, source_root); // Set up the builder. let mut builder = self From ba295deb62df927b79e6a1603e0ec63415eee037 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 11 Jun 2026 15:33:40 +0200 Subject: [PATCH 5/5] Avoid static metadata path in test Co-authored-by: Tomasz Kramkowski --- crates/uv/tests/pip_install/pip_install.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/uv/tests/pip_install/pip_install.rs b/crates/uv/tests/pip_install/pip_install.rs index ba82c856d013f..ae08384d021b2 100644 --- a/crates/uv/tests/pip_install/pip_install.rs +++ b/crates/uv/tests/pip_install/pip_install.rs @@ -2420,12 +2420,15 @@ fn install_git_workspace_build_requirement() -> Result<()> { .write_str(indoc! {r#" [project] name = "project" - version = "0.1.0" + dynamic = ["version"] requires-python = ">=3.12" [build-system] requires = ["hatchling", "uv-test-stop-discovery-at-build-dependency"] build-backend = "hatchling.build" + + [tool.hatch.version] + path = "src/project/__init__.py" "#})?; repository .child("packages/project/src/project/__init__.py")