Skip to content

BUG: Use meson compile wrapper on Windows #371

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

Merged
merged 2 commits into from
Apr 10, 2023
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
55 changes: 55 additions & 0 deletions docs/how-to-guides/meson-args.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it actually only GCC/LLVM, or any other compiler? I'd expect the latter. If so, maybe rephrase like "When no other compiler known to Meson (e.g., GCC or Clang-cl) is found on ...."

Copy link
Contributor

Choose a reason for hiding this comment

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

The same info is repeated under Examples, so if editing here then it needs editing further down too.

Copy link
Member

@eli-schwartz eli-schwartz Apr 10, 2023

Choose a reason for hiding this comment

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

In terms of the code implementation, we quite literally check for shutil.which with any of cc, gcc, clang, clang-cl. The remaining autodetected default compilers for C are icl and pgcc, but icl ships with its own cl.exe (super "helpful" as noted by the code comments in detect.py) and meson will prefer cl.exe over pgcc anyway, if it can.

The --vsenv code is not kept in sync with detect.py's list of default compiler search names, whether it "should" do so is another question (that I don't have an opinion on, to be clear. :D)

Copy link
Member

Choose a reason for hiding this comment

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

I haven't found detailed documentation for the compiler auto detection. I had a look at the code, which behaves as @eli-schwartz describes https://github.com/mesonbuild/meson/blob/eb472a133f45826a246b40c3d775eebf098fd6b1/mesonbuild/utils/vsenv.py#L43-L51 I don't know how much of this is appropriate to document on the meson-python side, given that it is not explicitly documented and that I don't know how much of it is susceptible of change in Meson. I thought that GCC and LLVM covered the most popular cased and anyone using a less common compiler would know what they are doing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the explantions both - looks like we're good then with the current docs.

I don't know how much of this is appropriate to document on the meson-python side, given that it is not explicitly documented and that I don't know how much of it is susceptible of change in Meson.

agreed

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
<vsenv-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
========
Expand Down Expand Up @@ -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" .
39 changes: 27 additions & 12 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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')

Expand All @@ -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'))

Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions tests/packages/detect-compiler/detect_compiler.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2023 The meson-python developers
//
// SPDX-License-Identifier: MIT

#include <Python.h>

#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);
}
13 changes: 13 additions & 0 deletions tests/packages/detect-compiler/meson.build
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions tests/packages/detect-compiler/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2023 The meson-python developers
#
# SPDX-License-Identifier: MIT

[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']
63 changes: 52 additions & 11 deletions tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#
# SPDX-License-Identifier: MIT

import ast
import os
import platform
import shutil
import sys
Expand Down Expand Up @@ -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]))
Copy link
Contributor

Choose a reason for hiding this comment

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

slightly painful I guess, but fine in a test case

Copy link
Member

Choose a reason for hiding this comment

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

What are you referring to? The not-so-nice part is marshaling the arguments into the format expected by Meson. Parsing them back is just a consequence of that. However, the code for either does not look that horrible.

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)

Expand All @@ -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'))
Expand Down Expand Up @@ -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'