diff --git a/docs/how-to-guides/meson-args.rst b/docs/how-to-guides/meson-args.rst index 1cd03aff1..b6e77361d 100644 --- a/docs/how-to-guides/meson-args.rst +++ b/docs/how-to-guides/meson-args.rst @@ -44,6 +44,20 @@ building a Python wheel. User options specified via ``pyproject.toml`` or via Python build front-end config settings override the ``meson-python`` defaults. +When building on Windows, ``meson-python`` invokes the ``ninja`` +command via the ``meson compile`` wrapper. When the GCC or the LLVM +compilers are not found on the ``$PATH``, this activates the Visual +Studio environment and allows ``ninja`` to use the MSVC compilers. To +activate the Visual Studio environment unconditionally, pass the +``--vsenv`` option to ``meson setup``, see this :ref:`example +`. When using the ``meson compile`` wrapper, the user +supplied options for the compilation command are passed via the +``--ninja-args`` option. This ensures that the behaviour is +independent of how the build is initiated. Refer to the `Meson +documentation`__ for more details. + +__ https://mesonbuild.com/Commands.html#backend-specific-arguments + Examples ======== @@ -152,3 +166,44 @@ To set this option temporarily at build-time: .. code-block:: console $ python -m pip wheel . --config-settings=setup-args="-Doptimization=3" . + + +.. _vsenv-example: + +Force the use of the MSVC compilers on Windows +---------------------------------------------- + +The MSVC compilers are not installed in the ``$PATH``. The Visual +Studio environment needs to be activated for ``ninja`` to be able to +use these compilers. This is taken care of by ``meson compile`` but +only when the GCC compilers or the LLVM compilers are not found on the +``$PATH``. Passing the ``--vsenv`` option to ``meson setup`` forces +the activation of the Visual Studio environment and generates an error +when the activation fails. + +This option has no effect on other platforms thus, if your project +requires to be compiled with MSVC, you can consider to set this option +permanently in the project's ``pyproject.toml``: + +.. code-block:: toml + + [tool.meson-python.args] + setup = ['--vsenv'] + +To set this option temporarily at build-time: + +.. tab-set:: + + .. tab-item:: pypa/build + :sync: key_pypa_build + + .. code-block:: console + + $ python -m build -Csetup-args="--vsenv" . + + .. tab-item:: pip + :sync: key_pip + + .. code-block:: console + + $ python -m pip wheel . --config-settings=setup-args="--vsenv" . diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index fb9c434b8..1697f7291 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -489,7 +489,10 @@ def _wheel_write_metadata(self, whl: mesonpy._wheelfile.WheelFile) -> None: ) def build(self, directory: Path) -> pathlib.Path: - self._project.build() # ensure project is built + # ensure project is built + self._project.build() + # install the project + self._project.install() wheel_file = pathlib.Path(directory, f'{self.name}.whl') @@ -521,7 +524,8 @@ def build(self, directory: Path) -> pathlib.Path: return wheel_file def build_editable(self, directory: Path, verbose: bool = False) -> pathlib.Path: - self._project.build() # ensure project is built + # ensure project is built + self._project.build() wheel_file = pathlib.Path(directory, f'{self.name}.whl') @@ -534,14 +538,13 @@ def build_editable(self, directory: Path, verbose: bool = False) -> pathlib.Path # install loader module loader_module_name = f'_{self.normalized_name.replace(".", "_")}_editable_loader' - build_cmd = [self._project._ninja, *self._project._meson_args['compile']] whl.writestr( f'{loader_module_name}.py', read_binary('mesonpy', '_editable.py') + textwrap.dedent(f''' install( {self.top_level_modules!r}, {os.fspath(self._build_dir)!r}, - {build_cmd}, + {self._project._build_command!r}, {verbose!r}, )''').encode('utf-8')) @@ -787,18 +790,30 @@ def _wheel_builder(self) -> _WheelBuilder: self._install_plan, ) - def build_commands(self) -> Sequence[Sequence[str]]: + @property + def _build_command(self) -> List[str]: assert self._ninja is not None # help mypy out - return ( - (self._ninja, *self._meson_args['compile'],), - ('meson', 'install', '--no-rebuild', '--destdir', os.fspath(self._install_dir), *self._meson_args['install']), - ) + if platform.system() == 'Windows': + # On Windows use 'meson compile' to setup the MSVC compiler + # environment. Using the --ninja-args option allows to + # provide the exact same semantics for the compile arguments + # provided by the users. + cmd = ['meson', 'compile'] + args = list(self._meson_args['compile']) + if args: + cmd.append(f'--ninja-args={args!r}') + return cmd + return [self._ninja, *self._meson_args['compile']] @functools.lru_cache(maxsize=None) def build(self) -> None: - """Trigger the Meson build.""" - for cmd in self.build_commands(): - self._run(cmd) + """Build the Meson project.""" + self._run(self._build_command) + + def install(self) -> None: + """Install the Meson project.""" + destdir = os.fspath(self._install_dir) + self._run(['meson', 'install', '--no-rebuild', '--destdir', destdir, *self._meson_args['install']]) @classmethod @contextlib.contextmanager diff --git a/tests/packages/detect-compiler/detect_compiler.c b/tests/packages/detect-compiler/detect_compiler.c new file mode 100644 index 000000000..3ff86b2b8 --- /dev/null +++ b/tests/packages/detect-compiler/detect_compiler.c @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 The meson-python developers +// +// SPDX-License-Identifier: MIT + +#include + +#if defined _MSC_VER +# define _COMPILER "msvc" +#elif defined __clang__ +# define _COMPILER "clang" +#elif defined __GNUC__ +# define _COMPILER "gcc" +#else +# define _COMPILER "unknown" +#endif + +static PyObject* compiler(PyObject* self) +{ + return PyUnicode_FromString(_COMPILER); +} + +static PyMethodDef methods[] = { + {"compiler", (PyCFunction)compiler, METH_NOARGS, NULL}, + {NULL, NULL, 0, NULL}, +}; + +static struct PyModuleDef module = { + PyModuleDef_HEAD_INIT, + "detect_compiler", + NULL, + -1, + methods, +}; + +PyMODINIT_FUNC PyInit_detect_compiler(void) +{ + return PyModule_Create(&module); +} diff --git a/tests/packages/detect-compiler/meson.build b/tests/packages/detect-compiler/meson.build new file mode 100644 index 000000000..9f8b22eb5 --- /dev/null +++ b/tests/packages/detect-compiler/meson.build @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2023 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project( + 'detect-compiler', + 'c', + version: '1.0', +) + +py = import('python').find_installation() + +py.extension_module('detect_compiler', 'detect_compiler.c', install: true) diff --git a/tests/packages/detect-compiler/pyproject.toml b/tests/packages/detect-compiler/pyproject.toml new file mode 100644 index 000000000..79b6ea07f --- /dev/null +++ b/tests/packages/detect-compiler/pyproject.toml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2023 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] diff --git a/tests/test_project.py b/tests/test_project.py index a08129659..049e2e989 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: MIT +import ast +import os import platform import shutil import sys @@ -60,12 +62,26 @@ def test_unsupported_python_version(package_unsupported_python_version): def test_user_args(package_user_args, tmp_path, monkeypatch): project_run = mesonpy.Project._run - call_args_list = [] + cmds = [] + args = [] def wrapper(self, cmd): # intercept and filter out test arguments and forward the call - call_args_list.append(tuple(cmd)) - return project_run(self, [x for x in cmd if not x.startswith(('config-', 'cli-'))]) + if cmd[:2] == ['meson', 'compile']: + # when using meson compile instead of ninja directly, the + # arguments needs to be unmarshalled from the form used to + # pass them to the --ninja-args option + assert cmd[-1].startswith('--ninja-args=') + cmds.append(cmd[:2]) + args.append(ast.literal_eval(cmd[-1].split('=')[1])) + elif cmd[:1] == ['meson']: + cmds.append(cmd[:2]) + args.append(cmd[2:]) + else: + # direct ninja invocation + cmds.append([os.path.basename(cmd[0])]) + args.append(cmd[1:]) + return project_run(self, [x for x in cmd if not x.startswith(('config-', 'cli-', '--ninja-args'))]) monkeypatch.setattr(mesonpy.Project, '_run', wrapper) @@ -79,19 +95,32 @@ def wrapper(self, cmd): mesonpy.build_sdist(tmp_path, config_settings) mesonpy.build_wheel(tmp_path, config_settings) - expected = [ + # check that the right commands are executed, namely that 'meson + # compile' is used on Windows rather than a 'ninja' direct + # invocation. + assert cmds == [ # sdist: calls to 'meson setup' and 'meson dist' - ('config-setup', 'cli-setup'), - ('config-dist', 'cli-dist'), + ['meson', 'setup'], + ['meson', 'dist'], # wheel: calls to 'meson setup', 'meson compile', and 'meson install' - ('config-setup', 'cli-setup'), - ('config-compile', 'cli-compile'), - ('config-install', 'cli-install'), + ['meson', 'setup'], + ['meson', 'compile'] if platform.system() == 'Windows' else ['ninja'], + ['meson', 'install'] ] - for expected_args, call_args in zip(expected, call_args_list): + # check that the user options are passed to the invoked commands + expected = [ + # sdist: calls to 'meson setup' and 'meson dist' + ['config-setup', 'cli-setup'], + ['config-dist', 'cli-dist'], + # wheel: calls to 'meson setup', 'meson compile', and 'meson install' + ['config-setup', 'cli-setup'], + ['config-compile', 'cli-compile'], + ['config-install', 'cli-install'], + ] + for expected_args, cmd_args in zip(expected, args): for arg in expected_args: - assert arg in call_args + assert arg in cmd_args @pytest.mark.parametrize('package', ('top-level', 'meson-args')) @@ -190,3 +219,15 @@ def test_invalid_build_dir(package_pure, tmp_path, mocker): assert meson.call_args_list[0].args[1][1] == 'setup' assert '--reconfigure' not in meson.call_args_list[0].args[1] project.build() + + +@pytest.mark.skipif(not os.getenv('CI') or platform.system() != 'Windows', reason='Requires MSVC') +def test_compiler(venv, package_detect_compiler, tmp_path): + # Check that things are setup properly to use the MSVC compiler on + # Windows. This effectively means running the compilation step + # with 'meson compile' instead of 'ninja' on Windows. Run this + # test only on CI where we know that MSVC is available. + wheel = mesonpy.build_wheel(tmp_path, {'setup-args': ['--vsenv']}) + venv.pip('install', os.fspath(tmp_path / wheel)) + compiler = venv.python('-c', 'import detect_compiler; print(detect_compiler.compiler())').strip() + assert compiler == 'msvc'