3232import warnings
3333
3434from typing import (
35- Any , ClassVar , DefaultDict , Dict , List , Optional , Set , TextIO , Tuple , Type ,
36- Union
35+ Any , ClassVar , DefaultDict , Dict , List , Optional , Sequence , Set , TextIO ,
36+ Tuple , Type , Union
3737)
3838
3939
4747import mesonpy ._tags
4848import mesonpy ._util
4949
50- from mesonpy ._compat import Iterator , Path
50+ from mesonpy ._compat import Collection , Iterator , Literal , Mapping , Path
5151
5252
5353if typing .TYPE_CHECKING : # pragma: no cover
@@ -134,6 +134,10 @@ def _setup_cli() -> None:
134134 colorama .init () # fix colors on windows
135135
136136
137+ class ConfigError (Exception ):
138+ """Error in the backend configuration."""
139+
140+
137141class MesonBuilderError (Exception ):
138142 """Error when building the Meson package."""
139143
@@ -538,6 +542,9 @@ def build(self, directory: Path) -> pathlib.Path:
538542 return wheel_file
539543
540544
545+ MesonArgs = Mapping [Literal ['dist' , 'setup' , 'compile' , 'install' ], Collection [str ]]
546+
547+
541548class Project ():
542549 """Meson project wrapper to generate Python artifacts."""
543550
@@ -551,6 +558,7 @@ def __init__(
551558 source_dir : Path ,
552559 working_dir : Path ,
553560 build_dir : Optional [Path ] = None ,
561+ meson_args : Optional [MesonArgs ] = None ,
554562 ) -> None :
555563 self ._source_dir = pathlib .Path (source_dir ).absolute ()
556564 self ._working_dir = pathlib .Path (working_dir ).absolute ()
@@ -578,6 +586,13 @@ def __init__(
578586 if self ._metadata :
579587 self ._validate_metadata ()
580588
589+ # load meson args
590+ self ._meson_args = collections .defaultdict (tuple , meson_args or {})
591+ for key in self ._get_config_key ('args' ):
592+ args_from_config = tuple (self ._get_config_key (f'args.{ key } ' ))
593+ self ._meson_args [key ] = args_from_config + tuple (self ._meson_args [key ])
594+ # XXX: We should validate the user args to make sure they don't conflict with ours.
595+
581596 # make sure the build dir exists
582597 self ._build_dir .mkdir (exist_ok = True )
583598 self ._install_dir .mkdir (exist_ok = True )
@@ -608,6 +623,17 @@ def __init__(
608623 if self ._metadata and 'version' in self ._metadata .dynamic :
609624 self ._metadata .version = self .version
610625
626+ def _get_config_key (self , key : str ) -> Any :
627+ value : Any = self ._config
628+ for part in f'tool.mesonpy.{ key } ' .split ('.' ):
629+ if not isinstance (value , Mapping ):
630+ raise ConfigError (
631+ f'Found unexpected value in `{ part } ` when looking for '
632+ f'config key `tool.mesonpy.{ key } ` (`{ value } `)'
633+ )
634+ value = value .get (part , {})
635+ return value
636+
611637 def _proc (self , * args : str ) -> None :
612638 """Invoke a subprocess."""
613639 print ('{cyan}{bold}+ {}{reset}' .format (' ' .join (args ), ** _STYLES ))
@@ -628,19 +654,18 @@ def _configure(self, reconfigure: bool = False) -> None:
628654 f'--prefix={ sys .base_prefix } ' ,
629655 os .fspath (self ._source_dir ),
630656 os .fspath (self ._build_dir ),
657+ f'--native-file={ os .fspath (self ._meson_native_file )} ' ,
658+ # TODO: Allow configuring these arguments
659+ '-Ddebug=false' ,
660+ '-Doptimization=2' ,
661+ # user args
662+ * self ._meson_args ['setup' ],
631663 ]
632664 if reconfigure :
633665 setup_args .insert (0 , '--reconfigure' )
634666
635667 try :
636- self ._meson (
637- 'setup' ,
638- f'--native-file={ os .fspath (self ._meson_native_file )} ' ,
639- # TODO: Allow configuring these arguments
640- '-Ddebug=false' ,
641- '-Doptimization=2' ,
642- * setup_args ,
643- )
668+ self ._meson ('setup' , * setup_args )
644669 except subprocess .CalledProcessError :
645670 if reconfigure : # if failed reconfiguring, try a normal configure
646671 self ._configure ()
@@ -686,19 +711,20 @@ def _wheel_builder(self) -> _WheelBuilder:
686711 @functools .lru_cache (maxsize = None )
687712 def build (self ) -> None :
688713 """Trigger the Meson build."""
689- self ._meson ('compile' )
690- self ._meson ('install' , '--destdir' , os .fspath (self ._install_dir ))
714+ self ._meson ('compile' , * self . _meson_args [ 'compile' ], )
715+ self ._meson ('install' , '--destdir' , os .fspath (self ._install_dir ), * self . _meson_args [ 'install' ], )
691716
692717 @classmethod
693718 @contextlib .contextmanager
694719 def with_temp_working_dir (
695720 cls ,
696721 source_dir : Path = os .path .curdir ,
697722 build_dir : Optional [Path ] = None ,
723+ meson_args : Optional [MesonArgs ] = None ,
698724 ) -> Iterator [Project ]:
699725 """Creates a project instance pointing to a temporary working directory."""
700726 with tempfile .TemporaryDirectory (prefix = '.mesonpy-' , dir = os .fspath (source_dir )) as tmpdir :
701- yield cls (source_dir , tmpdir , build_dir )
727+ yield cls (source_dir , tmpdir , build_dir , meson_args )
702728
703729 @functools .lru_cache ()
704730 def _info (self , name : str ) -> Dict [str , Any ]:
@@ -806,7 +832,7 @@ def pep621(self) -> bool:
806832 def sdist (self , directory : Path ) -> pathlib .Path :
807833 """Generates a sdist (source distribution) in the specified directory."""
808834 # generate meson dist file
809- self ._meson ('dist' , '--allow-dirty' , '--no-tests' , '--formats' , 'gztar' )
835+ self ._meson ('dist' , '--allow-dirty' , '--no-tests' , '--formats' , 'gztar' , * self . _meson_args [ 'dist' ], )
810836
811837 # move meson dist file to output path
812838 dist_name = f'{ self .name } -{ self .version } '
@@ -882,8 +908,51 @@ def _project(config_settings: Optional[Dict[Any, Any]]) -> Iterator[Project]:
882908 if config_settings is None :
883909 config_settings = {}
884910
911+ # expand all string values to single element tuples and convert collections to tuple
912+ config_settings = {
913+ key : tuple (value ) if isinstance (value , Collection ) and not isinstance (value , str ) else (value ,)
914+ for key , value in config_settings .items ()
915+ }
916+
917+ builddir_value = config_settings .get ('builddir' , {})
918+ if len (builddir_value ) > 0 :
919+ if len (builddir_value ) != 1 :
920+ raise ConfigError ('Specified multiple values for `builddir`, only one is allowed' )
921+ builddir = builddir_value [0 ]
922+ if not isinstance (builddir , str ):
923+ raise ConfigError (f'Config option `builddir` should be a string (found `{ type (builddir )} `)' )
924+ else :
925+ builddir = None
926+
927+ def _validate_string_collection (key : str ) -> None :
928+ assert isinstance (config_settings , Mapping )
929+ problematic_items : Sequence [Any ] = list (filter (None , (
930+ item if not isinstance (item , str ) else None
931+ for item in config_settings .get (key , ())
932+ )))
933+ if problematic_items :
934+ raise ConfigError (
935+ f'Config option `{ key } ` should only contain string items, but '
936+ 'contains the following parameters that do not meet this criteria:' +
937+ '' .join ((
938+ f'\t - { item } (type: { type (item )} )'
939+ for item in problematic_items
940+ ))
941+ )
942+
943+ _validate_string_collection ('dist_args' )
944+ _validate_string_collection ('setup_args' )
945+ _validate_string_collection ('compile_args' )
946+ _validate_string_collection ('install_args' )
947+
885948 with Project .with_temp_working_dir (
886- build_dir = config_settings .get ('builddir' ),
949+ build_dir = builddir ,
950+ meson_args = typing .cast (MesonArgs , {
951+ 'dist' : config_settings .get ('dist_args' , ()),
952+ 'setup' : config_settings .get ('setup_args' , ()),
953+ 'compile' : config_settings .get ('compile_args' , ()),
954+ 'install' : config_settings .get ('install_args' , ()),
955+ }),
887956 ) as project :
888957 yield project
889958
0 commit comments