Skip to content

Commit

Permalink
ENH: allow users to pass options directly to Meson
Browse files Browse the repository at this point in the history
Signed-off-by: Filipe Laíns <[email protected]>
  • Loading branch information
FFY00 committed Nov 16, 2022
1 parent f50c591 commit 2669466
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 16 deletions.
101 changes: 85 additions & 16 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
import warnings

from typing import (
Any, ClassVar, DefaultDict, Dict, List, Optional, Set, TextIO, Tuple, Type,
Union
Any, ClassVar, DefaultDict, Dict, List, Optional, Sequence, Set, TextIO,
Tuple, Type, Union
)


Expand All @@ -47,7 +47,7 @@
import mesonpy._tags
import mesonpy._util

from mesonpy._compat import Iterator, Path
from mesonpy._compat import Collection, Iterator, Literal, Mapping, Path


if typing.TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -134,6 +134,10 @@ def _setup_cli() -> None:
colorama.init() # fix colors on windows


class ConfigError(Exception):
"""Error in the backend configuration."""


class MesonBuilderError(Exception):
"""Error when building the Meson package."""

Expand Down Expand Up @@ -538,6 +542,9 @@ def build(self, directory: Path) -> pathlib.Path:
return wheel_file


MesonArgs = Mapping[Literal['dist', 'setup', 'compile', 'install'], Collection[str]]


class Project():
"""Meson project wrapper to generate Python artifacts."""

