1414import argparse
1515import collections
1616import contextlib
17+ import copy
1718import difflib
1819import functools
1920import importlib .machinery
4243else :
4344 import tomllib
4445
46+ if sys .version_info < (3 , 8 ):
47+ import importlib_metadata
48+ else :
49+ import importlib .metadata as importlib_metadata
50+
51+ import packaging .requirements
4552import packaging .version
4653import pyproject_metadata
4754
@@ -132,6 +139,8 @@ def _init_colors() -> Dict[str, str]:
132139_EXTENSION_SUFFIX_REGEX = re .compile (r'^\.(?:(?P<abi>[^.]+)\.)?(?:so|pyd|dll)$' )
133140assert all (re .match (_EXTENSION_SUFFIX_REGEX , x ) for x in _EXTENSION_SUFFIXES )
134141
142+ _REQUIREMENT_NAME_REGEX = re .compile (r'^(?P<name>[A-Za-z0-9][A-Za-z0-9-_.]+)' )
143+
135144
136145# Maps wheel installation paths to Meson installation path placeholders.
137146# See https://docs.python.org/3/library/sysconfig.html#installation-paths
@@ -222,12 +231,13 @@ def __init__(
222231 source_dir : pathlib .Path ,
223232 build_dir : pathlib .Path ,
224233 sources : Dict [str , Dict [str , Any ]],
234+ build_time_pins_templates : List [str ],
225235 ) -> None :
226236 self ._project = project
227237 self ._source_dir = source_dir
228238 self ._build_dir = build_dir
229239 self ._sources = sources
230-
240+ self . _build_time_pins = build_time_pins_templates
231241 self ._libs_build_dir = self ._build_dir / 'mesonpy-wheel-libs'
232242
233243 @cached_property
@@ -472,8 +482,12 @@ def _install_path(
472482 wheel_file .write (origin , location )
473483
474484 def _wheel_write_metadata (self , whl : mesonpy ._wheelfile .WheelFile ) -> None :
485+ # copute dynamic dependencies
486+ metadata = copy .copy (self ._project .metadata )
487+ metadata .dependencies = _compute_build_time_dependencies (metadata .dependencies , self ._build_time_pins )
488+
475489 # add metadata
476- whl .writestr (f'{ self .distinfo_dir } /METADATA' , bytes (self . _project . metadata .as_rfc822 ()))
490+ whl .writestr (f'{ self .distinfo_dir } /METADATA' , bytes (metadata .as_rfc822 ()))
477491 whl .writestr (f'{ self .distinfo_dir } /WHEEL' , self .wheel )
478492 if self .entrypoints_txt :
479493 whl .writestr (f'{ self .distinfo_dir } /entry_points.txt' , self .entrypoints_txt )
@@ -573,7 +587,9 @@ def _strings(value: Any, name: str) -> List[str]:
573587 scheme = _table ({
574588 'args' : _table ({
575589 name : _strings for name in _MESON_ARGS_KEYS
576- })
590+ }),
591+ 'dependencies' : _strings ,
592+ 'build-time-pins' : _strings ,
577593 })
578594
579595 table = pyproject .get ('tool' , {}).get ('meson-python' , {})
@@ -622,6 +638,7 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None:
622638 """Validate package metadata."""
623639
624640 allowed_dynamic_fields = [
641+ 'dependencies' ,
625642 'version' ,
626643 ]
627644
@@ -638,9 +655,36 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None:
638655 raise ConfigError (f'building with Python { platform .python_version ()} , version { metadata .requires_python } required' )
639656
640657
658+ def _compute_build_time_dependencies (
659+ dependencies : List [packaging .requirements .Requirement ],
660+ pins : List [str ]) -> List [packaging .requirements .Requirement ]:
661+ for template in pins :
662+ match = _REQUIREMENT_NAME_REGEX .match (template )
663+ if not match :
664+ raise ConfigError (f'invalid requirement format in "build-time-pins": { template !r} ' )
665+ name = match .group (1 )
666+ try :
667+ version = packaging .version .parse (importlib_metadata .version (name ))
668+ except importlib_metadata .PackageNotFoundError as exc :
669+ raise ConfigError (f'package "{ name } " specified in "build-time-pins" not found: { template !r} ' ) from exc
670+ pin = packaging .requirements .Requirement (template .format (v = version ))
671+ if pin .marker :
672+ raise ConfigError (f'requirements in "build-time-pins" cannot contain markers: { template !r} ' )
673+ if pin .extras :
674+ raise ConfigError (f'requirements in "build-time-pins" cannot contain extras: { template !r} ' )
675+ added = False
676+ for d in dependencies :
677+ if d .name == name :
678+ d .specifier = d .specifier & pin .specifier
679+ added = True
680+ if not added :
681+ dependencies .append (pin )
682+ return dependencies
683+
684+
641685class Project ():
642686 """Meson project wrapper to generate Python artifacts."""
643- def __init__ (
687+ def __init__ ( # noqa: C901
644688 self ,
645689 source_dir : Path ,
646690 working_dir : Path ,
@@ -657,6 +701,7 @@ def __init__(
657701 self ._meson_cross_file = self ._build_dir / 'meson-python-cross-file.ini'
658702 self ._meson_args : MesonArgs = collections .defaultdict (list )
659703 self ._env = os .environ .copy ()
704+ self ._build_time_pins = []
660705
661706 _check_meson_version ()
662707
@@ -743,6 +788,13 @@ def __init__(
743788 if 'version' in self ._metadata .dynamic :
744789 self ._metadata .version = packaging .version .Version (self ._meson_version )
745790
791+ # set base dependencie if dynamic
792+ if 'dependencies' in self ._metadata .dynamic :
793+ dependencies = [packaging .requirements .Requirement (d ) for d in pyproject_config .get ('dependencies' , [])]
794+ self ._metadata .dependencies = dependencies
795+ self ._metadata .dynamic .remove ('dependencies' )
796+ self ._build_time_pins = pyproject_config .get ('build-time-pins' , [])
797+
746798 def _run (self , cmd : Sequence [str ]) -> None :
747799 """Invoke a subprocess."""
748800 print ('{cyan}{bold}+ {}{reset}' .format (' ' .join (cmd ), ** _STYLES ))
@@ -786,6 +838,7 @@ def _wheel_builder(self) -> _WheelBuilder:
786838 self ._source_dir ,
787839 self ._build_dir ,
788840 self ._install_plan ,
841+ self ._build_time_pins ,
789842 )
790843
791844 def build_commands (self , install_dir : Optional [pathlib .Path ] = None ) -> Sequence [Sequence [str ]]:
0 commit comments