Skip to content

Commit 8824bc2

Browse files
henryiiiFFY00
andauthored
ENH: use system ninja if adequate
PR #175 Co-authored-by: Filipe Laíns <[email protected]>
1 parent 26a0897 commit 8824bc2

File tree

4 files changed

+105
-38
lines changed

4 files changed

+105
-38
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[flake8]
22
max-line-length = 127
3-
max-complexity = 10
3+
max-complexity = 12
44
extend-ignore = E203

mesonpy/__init__.py

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,6 @@
6464
__version__ = '0.11.0.dev0'
6565

6666

67-
class _depstr:
68-
"""Namespace that holds the requirement strings for dependencies we *might*
69-
need at runtime. Having them in one place makes it easier to update.
70-
"""
71-
patchelf = 'patchelf >= 0.11.0'
72-
wheel = 'wheel >= 0.36.0' # noqa: F811
73-
74-
7567
_COLORS = {
7668
'cyan': '\33[36m',
7769
'yellow': '\33[93m',
@@ -82,6 +74,16 @@ class _depstr:
8274
'reset': '\33[0m',
8375
}
8476
_NO_COLORS = {color: '' for color in _COLORS}
77+
_NINJA_REQUIRED_VERSION = '1.8.2'
78+
79+
80+
class _depstr:
81+
"""Namespace that holds the requirement strings for dependencies we *might*
82+
need at runtime. Having them in one place makes it easier to update.
83+
"""
84+
patchelf = 'patchelf >= 0.11.0'
85+
wheel = 'wheel >= 0.36.0' # noqa: F811
86+
ninja = f'ninja >= {_NINJA_REQUIRED_VERSION}'
8587

8688

