Skip to content

Commit 4bdb1d2

Browse files
committed
ENH: allow users to pass options directly to Meson
Signed-off-by: Filipe Laíns <[email protected]>
1 parent f50c591 commit 4bdb1d2

File tree

8 files changed

+162
-18
lines changed

8 files changed

+162
-18
lines changed

mesonpy/__init__.py

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
import warnings
3333

3434
from 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

@@ -47,7 +47,7 @@
4747
import mesonpy._tags
4848
import mesonpy._util
4949

50-
from mesonpy._compat import Iterator, Path
50+
from mesonpy._compat import Collection, Iterator, Literal, Mapping, Path
5151

5252

5353
if 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+
137141
class 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+
541548
class 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

mesonpy/_compat.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010

1111

1212
if sys.version_info >= (3, 9):
13-
from collections.abc import Collection, Iterable, Iterator, Sequence
13+
from collections.abc import (
14+
Collection, Iterable, Iterator, Mapping, Sequence
15+
)
1416
else:
15-
from typing import Collection, Iterable, Iterator, Sequence
17+
from typing import Collection, Iterable, Iterator, Mapping, Sequence
1618

1719

1820
if sys.version_info >= (3, 8):
@@ -39,6 +41,7 @@ def is_relative_to(path: pathlib.Path, other: Union[pathlib.Path, str]) -> bool:
3941
'Iterable',
4042
'Iterator',
4143
'Literal',
44+
'Mapping',
4245
'Path',
4346
'Sequence',
4447
]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
project(
2+
'dist-script', 'c',
3+
version: '1.0.0',
4+
)
5+
6+
meson.add_dist_script('')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
build-backend = 'mesonpy'
3+
requires = ['meson-python']

tests/packages/user-args/meson.build

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
project(
2+
'user-args',
3+
version: '1.0.0',
4+
)
5+
6+
py_mod = import('python')
7+
py = py_mod.find_installation()
8+
9+
py.install_sources('pure.py')

tests/packages/user-args/pure.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def foo():
2+
return 'bar'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[build-system]
2+
build-backend = 'mesonpy'
3+
requires = ['meson-python']
4+
5+
[tool.mesonpy.args]
6+
dist = ['config-dist']
7+
setup = ['config-setup']
8+
compile = ['config-compile']
9+
install = ['config-install']

tests/test_project.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# SPDX-License-Identifier: MIT
22

3+
import contextlib
34
import platform
5+
import subprocess
6+
import sys
47

58
import pytest
69

@@ -46,3 +49,43 @@ def test_unsupported_python_version(package_unsupported_python_version):
4649
)):
4750
with mesonpy.Project.with_temp_working_dir():
4851
pass
52+
53+
54+
@pytest.mark.skipif(
55+
sys.version_info < (3, 8),
56+
reason="unittest.mock doesn't support the required APIs for this test",
57+
)
58+
def test_user_args(package_user_args, mocker, tmp_dir_session):
59+
mocker.patch('mesonpy.Project._meson')
60+
61+
def last_two_meson_args():
62+
return [
63+
call.args[-2:] for call in mesonpy.Project._meson.call_args_list
64+
]
65+
66+
# create the build directory ourselves because Project._meson is mocked
67+
builddir = str(tmp_dir_session / 'build')
68+
subprocess.check_call(['meson', 'setup', '.', builddir])
69+
70+
config_settings = {
71+
'builddir': builddir, # use the build directory we created
72+
'dist_args': ('cli-dist',),
73+
'setup_args': ('cli-setup',),
74+
'compile_args': ('cli-compile',),
75+
'install_args': ('cli-install',),
76+
}
77+
78+
with contextlib.suppress(Exception):
79+
mesonpy.build_sdist(tmp_dir_session / 'dist', config_settings)
80+
with contextlib.suppress(Exception):
81+
mesonpy.build_wheel(tmp_dir_session / 'dist', config_settings)
82+
83+
assert last_two_meson_args() == [
84+
# sdist
85+
('config-setup', 'cli-setup'),
86+
('config-dist', 'cli-dist'),
87+
# wheel
88+
('config-setup', 'cli-setup'),
89+
('config-compile', 'cli-compile'),
90+
('config-install', 'cli-install'),
91+
]

0 commit comments

Comments
 (0)