Skip to content

Commit 2f3dce2

Browse files
committed
ENH: use system ninja if adequate
Signed-off-by: Henry Schreiner <[email protected]>
1 parent 71c6a3a commit 2f3dce2

File tree

5 files changed

+151
-34
lines changed

5 files changed

+151
-34
lines changed

mesonpy/__init__.py

+36-7
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class _depstr:
6969
"""
7070
patchelf = 'patchelf >= 0.11.0'
7171
wheel = 'wheel >= 0.36.0' # noqa: F811
72+
ninja = 'ninja >= 1.8.2'
7273

7374

7475
_COLORS = {
@@ -995,6 +996,34 @@ def _project(config_settings: Optional[Dict[Any, Any]]) -> Iterator[Project]:
995996
yield project
996997

997998

999+
def _get_ninja_if_needed() -> List[str]:
1000+
env_ninja = os.environ.get('NINJA', None)
1001+
ninja_candidates = [env_ninja] if env_ninja else ['ninja', 'ninja-build', 'samu']
1002+
for ninja in ninja_candidates:
1003+
ninja_path = shutil.which(ninja)
1004+
if ninja_path is None:
1005+
continue
1006+
1007+
result = subprocess.run([ninja_path, '--version'], check=False, text=True, capture_output=True)
1008+
1009+
try:
1010+
candidate_version = tuple(int(x) for x in result.stdout.split('.')[:3])
1011+
except ValueError:
1012+
# The meson search function skips forward if the version is not readable or too low
1013+
continue
1014+
if candidate_version < (1, 8, 2):
1015+
continue
1016+
return []
1017+
1018+
return [_depstr.ninja]
1019+
1020+
1021+
def get_requires_for_build_sdist(
1022+
config_settings: Optional[Dict[str, str]] = None,
1023+
) -> List[str]:
1024+
return _get_ninja_if_needed()
1025+
1026+
9981027
def build_sdist(
9991028
sdist_directory: str,
10001029
config_settings: Optional[Dict[Any, Any]] = None,
@@ -1009,13 +1038,13 @@ def build_sdist(
10091038
def get_requires_for_build_wheel(
10101039
config_settings: Optional[Dict[str, str]] = None,
10111040
) -> List[str]:
1012-
dependencies = [_depstr.wheel]
1013-
with _project(config_settings) as project:
1014-
if not project.is_pure and platform.system() == 'Linux':
1015-
# we may need patchelf
1016-
if not shutil.which('patchelf'): # XXX: This is slightly dangerous.
1017-
# patchelf not already acessible on the system
1018-
dependencies.append(_depstr.patchelf)
1041+
dependencies = [_depstr.wheel, *_get_ninja_if_needed()]
1042+
if sys.platform.startswith('linux'):
1043+
# we may need patchelf
1044+
if not shutil.which('patchelf'):
1045+
# patchelf not already accessible on the system
1046+
dependencies.append(_depstr.patchelf)
1047+
10191048
return dependencies
10201049

10211050

pyproject.toml

+4-3
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"',
@@ -42,11 +40,14 @@ test = [
4240
'pytest',
4341
'pytest-cov',
4442
'pytest-mock',
43+
'pytest-virtualenv',
4544
'GitPython',
4645
'auditwheel',
4746
'Cython',
4847
'pyproject-metadata>=0.6.1',
49-
'importlib_metadata; python_version<"3.8"'
48+
'importlib_metadata; python_version<"3.8"',
49+
'ninja',
50+
'build',
5051
]
5152
docs = [
5253
'furo>=2021.08.31',

tests/conftest.py

+56
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import os.path
66
import pathlib
77
import shutil
8+
import subprocess
9+
import sys
810
import tempfile
911
import venv
1012

@@ -103,3 +105,57 @@ def fixture(tmp_dir_session):
103105
globals()[f'package_{normalized}'] = generate_package_fixture(package)
104106
globals()[f'sdist_{normalized}'] = generate_sdist_fixture(package)
105107
globals()[f'wheel_{normalized}'] = generate_wheel_fixture(package)
108+
109+
110+
@pytest.fixture(scope='session')
111+
def pep518_wheelhouse(tmpdir_factory):
112+
wheelhouse = tmpdir_factory.mktemp('wheelhouse')
113+
dist = tmpdir_factory.mktemp('dist')
114+
subprocess.run(
115+
[sys.executable, '-m', 'build', '--wheel', '--outdir', str(dist)],
116+
cwd=str(package_dir.parent.parent),
117+
check=True,
118+
)
119+
(wheel_path,) = dist.visit('*.whl')
120+
subprocess.run(
121+
[
122+
sys.executable,
123+
'-m',
124+
'pip',
125+
'download',
126+
'-q',
127+
'-d',
128+
str(wheelhouse),
129+
str(wheel_path),
130+
],
131+
check=True,
132+
)
133+
subprocess.run(
134+
[
135+
sys.executable,
136+
'-m',
137+
'pip',
138+
'download',
139+
'-q',
140+
'-d',
141+
str(wheelhouse),
142+
'build',
143+
'colorama',
144+
'meson',
145+
'ninja',
146+
'patchelf',
147+
'pyproject-metadata',
148+
'tomli',
149+
'typing-extensions',
150+
'wheel',
151+
],
152+
check=True,
153+
)
154+
return str(wheelhouse)
155+
156+
157+
@pytest.fixture
158+
def pep518(pep518_wheelhouse, monkeypatch):
159+
monkeypatch.setenv('PIP_FIND_LINKS', pep518_wheelhouse)
160+
monkeypatch.setenv('PIP_NO_INDEX', 'true')
161+
return pep518_wheelhouse

tests/test_pep517.py

+34-24
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,34 @@
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-
17-
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)
29-
30-
if mock.called: # sanity check for the future if we add another usage
31-
mock.assert_called_once_with('patchelf')
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}
40+
if system_patchelf is None and sys.platform.startswith('linux'):
41+
expected |= {mesonpy._depstr.patchelf}
42+
if ninja is None or [int(x) for x in ninja.split('.')] < [1, 8, 2]:
43+
expected |= {mesonpy._depstr.ninja}
3244

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

tests/test_pep518.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pytest
2+
3+
from .conftest import cd_package
4+
5+
6+
@pytest.mark.parametrize(
7+
('package'),
8+
[
9+
'scipy-like',
10+
]
11+
)
12+
@pytest.mark.parametrize(
13+
'build_args', ['', '--wheel'], ids=['sdist_to_wheel', 'wheel_directly']
14+
)
15+
def test_pep518(pep518, virtualenv, package, build_args, tmp_path):
16+
dist = tmp_path / 'dist'
17+
18+
virtualenv.run('python -m pip install build')
19+
20+
with cd_package(package) as package_dir:
21+
virtualenv.run(f'python -m build --outdir={dist} {build_args}', cwd=package_dir)

0 commit comments

Comments
 (0)