diff --git a/docs/reference/environment-variables.rst b/docs/reference/environment-variables.rst index 3413f4bd6..4714f77b9 100644 --- a/docs/reference/environment-variables.rst +++ b/docs/reference/environment-variables.rst @@ -59,6 +59,12 @@ Environment variables possible for ``meson-python`` to detect this, and it will not set the Python wheel platform tag accordingly. +.. envvar:: MESON + + Specifies the ``meson`` executable or script to use. It overrides + ``tool.meson-python.meson``. See :ref:`reference-pyproject-settings` for + more details. + .. envvar:: MESONPY_EDITABLE_VERBOSE Setting this environment variable to any value enables directing to the @@ -68,7 +74,7 @@ Environment variables .. envvar:: NINJA - Specify the ninja_ executable to use. It can also be used to select + Specifies the ninja_ executable to use. It can also be used to select ninja_ alternatives like samurai_. .. _ninja: https://ninja-build.org diff --git a/docs/reference/pyproject-settings.rst b/docs/reference/pyproject-settings.rst index f0921d019..3beb47c61 100644 --- a/docs/reference/pyproject-settings.rst +++ b/docs/reference/pyproject-settings.rst @@ -26,6 +26,14 @@ use them and examples. ``-Dpython.allow_limited_api=false`` option is passed to ``meson setup``. +.. option:: tool.meson-python.meson + + A string specifying the ``meson`` executable or script to use. If it is a + path to an existing file with a name ending in ``.py``, it will be invoked + as a Python script using the same Python interpreter that is used to run + ``meson-python`` itself. It can be overrridden by the :envvar:`MESON` + environment variable. + .. option:: tool.meson-python.args.dist Extra arguments to be passed to the ``meson dist`` command. diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index afad4f363..a5d98bcc7 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -528,7 +528,15 @@ def _bool(value: Any, name: str) -> bool: raise ConfigError(f'Configuration entry "{name}" must be a boolean') return value + def _string_or_path(value: Any, name: str) -> str: + if not isinstance(value, str): + raise ConfigError(f'Configuration entry "{name}" must be a string') + if os.path.isfile(value): + value = os.path.abspath(value) + return value + scheme = _table({ + 'meson': _string_or_path, 'limited-api': _bool, 'args': _table({ name: _strings for name in _MESON_ARGS_KEYS @@ -592,7 +600,7 @@ def _string_or_strings(value: Any, name: str) -> List[str]: class Project(): """Meson project wrapper to generate Python artifacts.""" - def __init__( # noqa: C901 + def __init__( self, source_dir: Path, build_dir: Path, @@ -607,7 +615,22 @@ def __init__( # noqa: C901 self._meson_args: MesonArgs = collections.defaultdict(list) self._limited_api = False - _check_meson_version() + # load pyproject.toml + pyproject = tomllib.loads(self._source_dir.joinpath('pyproject.toml').read_text()) + + # load meson args from pyproject.toml + pyproject_config = _validate_pyproject_config(pyproject) + for key, value in pyproject_config.get('args', {}).items(): + self._meson_args[key].extend(value) + + # meson arguments from the command line take precedence over + # arguments from the configuration file thus are added later + if meson_args: + for key, value in meson_args.items(): + self._meson_args[key].extend(value) + + # determine command to invoke meson + self._meson = _get_meson_command(pyproject_config.get('meson')) self._ninja = _env_ninja_command() if self._ninja is None: @@ -645,20 +668,6 @@ def __init__( # noqa: C901 self._meson_cross_file.write_text(cross_file_data) self._meson_args['setup'].extend(('--cross-file', os.fspath(self._meson_cross_file))) - # load pyproject.toml - pyproject = tomllib.loads(self._source_dir.joinpath('pyproject.toml').read_text()) - - # load meson args from pyproject.toml - pyproject_config = _validate_pyproject_config(pyproject) - for key, value in pyproject_config.get('args', {}).items(): - self._meson_args[key].extend(value) - - # meson arguments from the command line take precedence over - # arguments from the configuration file thus are added later - if meson_args: - for key, value in meson_args.items(): - self._meson_args[key].extend(value) - # write the native file native_file_data = textwrap.dedent(f''' [binaries] @@ -740,7 +749,7 @@ def _configure(self, reconfigure: bool = False) -> None: ] if reconfigure: setup_args.insert(0, '--reconfigure') - self._run(['meson', 'setup', *setup_args]) + self._run(self._meson + ['setup', *setup_args]) @property def _build_command(self) -> List[str]: @@ -750,7 +759,7 @@ def _build_command(self) -> List[str]: # environment. Using the --ninja-args option allows to # provide the exact same semantics for the compile arguments # provided by the users. - cmd = ['meson', 'compile'] + cmd = self._meson + ['compile'] args = list(self._meson_args['compile']) if args: cmd.append(f'--ninja-args={args!r}') @@ -824,7 +833,7 @@ def version(self) -> str: def sdist(self, directory: Path) -> pathlib.Path: """Generates a sdist (source distribution) in the specified directory.""" # generate meson dist file - self._run(['meson', 'dist', '--allow-dirty', '--no-tests', '--formats', 'gztar', *self._meson_args['dist']]) + self._run(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}' @@ -919,6 +928,37 @@ def _parse_version_string(string: str) -> Tuple[int, ...]: return (0, ) +def _get_meson_command( + meson: Optional[str] = None, *, version: str = _MESON_REQUIRED_VERSION + ) -> List[str]: + """Return the command to invoke meson.""" + + # The MESON env var, if set, overrides the config value from pyproject.toml. + # The config value, if given, is an absolute path or the name of an executable. + meson = os.environ.get('MESON', meson or 'meson') + + # If the specified Meson string ends in `.py`, we run it with the current + # Python executable. This avoids problems for users on Windows, where + # making a script executable isn't enough to get it to run when invoked + # directly. For packages that vendor a forked Meson, the `meson.py` in the + # root of the Meson repo can be used this way. + if meson.endswith('.py'): + cmd = [sys.executable, meson] + else: + cmd = [meson] + + # The meson Python package is a dependency of the meson-python Python + # package, however, it may occur that the meson Python package is installed + # but the corresponding meson command is not available in $PATH. Implement + # a runtime check to verify that the build environment is setup correcly. + required_version = _parse_version_string(version) + meson_version = subprocess.run(cmd + ['--version'], check=False, text=True, capture_output=True).stdout + if _parse_version_string(meson_version) < required_version: + raise ConfigError(f'Could not find meson version {version} or newer, found {meson_version}.') + + return cmd + + def _env_ninja_command(*, version: str = _NINJA_REQUIRED_VERSION) -> Optional[str]: """Returns the path to ninja, or None if no ninja found.""" required_version = _parse_version_string(version) @@ -933,22 +973,6 @@ def _env_ninja_command(*, version: str = _NINJA_REQUIRED_VERSION) -> Optional[st return None -def _check_meson_version(*, version: str = _MESON_REQUIRED_VERSION) -> None: - """Check that the meson executable in the path has an appropriate version. - - The meson Python package is a dependency of the meson-python - Python package, however, it may occur that the meson Python - package is installed but the corresponding meson command is not - available in $PATH. Implement a runtime check to verify that the - build environment is setup correcly. - - """ - required_version = _parse_version_string(version) - meson_version = subprocess.run(['meson', '--version'], check=False, text=True, capture_output=True).stdout - if _parse_version_string(meson_version) < required_version: - raise ConfigError(f'Could not find meson version {version} or newer, found {meson_version}.') - - def _add_ignore_files(directory: pathlib.Path) -> None: directory.joinpath('.gitignore').write_text(textwrap.dedent(''' # This file is generated by meson-python. It will not be recreated if deleted or modified. diff --git a/pyproject.toml b/pyproject.toml index 91203197f..c55a5726c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,6 @@ extend-ignore = [ select = [ 'B', # flake8-bugbear 'C4', # flake8-comprehensions - 'C9', # mccabe 'E', # pycodestyle 'F', # pyflakes 'W', # pycodestyle @@ -92,10 +91,6 @@ exclude = [ 'docs/conf.py', ] -[tool.ruff.mccabe] -max-complexity = 12 - - [tool.isort] lines_between_types = 1 lines_after_imports = 2 diff --git a/tests/packages/vendored-meson/meson.build b/tests/packages/vendored-meson/meson.build new file mode 100644 index 000000000..d3be1a790 --- /dev/null +++ b/tests/packages/vendored-meson/meson.build @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2023 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('vendored-meson', version: '1.0.0') + +py = import('python').find_installation() + +if not get_option('custom-meson-used') + error('Expected option "custom-meson-used" was not specified') +endif diff --git a/tests/packages/vendored-meson/meson_options.txt b/tests/packages/vendored-meson/meson_options.txt new file mode 100644 index 000000000..fce15cd4f --- /dev/null +++ b/tests/packages/vendored-meson/meson_options.txt @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2023 The meson-python developers +# +# SPDX-License-Identifier: MIT + +option('custom-meson-used', type: 'boolean', value: false) diff --git a/tests/packages/vendored-meson/pyproject.toml b/tests/packages/vendored-meson/pyproject.toml new file mode 100644 index 000000000..118a060af --- /dev/null +++ b/tests/packages/vendored-meson/pyproject.toml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2023 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] + +[tool.meson-python] +meson = 'third-party/meson.py' diff --git a/tests/packages/vendored-meson/third-party/meson.py b/tests/packages/vendored-meson/third-party/meson.py new file mode 100644 index 000000000..c9145a7b0 --- /dev/null +++ b/tests/packages/vendored-meson/third-party/meson.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2023 The meson-python developers +# +# SPDX-License-Identifier: MIT + +import sys + +from mesonbuild import mesonmain + + +if 'setup' in sys.argv: + sys.argv.append('-Dcustom-meson-used=true') + + +if __name__ == '__main__': + sys.exit(mesonmain.main()) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 6b68616d7..a480802a2 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -333,3 +333,9 @@ def test_install_subdir(wheel_install_subdir): 'nested/deep/deep.py', 'nested/nested.py', } + + +def test_vendored_meson(wheel_vendored_meson): + # This test will error if the vendored meson.py wrapper script in + # the test package isn't used. + pass