@@ -213,6 +213,9 @@ pub struct Project {
213
213
pub version : Option < Version > ,
214
214
/// The Python version requirements of the project
215
215
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 > > ,
216
219
}
217
220
218
221
/// The `[build-system]` section of a pyproject.toml as specified in PEP 517.
@@ -327,6 +330,10 @@ impl Pep517Backend {
327
330
}
328
331
}
329
332
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
+
330
337
/// Uses an [`Arc`] internally, clone freely.
331
338
#[ derive( Debug , Default , Clone ) ]
332
339
pub struct SourceBuildContext {
@@ -347,6 +354,8 @@ pub struct SourceBuild {
347
354
config_settings : ConfigSettings ,
348
355
/// If performing a PEP 517 build, the backend to use.
349
356
pep517_backend : Option < Pep517Backend > ,
357
+ /// The PEP 621 project metadata, if any.
358
+ project : Option < Project > ,
350
359
/// The virtual environment in which to build the source distribution.
351
360
venv : PythonEnvironment ,
352
361
/// Populated if `prepare_metadata_for_build_wheel` was called.
@@ -399,8 +408,9 @@ impl SourceBuild {
399
408
let default_backend: Pep517Backend = DEFAULT_BACKEND . clone ( ) ;
400
409
401
410
// 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) ?;
404
414
405
415
// Create a virtual environment, or install into the shared environment if requested.
406
416
let venv = match build_isolation {
@@ -487,6 +497,7 @@ impl SourceBuild {
487
497
temp_dir,
488
498
source_tree,
489
499
pep517_backend,
500
+ project,
490
501
venv,
491
502
build_kind,
492
503
config_settings,
@@ -542,17 +553,18 @@ impl SourceBuild {
542
553
} )
543
554
}
544
555
556
+ /// Extract the PEP 517 backend from the `pyproject.toml` or `setup.py` file.
545
557
fn get_pep517_backend (
546
- setup_py : SetupPyStrategy ,
547
558
source_tree : & Path ,
559
+ setup_py : SetupPyStrategy ,
548
560
default_backend : & Pep517Backend ,
549
- ) -> Result < Option < Pep517Backend > , Box < Error > > {
561
+ ) -> Result < ( Option < Pep517Backend > , Option < Project > ) , Box < Error > > {
550
562
match fs:: read_to_string ( source_tree. join ( "pyproject.toml" ) ) {
551
563
Ok ( toml) => {
552
564
let pyproject_toml: PyProjectToml =
553
565
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 {
556
568
// If `build-backend` is missing, inject the legacy setuptools backend, but
557
569
// retain the `requires`, to match `pip` and `build`. Note that while PEP 517
558
570
// says that in this case we "should revert to the legacy behaviour of running
@@ -565,12 +577,13 @@ impl SourceBuild {
565
577
. unwrap_or_else ( || "setuptools.build_meta:__legacy__" . to_string ( ) ) ,
566
578
backend_path : build_system. backend_path ,
567
579
requirements : build_system. requires ,
568
- } ) )
580
+ }
569
581
} else {
570
582
// If a `pyproject.toml` is present, but `[build-system]` is missing, proceed with
571
583
// 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 ) )
574
587
}
575
588
Err ( err) if err. kind ( ) == io:: ErrorKind :: NotFound => {
576
589
// We require either a `pyproject.toml` or a `setup.py` file at the top level.
@@ -587,8 +600,8 @@ impl SourceBuild {
587
600
// 517 builds the default in the future.
588
601
// See: https://github.com/pypa/pip/issues/9175.
589
602
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 ) ) ,
592
605
}
593
606
}
594
607
Err ( err) => Err ( Box :: new ( err. into ( ) ) ) ,
@@ -607,6 +620,31 @@ impl SourceBuild {
607
620
return Ok ( Some ( metadata_dir. clone ( ) ) ) ;
608
621
}
609
622
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
+
610
648
let metadata_directory = self . temp_dir . path ( ) . join ( "metadata_directory" ) ;
611
649
fs:: create_dir ( & metadata_directory) ?;
612
650
0 commit comments