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
@@ -129,6 +136,8 @@ def _init_colors() -> Dict[str, str]:
129136_EXTENSION_SUFFIX_REGEX = re .compile (r'^\.(?:(?P<abi>[^.]+)\.)?(?:so|pyd|dll)$' )
130137assert all (re .match (_EXTENSION_SUFFIX_REGEX , x ) for x in _EXTENSION_SUFFIXES )
131138
139+ _REQUIREMENT_NAME_REGEX = re .compile (r'^(?P<name>[A-Za-z0-9][A-Za-z0-9-_.]+)' )
140+
132141
133142# Maps wheel installation paths to Meson installation path placeholders.
134143# See https://docs.python.org/3/library/sysconfig.html#installation-paths
@@ -218,12 +227,13 @@ def __init__(
218227 source_dir : pathlib .Path ,
219228 build_dir : pathlib .Path ,
220229 sources : Dict [str , Dict [str , Any ]],
230+ build_time_pins_templates : List [str ],
221231 ) -> None :
222232 self ._project = project
223233 self ._source_dir = source_dir
224234 self ._build_dir = build_dir
225235 self ._sources = sources
226-
236+ self . _build_time_pins = build_time_pins_templates
227237 self ._libs_build_dir = self ._build_dir / 'mesonpy-wheel-libs'
228238
229239 @cached_property
@@ -469,8 +479,12 @@ def _install_path(
469479 wheel_file .write (origin , location )
470480
471481 def _wheel_write_metadata (self , whl : mesonpy ._wheelfile .WheelFile ) -> None :
482+ # copute dynamic dependencies
483+ metadata = copy .copy (self ._project .metadata )
484+ metadata .dependencies = _compute_build_time_dependencies (metadata .dependencies , self ._build_time_pins )
485+
472486 # add metadata
473- whl .writestr (f'{ self .distinfo_dir } /METADATA' , bytes (self . _project . metadata .as_rfc822 ()))
487+ whl .writestr (f'{ self .distinfo_dir } /METADATA' , bytes (metadata .as_rfc822 ()))
474488 whl .writestr (f'{ self .distinfo_dir } /WHEEL' , self .wheel )
475489 if self .entrypoints_txt :
476490 whl .writestr (f'{ self .distinfo_dir } /entry_points.txt' , self .entrypoints_txt )
@@ -570,7 +584,9 @@ def _strings(value: Any, name: str) -> List[str]:
570584 scheme = _table ({
571585 'args' : _table ({
572586 name : _strings for name in _MESON_ARGS_KEYS
573- })
587+ }),
588+ 'dependencies' : _strings ,
589+ 'build-time-pins' : _strings ,
574590 })
575591
576592 table = pyproject .get ('tool' , {}).get ('meson-python' , {})
@@ -619,6 +635,7 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None:
619635 """Validate package metadata."""
620636
621637 allowed_dynamic_fields = [
638+ 'dependencies' ,
622639 'version' ,
623640 ]
624641
@@ -635,9 +652,36 @@ def _validate_metadata(metadata: pyproject_metadata.StandardMetadata) -> None:
635652 raise ConfigError (f'building with Python { platform .python_version ()} , version { metadata .requires_python } required' )
636653
637654
655+ def _compute_build_time_dependencies (
656+ dependencies : List [packaging .requirements .Requirement ],
657+ pins : List [str ]) -> List [packaging .requirements .Requirement ]:
658+ for template in pins :
659+ match = _REQUIREMENT_NAME_REGEX .match (template )
660+ if not match :
661+ raise ConfigError (f'invalid requirement format in "build-time-pins": { template !r} ' )
662+ name = match .group (1 )
663+ try :
664+ version = packaging .version .parse (importlib_metadata .version (name ))
665+ except importlib_metadata .PackageNotFoundError as exc :
666+ raise ConfigError (f'package "{ name } " specified in "build-time-pins" not found: { template !r} ' ) from exc
667+ pin = packaging .requirements .Requirement (template .format (v = version ))
668+ if pin .marker :
669+ raise ConfigError (f'requirements in "build-time-pins" cannot contain markers: { template !r} ' )
670+ if pin .extras :
671+ raise ConfigError (f'requirements in "build-time-pins" cannot contain extras: { template !r} ' )
672+ added = False
673+ for d in dependencies :
674+ if d .name == name :
675+ d .specifier = d .specifier & pin .specifier
676+ added = True
677+ if not added :
678+ dependencies .append (pin )
679+ return dependencies
680+
681+
638682class Project ():
639683 """Meson project wrapper to generate Python artifacts."""
640- def __init__ (
684+ def __init__ ( # noqa: C901
641685 self ,
642686 source_dir : Path ,
643687 working_dir : Path ,
@@ -654,6 +698,7 @@ def __init__(
654698 self ._meson_cross_file = self ._build_dir / 'meson-python-cross-file.ini'
655699 self ._meson_args : MesonArgs = collections .defaultdict (list )
656700 self ._env = os .environ .copy ()
701+ self ._build_time_pins = []
657702
658703 _check_meson_version ()
659704
@@ -740,6 +785,13 @@ def __init__(
740785 if 'version' in self ._metadata .dynamic :
741786 self ._metadata .version = packaging .version .Version (self ._meson_version )
742787
788+ # set base dependencie if dynamic
789+ if 'dependencies' in self ._metadata .dynamic :
790+ dependencies = [packaging .requirements .Requirement (d ) for d in pyproject_config .get ('dependencies' , [])]
791+ self ._metadata .dependencies = dependencies
792+ self ._metadata .dynamic .remove ('dependencies' )
793+ self ._build_time_pins = pyproject_config .get ('build-time-pins' , [])
794+
743795 def _run (self , cmd : Sequence [str ]) -> None :
744796 """Invoke a subprocess."""
745797 print ('{cyan}{bold}+ {}{reset}' .format (' ' .join (cmd ), ** _STYLES ))
@@ -783,6 +835,7 @@ def _wheel_builder(self) -> _WheelBuilder:
783835 self ._source_dir ,
784836 self ._build_dir ,
785837 self ._install_plan ,
838+ self ._build_time_pins ,
786839 )
787840
788841 def build_commands (self , install_dir : Optional [pathlib .Path ] = None ) -> Sequence [Sequence [str ]]:
0 commit comments