mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user