diff --git a/crates/uv-build/src/lib.rs b/crates/uv-build/src/lib.rs index c61108054dab6..1de7a5eace127 100644 --- a/crates/uv-build/src/lib.rs +++ b/crates/uv-build/src/lib.rs @@ -213,6 +213,9 @@ pub struct Project { pub version: Option, /// The Python version requirements of the project pub requires_python: Option, + /// Specifies which fields listed by PEP 621 were intentionally unspecified so another tool + /// can/will provide such metadata dynamically. + pub dynamic: Option>, } /// The `[build-system]` section of a pyproject.toml as specified in PEP 517. @@ -347,6 +350,8 @@ pub struct SourceBuild { config_settings: ConfigSettings, /// If performing a PEP 517 build, the backend to use. pep517_backend: Option, + /// The PEP 621 project metadata, if any. + project: Option, /// The virtual environment in which to build the source distribution. venv: PythonEnvironment, /// Populated if `prepare_metadata_for_build_wheel` was called. @@ -399,8 +404,9 @@ impl SourceBuild { let default_backend: Pep517Backend = DEFAULT_BACKEND.clone(); // Check if we have a PEP 517 build backend. - let pep517_backend = Self::get_pep517_backend(setup_py, &source_tree, &default_backend) - .map_err(|err| *err)?; + let (pep517_backend, project) = + Self::get_pep517_backend(&source_tree, setup_py, &default_backend) + .map_err(|err| *err)?; // Create a virtual environment, or install into the shared environment if requested. let venv = match build_isolation { @@ -487,6 +493,7 @@ impl SourceBuild { temp_dir, source_tree, pep517_backend, + project, venv, build_kind, config_settings, @@ -542,17 +549,18 @@ impl SourceBuild { }) } + /// Extract the PEP 517 backend from the `pyproject.toml` or `setup.py` file. fn get_pep517_backend( - setup_py: SetupPyStrategy, source_tree: &Path, + setup_py: SetupPyStrategy, default_backend: &Pep517Backend, - ) -> Result, Box> { + ) -> Result<(Option, Option), Box> { match fs::read_to_string(source_tree.join("pyproject.toml")) { Ok(toml) => { let pyproject_toml: PyProjectToml = toml::from_str(&toml).map_err(Error::InvalidPyprojectToml)?; - if let Some(build_system) = pyproject_toml.build_system { - Ok(Some(Pep517Backend { + let backend = if let Some(build_system) = pyproject_toml.build_system { + Pep517Backend { // If `build-backend` is missing, inject the legacy setuptools backend, but // retain the `requires`, to match `pip` and `build`. Note that while PEP 517 // says that in this case we "should revert to the legacy behaviour of running @@ -565,12 +573,13 @@ impl SourceBuild { .unwrap_or_else(|| "setuptools.build_meta:__legacy__".to_string()), backend_path: build_system.backend_path, requirements: build_system.requires, - })) + } } else { // If a `pyproject.toml` is present, but `[build-system]` is missing, proceed with // a PEP 517 build using the default backend, to match `pip` and `build`. - Ok(Some(default_backend.clone())) - } + default_backend.clone() + }; + Ok((Some(backend), pyproject_toml.project)) } Err(err) if err.kind() == io::ErrorKind::NotFound => { // We require either a `pyproject.toml` or a `setup.py` file at the top level. @@ -587,8 +596,8 @@ impl SourceBuild { // 517 builds the default in the future. // See: https://github.com/pypa/pip/issues/9175. match setup_py { - SetupPyStrategy::Pep517 => Ok(Some(default_backend.clone())), - SetupPyStrategy::Setuptools => Ok(None), + SetupPyStrategy::Pep517 => Ok((Some(default_backend.clone()), None)), + SetupPyStrategy::Setuptools => Ok((None, None)), } } Err(err) => Err(Box::new(err.into())), @@ -607,6 +616,31 @@ impl SourceBuild { return Ok(Some(metadata_dir.clone())); } + // Hatch allows for highly dynamic customization of metadata via hooks. In such cases, Hatch + // can't upload the PEP 517 contract, in that the metadata Hatch would return by + // `prepare_metadata_for_build_wheel` isn't guaranteed to match that of the built wheel. + // + // Hatch disables `prepare_metadata_for_build_wheel` entirely for pip. We'll instead disable + // it on our end when metadata is defined as "dynamic" in the pyproject.toml, which should + // allow us to leverage the hook in _most_ cases while still avoiding incorrect metadata for + // the remaining cases. + // + // See: https://github.com/astral-sh/uv/issues/2130 + if pep517_backend.backend == "hatchling.build" { + if self + .project + .as_ref() + .and_then(|project| project.dynamic.as_ref()) + .is_some_and(|dynamic| { + dynamic + .iter() + .any(|field| field == "dependencies" || field == "optional-dependencies") + }) + { + return Ok(None); + } + } + let metadata_directory = self.temp_dir.path().join("metadata_directory"); fs::create_dir(&metadata_directory)?; diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index fafc8066c6471..01a15d2f6911f 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -6046,3 +6046,39 @@ fn unnamed_https_requirement() -> Result<()> { Ok(()) } + +/// Detect the package name from metadata sources from local directories. +#[test] +fn dynamic_dependencies() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("hatch-dynamic @ ../../scripts/packages/hatch_dynamic")?; + + let filter_path = regex::escape(&requirements_in.user_display().to_string()); + let filters: Vec<_> = [(filter_path.as_str(), "requirements.in")] + .into_iter() + .chain(INSTA_FILTERS.to_vec()) + .collect(); + + uv_snapshot!(filters, context.compile() + .arg(requirements_in.path()) + .current_dir(current_dir()?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in + anyio==4.3.0 + # via hatch-dynamic + hatch-dynamic @ ../../scripts/packages/hatch_dynamic + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + Ok(()) +} diff --git a/scripts/packages/hatch_dynamic/hatch_build.py b/scripts/packages/hatch_dynamic/hatch_build.py new file mode 100644 index 0000000000000..3d2022b99579f --- /dev/null +++ b/scripts/packages/hatch_dynamic/hatch_build.py @@ -0,0 +1,6 @@ +from hatchling.builders.hooks.plugin.interface import BuildHookInterface + + +class LiteraryBuildHook(BuildHookInterface): + def initialize(self, version, build_data): + build_data['dependencies'].append('anyio') diff --git a/scripts/packages/hatch_dynamic/hatch_dynamic/__init__.py b/scripts/packages/hatch_dynamic/hatch_dynamic/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/scripts/packages/hatch_dynamic/pyproject.toml b/scripts/packages/hatch_dynamic/pyproject.toml new file mode 100644 index 0000000000000..4b0e9a312b061 --- /dev/null +++ b/scripts/packages/hatch_dynamic/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "hatch-dynamic" +version = "1.0.0" +dynamic = ["dependencies"] + +[tool.hatch.build.targets.wheel.hooks.custom]