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
@@ -130,6 +137,8 @@ def _init_colors() -> Dict[str, str]:
130137_EXTENSION_SUFFIX_REGEX = re .compile (r'^\.(?:(?P<abi>[^.]+)\.)?(?:so|pyd|dll)$' )
131138assert all (re .match (_EXTENSION_SUFFIX_REGEX , x ) for x in _EXTENSION_SUFFIXES )
132139
140+ _REQUIREMENT_NAME_REGEX = re .compile (r'^(?P<name>[A-Za-z0-9][A-Za-z0-9-_.]+)' )
141+
133142
134143# Maps wheel installation paths to Meson installation path placeholders.
135144# See https://docs.python.org/3/library/sysconfig.html#installation-paths
@@ -220,12 +229,13 @@ def __init__(
220229 source_dir : pathlib .Path ,
221230 build_dir : pathlib .Path ,
222231 sources : Dict [str , Dict [str , Any ]],
232+ build_time_pins_templates : List [str ],
223233 ) -> None :
224234 self ._project = project
225235 self ._source_dir = source_dir
226236 self ._build_dir = build_dir
227237 self ._sources = sources
228-
238+ self . _build_time_pins = build_time_pins_templates
229239 self ._libs_build_dir = self ._build_dir / 'mesonpy-wheel-libs'
230240
231241 @cached_property
@@ -470,8 +480,12 @@ def _install_path(
470480 wheel_file .write (origin , location )
471481
472482 def _wheel_write_metadata (self , whl : mesonpy ._wheelfile .WheelFile ) -> None :
483+ # copute dynamic dependencies
484+ metadata = copy .copy (self ._project .metadata )
485+ metadata .dependencies = _compute_build_time_dependencies (metadata .dependencies , self ._build_time_pins )
486+
473487 # add metadata
474- whl .writestr (f'{ self .distinfo_dir } /METADATA' , bytes (self . _project . metadata .as_rfc822 ()))
488+ whl .writestr (f'{ self .distinfo_dir } /METADATA' , bytes (metadata .as_rfc822 ()))
475489 whl .writestr (f'{ self .distinfo_dir } /WHEEL' , self .wheel )
476490 if self .entrypoints_txt :
477491 whl .writestr (f'{ self .distinfo_dir } /entry_points.txt' , self .entrypoints_txt )
@@ -571,7 +585,9 @@ def _strings(value: Any, name: str) -> List[str]:
571585 scheme = _table ({
572586 'args' : _table ({
573587 name : _strings for name in _MESON_ARGS_KEYS
574- })
588+ }),
589+ 'dependencies' : _strings ,
590+ 'build-time-pins' : _strings ,
575591 })
576592
577593 table = pyproject .get ('tool' , {}).get ('meson-python' , {})
@@ -620,6 +636,7 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None:
620636 """Validate package metadata."""
621637
622638 allowed_dynamic_fields = [
639+ 'dependencies' ,
623640 'version' ,
624641 ]
625642
@@ -636,9 +653,36 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None:
636653 raise ConfigError (f'building with Python { platform .python_version ()} , version { metadata .requires_python } required' )
637654
638655
656+ def _compute_build_time_dependencies (
657+ dependencies : List [packaging .requirements .Requirement ],
658+ pins : List [str ]) -> List [packaging .requirements .Requirement ]:
659+ for template in pins :
660+ match = _REQUIREMENT_NAME_REGEX .match (template )
661+ if not match :
662+ raise ConfigError (f'invalid requirement format in "build-time-pins": { template !r} ' )
663+ name = match .group (1 )
664+ try :
665+ version = packaging .version .parse (importlib_metadata .version (name ))
666+ except importlib_metadata .PackageNotFoundError as exc :
667+ raise ConfigError (f'package "{ name } " specified in "build-time-pins" not found: { template !r} ' ) from exc
668+ pin = packaging .requirements .Requirement (template .format (v = version ))
669+ if pin .marker :
670+ raise ConfigError (f'requirements in "build-time-pins" cannot contain markers: { template !r} ' )
671+ if pin .extras :
672+ raise ConfigError (f'requirements in "build-time-pins" cannot contain extras: { template !r} ' )
673+ added = False
674+ for d in dependencies :
675+ if d .name == name :
676+ d .specifier = d .specifier & pin .specifier
677+ added = True
678+ if not added :
679+ dependencies .append (pin )
680+ return dependencies
681+
682+
639683class Project ():
640684 """Meson project wrapper to generate Python artifacts."""
641- def __init__ (
685+ def __init__ ( # noqa: C901
642686 self ,
643687 source_dir : Path ,
644688 working_dir : Path ,
@@ -655,6 +699,7 @@ def __init__(
655699 self ._meson_cross_file = self ._build_dir / 'meson-python-cross-file.ini'
656700 self ._meson_args : MesonArgs = collections .defaultdict (list )
657701 self ._env = os .environ .copy ()
702+ self ._build_time_pins = []
658703
659704 _check_meson_version ()
660705
@@ -741,6 +786,13 @@ def __init__(
741786 if 'version' in self ._metadata .dynamic :
742787 self ._metadata .version = packaging .version .Version (self ._meson_version )
743788
789+ # set base dependencie if dynamic
790+ if 'dependencies' in self ._metadata .dynamic :
791+ dependencies = [packaging .requirements .Requirement (d ) for d in pyproject_config .get ('dependencies' , [])]
792+ self ._metadata .dependencies = dependencies
793+ self ._metadata .dynamic .remove ('dependencies' )
794+ self ._build_time_pins = pyproject_config .get ('build-time-pins' , [])
795+
744796 def _run (self , cmd : Sequence [str ]) -> None :
745797 """Invoke a subprocess."""
746798 print ('{cyan}{bold}+ {}{reset}' .format (' ' .join (cmd ), ** _STYLES ))
@@ -784,6 +836,7 @@ def _wheel_builder(self) -> _WheelBuilder:
784836 self ._source_dir ,
785837 self ._build_dir ,
786838 self ._install_plan ,
839+ self ._build_time_pins ,
787840 )
788841
789842 def build_commands (self , install_dir : Optional [pathlib .Path ] = None ) -> Sequence [Sequence [str ]]:
0 commit comments