Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: allow users to pass options directly to Meson #167

Merged
merged 1 commit into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
7 changes: 5 additions & 2 deletions mesonpy/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@


if sys.version_info >= (3, 9):
from collections.abc import Collection, Iterable, Iterator, Sequence
from collections.abc import (
Collection, Iterable, Iterator, Mapping, Sequence
)
else:
from typing import Collection, Iterable, Iterator, Sequence
from typing import Collection, Iterable, Iterator, Mapping, Sequence


if sys.version_info >= (3, 8):
Expand All @@ -39,6 +41,7 @@ def is_relative_to(path: pathlib.Path, other: Union[pathlib.Path, str]) -> bool:
'Iterable',
'Iterator',
'Literal',
'Mapping',
'Path',
'Sequence',
]
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]
Copy link
Member

@dnicolodi dnicolodi Nov 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be mesonpy? I think we are settling to meson-python as project name and it is nice to try to be consistent. Actually, we could even think about using tool.meson.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point, let's try and update it. We should get rid of mesonpy everywhere except for in the build-backend = 'mesonpy' hook line.

I'd prefer meson-python here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whops, I missed this comment 😣

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'),
]