8789
def _init_colors() -> Dict[str, str]:
@@ -565,6 +567,12 @@ def __init__(
565567
self._build_dir = pathlib.Path(build_dir).absolute() if build_dir else (self._working_dir / 'build')
566568
self._install_dir = self._working_dir / 'install'
567569
self._meson_native_file = self._source_dir / '.mesonpy-native-file.ini'
570+
self._env = os.environ.copy()
571+
572+
# prepare environment
573+
ninja_path = _env_ninja_command()
574+
if ninja_path is not None:
575+
self._env.setdefault('NINJA', str(ninja_path))
568576

569577
# load config -- PEP 621 support is optional
570578
self._config = tomllib.loads(self._source_dir.joinpath('pyproject.toml').read_text())
@@ -637,7 +645,7 @@ def _get_config_key(self, key: str) -> Any:
637645
def _proc(self, *args: str) -> None:
638646
"""Invoke a subprocess."""
639647
print('{cyan}{bold}+ {}{reset}'.format(' '.join(args), **_STYLES))
640-
subprocess.check_call(list(args))
648+
subprocess.check_call(list(args), env=self._env)
641649

642650
def _meson(self, *args: str) -> None:
643651
"""Invoke Meson."""
@@ -957,6 +965,37 @@ def _validate_string_collection(key: str) -> None:
957965
yield project
958966

959967

968+
def _env_ninja_command(*, version: str = _NINJA_REQUIRED_VERSION) -> Optional[pathlib.Path]:
969+
"""
970+
Returns the path to ninja, or None if no ninja found.
971+
"""
972+
required_version = tuple(int(v) for v in version.split('.'))
973+
env_ninja = os.environ.get('NINJA', None)
974+
ninja_candidates = [env_ninja] if env_ninja else ['ninja', 'ninja-build', 'samu']
975+
for ninja in ninja_candidates:
976+
ninja_path = shutil.which(ninja)
977+
if ninja_path is None:
978+
continue
979+
980+
result = subprocess.run([ninja_path, '--version'], check=False, text=True, capture_output=True)
981+
982+
try:
983+
candidate_version = tuple(int(x) for x in result.stdout.split('.')[:3])
984+
except ValueError:
985+
continue
986+
if candidate_version < required_version:
987+
continue
988+
return pathlib.Path(ninja_path)
989+
990+
return None
991+
992+
993+
def get_requires_for_build_sdist(
994+
config_settings: Optional[Dict[str, str]] = None,
995+
) -> List[str]:
996+
return [_depstr.ninja] if _env_ninja_command() is None else []
997+
998+
960999
def build_sdist(
9611000
sdist_directory: str,
9621001
config_settings: Optional[Dict[Any, Any]] = None,
@@ -972,12 +1011,24 @@ def get_requires_for_build_wheel(
9721011
config_settings: Optional[Dict[str, str]] = None,
9731012
) -> List[str]:
9741013
dependencies = [_depstr.wheel]
975-
with _project(config_settings) as project:
976-
if not project.is_pure and platform.system() == 'Linux':
977-
# we may need patchelf
978-
if not shutil.which('patchelf'): # XXX: This is slightly dangerous.
979-
# patchelf not already acessible on the system
1014+
1015+
if _env_ninja_command() is None:
1016+
dependencies.append(_depstr.ninja)
1017+
1018+
if sys.platform.startswith('linux'):
1019+
# we may need patchelf
1020+
if not shutil.which('patchelf'):
1021+
# patchelf not already accessible on the system
1022+
if _env_ninja_command() is not None:
1023+
# we have ninja available, so we can run Meson and check if the project needs patchelf
1024+
with _project(config_settings) as project:
1025+
if not project.is_pure:
1026+
dependencies.append(_depstr.patchelf)
1027+
else:
1028+
# we can't check if the project needs patchelf, so always add it
1029+
# XXX: wait for https://github.com/mesonbuild/meson/pull/10779
9801030
dependencies.append(_depstr.patchelf)
1031+
9811032
return dependencies
9821033

9831034

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ build-backend = 'mesonpy'
33
backend-path = ['.']
44
requires = [
55
'meson>=0.63.3',
6-
'ninja',
76
'pyproject-metadata>=0.5.0',
87
'tomli>=1.0.0; python_version<"3.11"',
98
'typing-extensions>=3.7.4; python_version<"3.8"',
@@ -27,7 +26,6 @@ classifiers = [
2726
dependencies = [
2827
'colorama; os_name == "nt"',
2928
'meson>=0.63.3',
30-
'ninja',
3129
'pyproject-metadata>=0.5.0', # not a hard dependency, only needed for projects that use PEP 621 metadata
3230
'tomli>=1.0.0; python_version<"3.11"',
3331
'typing-extensions>=3.7.4; python_version<"3.8"',
@@ -47,6 +45,7 @@ test = [
4745
'Cython',
4846
'pyproject-metadata>=0.6.1',
4947
'wheel',
48+
'ninja',
5049
]
5150
docs = [
5251
'furo>=2021.08.31',

tests/test_pep517.py

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# SPDX-License-Identifier: MIT
22

3-
import platform
3+
import shutil
4+
import subprocess
5+
import sys
6+
7+
from typing import List
48

59
import pytest
610

@@ -9,28 +13,41 @@
913
from .conftest import cd_package
1014

1115

12-
if platform.system() == 'Linux':
13-
VENDORING_DEPS = {mesonpy._depstr.patchelf}
14-
else:
15-
VENDORING_DEPS = set()
16+
@pytest.mark.parametrize('package', ['pure', 'library'])
17+
@pytest.mark.parametrize('system_patchelf', ['patchelf', None], ids=['patchelf', 'nopatchelf'])
18+
@pytest.mark.parametrize('ninja', [None, '1.8.1', '1.8.3'], ids=['noninja', 'oldninja', 'newninja'])
19+
def test_get_requires_for_build_wheel(monkeypatch, package, system_patchelf, ninja):
20+
def which(prog: str) -> bool:
21+
if prog == 'patchelf':
22+
return system_patchelf
23+
if prog == 'ninja':
24+
return ninja and 'ninja'
25+
if prog in ('ninja-build', 'samu'):
26+
return None
27+
# smoke check for the future if we add another usage
28+
raise AssertionError(f'Called with {prog}, tests not expecting that usage')
29+
30+
def run(cmd: List[str], *args: object, **kwargs: object) -> subprocess.CompletedProcess:
31+
if cmd != ['ninja', '--version']:
32+
# smoke check for the future if we add another usage
33+
raise AssertionError(f'Called with {cmd}, tests not expecting that usage')
34+
return subprocess.CompletedProcess(cmd, 0, f'{ninja}\n', '')
35+
36+
monkeypatch.setattr(shutil, 'which', which)
37+
monkeypatch.setattr(subprocess, 'run', run)
38+
39+
expected = {mesonpy._depstr.wheel}
1640

41+
ninja_available = ninja is not None and [int(x) for x in ninja.split('.')] >= [1, 8, 2]
1742

18-
@pytest.mark.parametrize(
19-
('package', 'system_patchelf', 'expected'),
20-
[
21-
('pure', True, set()), # pure and system patchelf
22-
('library', True, set()), # not pure and system patchelf
23-
('pure', False, set()), # pure and no system patchelf
24-
('library', False, VENDORING_DEPS), # not pure and no system patchelf
25-
]
26-
)
27-
def test_get_requires_for_build_wheel(mocker, package, expected, system_patchelf):
28-
mock = mocker.patch('shutil.which', return_value=system_patchelf)
43+
if not ninja_available:
44+
expected |= {mesonpy._depstr.ninja}
2945

30-
if mock.called: # sanity check for the future if we add another usage
31-
mock.assert_called_once_with('patchelf')
46+
if (
47+
system_patchelf is None and sys.platform.startswith('linux')
48+
and (not ninja_available or (ninja_available and package != 'pure'))
49+
):
50+
expected |= {mesonpy._depstr.patchelf}
3251

3352
with cd_package(package):
34-
assert set(mesonpy.get_requires_for_build_wheel()) == expected | {
35-
mesonpy._depstr.wheel,
36-
}
53+
assert set(mesonpy.get_requires_for_build_wheel()) == expected

0 commit comments

Comments
 (0)