Allow uv in PEP 517 build hooks (#19879)

## Summary

Prior to this change, PEP 517 build hooks that invoked uv failed while
building a wheel from a source distribution. `uv build` extracts the
generated source distribution into uv's cache, so the nested uv
invocation treated the extracted project as an unsupported project
inside the configured cache and exited before the hook could complete.

This records the exact PEP 517 source tree in a hidden internal
environment variable and exempts nested uv invocations only when their
project directory is within that uv-managed tree. Other projects inside
the cache remain rejected, including paths that require canonicalization
to detect.

The regression coverage uses a Hatchling custom build hook that invokes
`uv export`, matching the reported failure while building both the
source distribution and wheel.

Closes https://github.com/astral-sh/uv/issues/19878.

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
Charlie Marsh
2026-06-16 11:48:58 -04:00
committed by GitHub
parent 7c9685ce54
commit c0b8732850
4 changed files with 97 additions and 16 deletions
+1
View File
@@ -1229,6 +1229,7 @@ impl PythonRunner {
.args(["-c", script])
.current_dir(source_tree.simplified())
.envs(environment_variables)
.env(EnvVars::UV_INTERNAL__BUILD_DIR, source_tree)
.env(EnvVars::PATH, modified_path)
.env(EnvVars::VIRTUAL_ENV, venv.root())
// NOTE: it would be nice to get colored output from build backends,
+5
View File
@@ -658,6 +658,11 @@ impl EnvVars {
#[attr_added_in("0.2.0")]
pub const UV_INTERNAL__PARENT_INTERPRETER: &'static str = "UV_INTERNAL__PARENT_INTERPRETER";
/// Used to identify the source tree when invoking PEP 517 build hooks.
#[attr_hidden]
#[attr_added_in("next release")]
pub const UV_INTERNAL__BUILD_DIR: &'static str = "UV_INTERNAL__BUILD_DIR";
/// Used to force showing the derivation tree during resolver error reporting.
#[attr_hidden]
#[attr_added_in("0.3.0")]
+30 -15
View File
@@ -538,21 +538,36 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
// the settings that go into the cache constructor, but the check happens before the first
// workspace discovery that's used beyond settings discovery.
let cache_dir = std::path::absolute(cache.root())?;
if project_dir.starts_with(&cache_dir) {
bail!(
"The project directory `{}` is inside the cache directory `{}`",
project_dir.user_display(),
cache_dir.user_display()
);
} else if let Ok(cache_dir) = fs_err::canonicalize(&cache_dir)
&& let Ok(project_dir) = fs_err::canonicalize(&*project_dir)
&& project_dir.starts_with(&cache_dir)
{
bail!(
"The project directory `{}` is inside the cache directory `{}`",
project_dir.user_display(),
cache_dir.user_display()
);
// PEP 517 hooks run from uv-managed source trees, including source distributions extracted
// into the cache, and can invoke uv recursively.
let project_is_in_build_dir =
std::env::var_os(EnvVars::UV_INTERNAL__BUILD_DIR).is_some_and(|build_dir| {
std::path::absolute(build_dir).is_ok_and(|build_dir| {
project_dir.starts_with(&build_dir)
|| fs_err::canonicalize(&*project_dir).is_ok_and(|project_dir| {
fs_err::canonicalize(build_dir)
.is_ok_and(|build_dir| project_dir.starts_with(build_dir))
})
})
});
if !project_is_in_build_dir {
if project_dir.starts_with(&cache_dir) {
bail!(
"The project directory `{}` is inside the cache directory `{}`",
project_dir.user_display(),
cache_dir.user_display()
);
}
if let Ok(cache_dir) = fs_err::canonicalize(&cache_dir)
&& let Ok(project_dir) = fs_err::canonicalize(&*project_dir)
&& project_dir.starts_with(&cache_dir)
{
bail!(
"The project directory `{}` is inside the cache directory `{}`",
project_dir.user_display(),
cache_dir.user_display()
);
}
}
let workspace_cache = WorkspaceCache::default();
+61 -1
View File
@@ -10,7 +10,7 @@ use std::env::current_dir;
use std::path::Path;
use url::Url;
use uv_static::EnvVars;
use uv_test::{DEFAULT_PYTHON_VERSION, apply_filters, uv_snapshot};
use uv_test::{DEFAULT_PYTHON_VERSION, apply_filters, get_bin, uv_snapshot};
use wiremock::{
Mock, MockServer, ResponseTemplate,
matchers::{method, path as url_path},
@@ -148,6 +148,66 @@ fn build_basic() -> Result<()> {
Ok(())
}
/// Build hooks can invoke uv while building a wheel from an extracted source distribution.
/// Regression test for <https://github.com/astral-sh/uv/issues/19878>.
#[test]
fn build_hook_invokes_uv() -> Result<()> {
let context = uv_test::test_context!("3.12");
let project = context.temp_dir.child("project");
project.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.hooks.custom]
path = "hatch_build.py"
"#})?;
project.child("src/project/__init__.py").touch()?;
project.child("hatch_build.py").write_str(&formatdoc! {r#"
import subprocess
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
class CustomBuildHook(BuildHookInterface):
def initialize(self, version, build_data):
subprocess.run(
[
{uv:?},
"export",
"--quiet",
"--no-dev",
"--no-editable",
"--no-emit-project",
"--output-file",
"requirements.txt",
],
check=True,
cwd=self.root,
)
"#, uv = get_bin!().display() })?;
uv_snapshot!(context.filters(), context.build().current_dir(&project), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Building source distribution...
Building wheel from source distribution...
Successfully built dist/project-0.1.0.tar.gz
Successfully built dist/project-0.1.0-py3-none-any.whl
");
Ok(())
}
/// A source distribution must include an in-tree build backend referenced by `backend-path`.
/// Regression test for <https://github.com/astral-sh/uv/issues/19771>.
#[test]