Skip to content

Commit 3d3cdef

Browse files
committed
Avoid dynamic
1 parent 71428f7 commit 3d3cdef

File tree

5 files changed

+101
-11
lines changed

5 files changed

+101
-11
lines changed

crates/uv-build/src/lib.rs

+49-11
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ pub struct Project {
213213
pub version: Option<Version>,
214214
/// The Python version requirements of the project
215215
pub requires_python: Option<VersionSpecifiers>,
216+
/// Specifies which fields listed by PEP 621 were intentionally unspecified so another tool
217+
/// can/will provide such metadata dynamically.
218+
pub dynamic: Option<Vec<String>>,
216219
}
217220

218221
/// The `[build-system]` section of a pyproject.toml as specified in PEP 517.
@@ -327,6 +330,10 @@ impl Pep517Backend {
327330
}
328331
}
329332

333+
/// A list of fields that are intentionally unspecified in the `pyproject.toml` file.
334+
#[derive(Debug, Clone, PartialEq, Eq)]
335+
pub struct Dynamic(Vec<String>);
336+
330337
/// Uses an [`Arc`] internally, clone freely.
331338
#[derive(Debug, Default, Clone)]
332339
pub struct SourceBuildContext {
@@ -347,6 +354,8 @@ pub struct SourceBuild {
347354
config_settings: ConfigSettings,
348355
/// If performing a PEP 517 build, the backend to use.
349356
pep517_backend: Option<Pep517Backend>,
357+
/// The PEP 621 project metadata, if any.
358+
project: Option<Project>,
350359
/// The virtual environment in which to build the source distribution.
351360
venv: PythonEnvironment,
352361
/// Populated if `prepare_metadata_for_build_wheel` was called.
@@ -399,8 +408,9 @@ impl SourceBuild {
399408
let default_backend: Pep517Backend = DEFAULT_BACKEND.clone();
400409

401410
// Check if we have a PEP 517 build backend.
402-
let pep517_backend = Self::get_pep517_backend(setup_py, &source_tree, &default_backend)
403-
.map_err(|err| *err)?;
411+
let (pep517_backend, project) =
412+
Self::get_pep517_backend(&source_tree, setup_py, &default_backend)
413+
.map_err(|err| *err)?;
404414

405415
// Create a virtual environment, or install into the shared environment if requested.
406416
let venv = match build_isolation {
@@ -487,6 +497,7 @@ impl SourceBuild {
487497
temp_dir,
488498
source_tree,
489499
pep517_backend,
500+
project,
490501
venv,
491502
build_kind,
492503
config_settings,
@@ -542,17 +553,18 @@ impl SourceBuild {
542553
})
543554
}
544555

556+
/// Extract the PEP 517 backend from the `pyproject.toml` or `setup.py` file.
545557
fn get_pep517_backend(
546-
setup_py: SetupPyStrategy,
547558
source_tree: &Path,
559+
setup_py: SetupPyStrategy,
548560
default_backend: &Pep517Backend,
549-
) -> Result<Option<Pep517Backend>, Box<Error>> {
561+
) -> Result<(Option<Pep517Backend>, Option<Project>), Box<Error>> {
550562
match fs::read_to_string(source_tree.join("pyproject.toml")) {
551563
Ok(toml) => {
552564
let pyproject_toml: PyProjectToml =
553565
toml::from_str(&toml).map_err(Error::InvalidPyprojectToml)?;
554-
if let Some(build_system) = pyproject_toml.build_system {
555-
Ok(Some(Pep517Backend {
566+
let backend = if let Some(build_system) = pyproject_toml.build_system {
567+
Pep517Backend {
556568
// If `build-backend` is missing, inject the legacy setuptools backend, but
557569
// retain the `requires`, to match `pip` and `build`. Note that while PEP 517
558570
// says that in this case we "should revert to the legacy behaviour of running
@@ -565,12 +577,13 @@ impl SourceBuild {
565577
.unwrap_or_else(|| "setuptools.build_meta:__legacy__".to_string()),
566578
backend_path: build_system.backend_path,
567579
requirements: build_system.requires,
568-
}))
580+
}
569581
} else {
570582
// If a `pyproject.toml` is present, but `[build-system]` is missing, proceed with
571583
// a PEP 517 build using the default backend, to match `pip` and `build`.
572-
Ok(Some(default_backend.clone()))
573-
}
584+
default_backend.clone()
585+
};
586+
Ok((Some(backend), pyproject_toml.project))
574587
}
575588
Err(err) if err.kind() == io::ErrorKind::NotFound => {
576589
// We require either a `pyproject.toml` or a `setup.py` file at the top level.
@@ -587,8 +600,8 @@ impl SourceBuild {
587600
// 517 builds the default in the future.
588601
// See: https://github.com/pypa/pip/issues/9175.
589602
match setup_py {
590-
SetupPyStrategy::Pep517 => Ok(Some(default_backend.clone())),
591-
SetupPyStrategy::Setuptools => Ok(None),
603+
SetupPyStrategy::Pep517 => Ok((Some(default_backend.clone()), None)),
604+
SetupPyStrategy::Setuptools => Ok((None, None)),
592605
}
593606
}
594607
Err(err) => Err(Box::new(err.into())),
@@ -607,6 +620,31 @@ impl SourceBuild {
607620
return Ok(Some(metadata_dir.clone()));
608621
}
609622

623+
// Hatch allows for highly dynamic customization of metadata via hooks. In such cases, Hatch
624+
// can't upload the PEP 517 contract, in that the metadata Hatch would return by
625+
// `prepare_metadata_for_build_wheel` isn't guaranteed to match that of the built wheel.
626+
//
627+
// Hatch disables `prepare_metadata_for_build_wheel` entirely for pip. We'll instead disable
628+
// it on our end when metadata is defined as "dynamic" in the pyproject.toml, which should
629+
// allow us to leverage the hook in _most_ cases while still avoiding incorrect metadata for
630+
// the remaining cases.
631+
//
632+
// See: https://github.com/astral-sh/uv/issues/2130
633+
if pep517_backend.backend == "hatchling.build" {
634+
if self
635+
.project
636+
.as_ref()
637+
.and_then(|project| project.dynamic.as_ref())
638+
.is_some_and(|dynamic| {
639+
dynamic
640+
.iter()
641+
.any(|field| field == "dependencies" || field == "optional-dependencies")
642+
})
643+
{
644+
return Ok(None);
645+
}
646+
}
647+
610648
let metadata_directory = self.temp_dir.path().join("metadata_directory");
611649
fs::create_dir(&metadata_directory)?;
612650

crates/uv/tests/pip_compile.rs

+36
Original file line numberDiff line numberDiff line change
@@ -6046,3 +6046,39 @@ fn unnamed_https_requirement() -> Result<()> {
60466046

60476047
Ok(())
60486048
}
6049+
6050+
/// Detect the package name from metadata sources from local directories.
6051+
#[test]
6052+
fn dynamic_dependencies() -> Result<()> {
6053+
let context = TestContext::new("3.12");
6054+
let requirements_in = context.temp_dir.child("requirements.in");
6055+
requirements_in.write_str("hatch-dynamic @ ../../scripts/packages/hatch_dynamic")?;
6056+
6057+
let filter_path = regex::escape(&requirements_in.user_display().to_string());
6058+
let filters: Vec<_> = [(filter_path.as_str(), "requirements.in")]
6059+
.into_iter()
6060+
.chain(INSTA_FILTERS.to_vec())
6061+
.collect();
6062+
6063+
uv_snapshot!(filters, context.compile()
6064+
.arg(requirements_in.path())
6065+
.current_dir(current_dir()?), @r###"
6066+
success: true
6067+
exit_code: 0
6068+
----- stdout -----
6069+
# This file was autogenerated by uv via the following command:
6070+
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
6071+
anyio==4.3.0
6072+
# via hatch-dynamic
6073+
hatch-dynamic @ ../../scripts/packages/hatch_dynamic
6074+
idna==3.6
6075+
# via anyio
6076+
sniffio==1.3.1
6077+
# via anyio
6078+
6079+
----- stderr -----
6080+
Resolved 4 packages in [TIME]
6081+
"###);
6082+
6083+
Ok(())
6084+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
2+
3+
4+
class LiteraryBuildHook(BuildHookInterface):
5+
def initialize(self, version, build_data):
6+
build_data['dependencies'].append('anyio')

scripts/packages/hatch_dynamic/hatch_dynamic/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "hatch-dynamic"
7+
version = "1.0.0"
8+
dynamic = ["dependencies"]
9+
10+
[tool.hatch.build.targets.wheel.hooks.custom]

0 commit comments

Comments
 (0)