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
@@ -125,6 +132,8 @@ def _init_colors() -> Dict[str, str]:
125132_EXTENSION_SUFFIX_REGEX = re .compile (r'^\.(?:(?P<abi>[^.]+)\.)?(?:so|pyd|dll)$' )
126133assert all (re .match (_EXTENSION_SUFFIX_REGEX , x ) for x in _EXTENSION_SUFFIXES )
127134
135+ _REQUIREMENT_NAME_REGEX = re .compile (r'^(?P<name>[A-Za-z0-9][A-Za-z0-9-_.]+)' )
136+
128137
129138def _showwarning (
130139 message : Union [Warning , str ],
@@ -197,14 +206,15 @@ def __init__(
197206 build_dir : pathlib .Path ,
198207 sources : Dict [str , Dict [str , Any ]],
199208 copy_files : Dict [str , str ],
209+ build_time_pins_templates : List [str ],
200210 ) -> None :
201211 self ._project = project
202212 self ._source_dir = source_dir
203213 self ._install_dir = install_dir
204214 self ._build_dir = build_dir
205215 self ._sources = sources
206216 self ._copy_files = copy_files
207-
217+ self . _build_time_pins = build_time_pins_templates
208218 self ._libs_build_dir = self ._build_dir / 'mesonpy-wheel-libs'
209219
210220 @cached_property
@@ -550,8 +560,12 @@ def _install_path(
550560 wheel_file .write (origin , location )
551561
552562 def _wheel_write_metadata (self , whl : mesonpy ._wheelfile .WheelFile ) -> None :
563+ # copute dynamic dependencies
564+ metadata = copy .copy (self ._project .metadata )
565+ metadata .dependencies = _compute_build_time_dependencies (metadata .dependencies , self ._build_time_pins )
566+
553567 # add metadata
554- whl .writestr (f'{ self .distinfo_dir } /METADATA' , bytes (self . _project . metadata .as_rfc822 ()))
568+ whl .writestr (f'{ self .distinfo_dir } /METADATA' , bytes (metadata .as_rfc822 ()))
555569 whl .writestr (f'{ self .distinfo_dir } /WHEEL' , self .wheel )
556570 if self .entrypoints_txt :
557571 whl .writestr (f'{ self .distinfo_dir } /entry_points.txt' , self .entrypoints_txt )
@@ -677,7 +691,9 @@ def _strings(value: Any, name: str) -> List[str]:
677691 scheme = _table ({
678692 'args' : _table ({
679693 name : _strings for name in _MESON_ARGS_KEYS
680- })
694+ }),
695+ 'dependencies' : _strings ,
696+ 'build-time-pins' : _strings ,
681697 })
682698
683699 table = pyproject .get ('tool' , {}).get ('meson-python' , {})
@@ -726,6 +742,7 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None:
726742 """Validate package metadata."""
727743
728744 allowed_dynamic_fields = [
745+ 'dependencies' ,
729746 'version' ,
730747 ]
731748
@@ -742,9 +759,36 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None:
742759 raise ConfigError (f'building with Python { platform .python_version ()} , version { metadata .requires_python } required' )
743760
744761
762+ def _compute_build_time_dependencies (
763+ dependencies : List [packaging .requirements .Requirement ],
764+ pins : List [str ]) -> List [packaging .requirements .Requirement ]:
765+ for template in pins :
766+ match = _REQUIREMENT_NAME_REGEX .match (template )
767+ if not match :
768+ raise ConfigError (f'invalid requirement format in "build-time-pins": { template !r} ' )
769+ name = match .group (1 )
770+ try :
771+ version = packaging .version .parse (importlib_metadata .version (name ))
772+ except importlib_metadata .PackageNotFoundError as exc :
773+ raise ConfigError (f'package "{ name } " specified in "build-time-pins" not found: { template !r} ' ) from exc
774+ pin = packaging .requirements .Requirement (template .format (v = version ))
775+ if pin .marker :
776+ raise ConfigError (f'requirements in "build-time-pins" cannot contain markers: { template !r} ' )
777+ if pin .extras :
778+ raise ConfigError (f'requirements in "build-time-pins" cannot contain erxtras: { template !r} ' )
779+ added = False
780+ for d in dependencies :
781+ if d .name == name :
782+ d .specifier = d .specifier & pin .specifier
783+ added = True
784+ if not added :
785+ dependencies .append (pin )
786+ return dependencies
787+
788+
745789class Project ():
746790 """Meson project wrapper to generate Python artifacts."""
747- def __init__ (
791+ def __init__ ( # noqa: C901
748792 self ,
749793 source_dir : Path ,
750794 working_dir : Path ,
@@ -761,6 +805,7 @@ def __init__(
761805 self ._meson_cross_file = self ._build_dir / 'meson-python-cross-file.ini'
762806 self ._meson_args : MesonArgs = collections .defaultdict (list )
763807 self ._env = os .environ .copy ()
808+ self ._build_time_pins = []
764809
765810 # prepare environment
766811 self ._ninja = _env_ninja_command ()
@@ -846,6 +891,13 @@ def __init__(
846891 if 'version' in self ._metadata .dynamic :
847892 self ._metadata .version = packaging .version .Version (self ._meson_version )
848893
894+ # set base dependencie if dynamic
895+ if 'dependencies' in self ._metadata .dynamic :
896+ dependencies = [packaging .requirements .Requirement (d ) for d in pyproject_config .get ('dependencies' , [])]
897+ self ._metadata .dependencies = dependencies
898+ self ._metadata .dynamic .remove ('dependencies' )
899+ self ._build_time_pins = pyproject_config .get ('build-time-pins' , [])
900+
849901 def _run (self , cmd : Sequence [str ]) -> None :
850902 """Invoke a subprocess."""
851903 print ('{cyan}{bold}+ {}{reset}' .format (' ' .join (cmd ), ** _STYLES ))
@@ -890,6 +942,7 @@ def _wheel_builder(self) -> _WheelBuilder:
890942 self ._build_dir ,
891943 self ._install_plan ,
892944 self ._copy_files ,
945+ self ._build_time_pins ,
893946 )
894947
895948 def build_commands (self , install_dir : Optional [pathlib .Path ] = None ) -> Sequence [Sequence [str ]]:
0 commit comments