Expand All @@ -551,6 +558,7 @@ def __init__(
source_dir: Path,
working_dir: Path,
build_dir: Optional[Path] = None,
meson_args: Optional[MesonArgs] = None,
) -> None:
self._source_dir = pathlib.Path(source_dir).absolute()
self._working_dir = pathlib.Path(working_dir).absolute()
Expand Down Expand Up @@ -578,6 +586,13 @@ def __init__(
if self._metadata:
self._validate_metadata()

# load meson args
self._meson_args = collections.defaultdict(tuple, meson_args or {})
for key in self._get_config_key('args'):
args_from_config = tuple(self._get_config_key(f'args.{key}'))
self._meson_args[key] = args_from_config + tuple(self._meson_args[key])
# XXX: We should validate the user args to make sure they don't conflict with ours.

# make sure the build dir exists
self._build_dir.mkdir(exist_ok=True)
self._install_dir.mkdir(exist_ok=True)
Expand Down Expand Up @@ -608,6 +623,17 @@ def __init__(
if self._metadata and 'version' in self._metadata.dynamic:
self._metadata.version = self.version

def _get_config_key(self, key: str) -> Any:
value: Any = self._config
for part in f'tool.mesonpy.{key}'.split('.'):
if not isinstance(value, Mapping):
raise ConfigError(
f'Found unexpected value in `{part}` when looking for '
f'config key `tool.mesonpy.{key}` (`{value}`)'
)
value = value.get(part, {})
return value

def _proc(self, *args: str) -> None:
"""Invoke a subprocess."""
print('{cyan}{bold}+ {}{reset}'.format(' '.join(args), **_STYLES))
Expand All @@ -628,19 +654,18 @@ def _configure(self, reconfigure: bool = False) -> None:
f'--prefix={sys.base_prefix}',
os.fspath(self._source_dir),
os.fspath(self._build_dir),
f'--native-file={os.fspath(self._meson_native_file)}',
# TODO: Allow configuring these arguments
'-Ddebug=false',
'-Doptimization=2',
# user args
*self._meson_args['setup'],
]
if reconfigure:
setup_args.insert(0, '--reconfigure')

try:
self._meson(
'setup',
f'--native-file={os.fspath(self._meson_native_file)}',
# TODO: Allow configuring these arguments
'-Ddebug=false',
'-Doptimization=2',
*setup_args,
)
self._meson('setup', *setup_args)
except subprocess.CalledProcessError:
if reconfigure: # if failed reconfiguring, try a normal configure
self._configure()
Expand Down Expand Up @@ -686,19 +711,20 @@ def _wheel_builder(self) -> _WheelBuilder:
@functools.lru_cache(maxsize=None)
def build(self) -> None:
"""Trigger the Meson build."""
self._meson('compile')
self._meson('install', '--destdir', os.fspath(self._install_dir))
self._meson('compile', *self._meson_args['compile'],)
self._meson('install', '--destdir', os.fspath(self._install_dir), *self._meson_args['install'],)

@classmethod
@contextlib.contextmanager
def with_temp_working_dir(
cls,
source_dir: Path = os.path.curdir,
build_dir: Optional[Path] = None,
meson_args: Optional[MesonArgs] = None,
) -> Iterator[Project]:
"""Creates a project instance pointing to a temporary working directory."""
with tempfile.TemporaryDirectory(prefix='.mesonpy-', dir=os.fspath(source_dir)) as tmpdir:
yield cls(source_dir, tmpdir, build_dir)
yield cls(source_dir, tmpdir, build_dir, meson_args)

@functools.lru_cache()
def _info(self, name: str) -> Dict[str, Any]:
Expand Down Expand Up @@ -806,7 +832,7 @@ def pep621(self) -> bool:
def sdist(self, directory: Path) -> pathlib.Path:
"""Generates a sdist (source distribution) in the specified directory."""
# generate meson dist file
self._meson('dist', '--allow-dirty', '--no-tests', '--formats', 'gztar')
self._meson('dist', '--allow-dirty', '--no-tests', '--formats', 'gztar', *self._meson_args['dist'],)

# move meson dist file to output path
dist_name = f'{self.name}-{self.version}'
Expand Down Expand Up @@ -882,8 +908,51 @@ def _project(config_settings: Optional[Dict[Any, Any]]) -> Iterator[Project]:
if config_settings is None:
config_settings = {}

# expand all string values to single element tuples and convert collections to tuple
config_settings = {
key: tuple(value) if isinstance(value, Collection) and not isinstance(value, str) else (value,)
for key, value in config_settings.items()
}

builddir_value = config_settings.get('builddir', {})
if len(builddir_value) > 0:
if len(builddir_value) != 1:
raise ConfigError('Specified multiple values for `builddir`, only one is allowed')
builddir = builddir_value[0]
if not isinstance(builddir, str):
raise ConfigError(f'Config option `builddir` should be a string (found `{type(builddir)}`)')
else:
builddir = None

def _validate_string_collection(key: str) -> None:
assert isinstance(config_settings, Mapping)
problematic_items: Sequence[Any] = list(filter(None, (
item if not isinstance(item, str) else None
for item in config_settings.get(key, ())
)))
if problematic_items:
raise ConfigError(
f'Config option `{key}` should only contain string items, but '
'contains the following parameters that do not meet this criteria:' +
''.join((
f'\t- {item} (type: {type(item)})'
for item in problematic_items
))
)

_validate_string_collection('dist_args')
_validate_string_collection('setup_args')
_validate_string_collection('compile_args')
_validate_string_collection('install_args')

with Project.with_temp_working_dir(
build_dir=config_settings.get('builddir'),
build_dir=builddir,
meson_args=typing.cast(MesonArgs, {
'dist': config_settings.get('dist_args', ()),
'setup': config_settings.get('setup_args', ()),
'compile': config_settings.get('compile_args', ()),
'install': config_settings.get('install_args', ()),
}),
) as project:
yield project

Expand Down
6 changes: 6 additions & 0 deletions tests/packages/dist-script/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
project(
'dist-script', 'c',
version: '1.0.0',
)

meson.add_dist_script('')
3 changes: 3 additions & 0 deletions tests/packages/dist-script/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']
9 changes: 9 additions & 0 deletions tests/packages/user-args/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
project(
'user-args',
version: '1.0.0',
)

py_mod = import('python')
py = py_mod.find_installation()

py.install_sources('pure.py')
2 changes: 2 additions & 0 deletions tests/packages/user-args/pure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def foo():
return 'bar'
9 changes: 9 additions & 0 deletions tests/packages/user-args/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']

[tool.mesonpy.args]
dist = ['config-dist']
setup = ['config-setup']
compile = ['config-compile']
install = ['config-install']
43 changes: 43 additions & 0 deletions tests/test_project.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# SPDX-License-Identifier: MIT

import contextlib
import platform
import subprocess
import sys

import pytest

Expand Down Expand Up @@ -46,3 +49,43 @@ def test_unsupported_python_version(package_unsupported_python_version):
)):
with mesonpy.Project.with_temp_working_dir():
pass


@pytest.mark.skipif(
sys.version_info < (3, 8),
reason="unittest.mock doesn't support the required APIs for this test",
)
def test_user_args(package_user_args, mocker, tmp_dir_session):
mocker.patch('mesonpy.Project._meson')

def last_two_meson_args():
return [
call.args[-2:] for call in mesonpy.Project._meson.call_args_list
]

# create the build directory ourselves because Project._meson is mocked
builddir = str(tmp_dir_session / 'build')
subprocess.check_call(['meson', 'setup', '.', builddir])

config_settings = {
'builddir': builddir, # use the build directory we created
'dist_args': ('cli-dist',),
'setup_args': ('cli-setup',),
'compile_args': ('cli-compile',),
'install_args': ('cli-install',),
}

with contextlib.suppress(Exception):
mesonpy.build_sdist(tmp_dir_session / 'dist', config_settings)
with contextlib.suppress(Exception):
mesonpy.build_wheel(tmp_dir_session / 'dist', config_settings)

assert last_two_meson_args() == [
# sdist
('config-setup', 'cli-setup'),
('config-dist', 'cli-dist'),
# wheel
('config-setup', 'cli-setup'),
('config-compile', 'cli-compile'),
('config-install', 'cli-install'),
]

0 comments on commit 2669466

Please sign in to comment.