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..f651ddd821f8f 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -2909,6 +2909,23 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Ok(hashes) } + /// 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<'path>( + source: &BuildableSource<'_>, + source_root: &'path Path, + ) -> Option<&'path 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 +3007,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(), @@ -3015,7 +3040,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .setup_build( source_root, subdirectory, - source_root, + &install_path, + stop_discovery_at, Some(&source.to_string()), source.as_dist(), &no_sources, @@ -3134,13 +3160,22 @@ 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); + // 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..ae08384d021b2 100644 --- a/crates/uv/tests/pip_install/pip_install.rs +++ b/crates/uv/tests/pip_install/pip_install.rs @@ -2382,6 +2382,112 @@ 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" + 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") + .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")]