diff --git a/mesonpy/_editable.py b/mesonpy/_editable.py index 73a764eb0..27238d7de 100644 --- a/mesonpy/_editable.py +++ b/mesonpy/_editable.py @@ -308,6 +308,26 @@ def find_spec( tree = self._rebuild() return find_spec(fullname, tree) + def _work_to_do(self, env: dict[str, str]) -> bool: + # Code is adapted from: + # https://github.com/mesonbuild/meson/blob/a35d4d368a21f4b70afa3195da4d6292a649cb4c/mesonbuild/mtest.py#L1635-L1636 + if sys.platform != 'win32': + dry_run_build_cmd = self._build_cmd + ['-n'] + else: + # On Windows meson compile is used so to do a dry run you need to add --ninja-args=-n without ignoring + # existing --ninja-args in self._build_cmd + ninja_index_and_arg_list = [(i, arg) for i, arg in enumerate(self._build_cmd) if arg.startswith('--ninja-args=')] + if not ninja_index_and_arg_list: + dry_run_build_cmd = self._build_cmd + ['--ninja-args=-n'] + else: + dry_run_build_cmd = self._build_cmd.copy() + # Last --ninja-args overrides all the previous ones, so need to modify only the last one to add -n + last_ninja_index, last_ninja_args = ninja_index_and_arg_list[-1] + dry_run_build_cmd[last_ninja_index] = last_ninja_args + ',-n' + + p = subprocess.run(dry_run_build_cmd, cwd=self._build_path, env=env, capture_output=True, check=True) + return b'ninja: no work to do.' not in p.stdout and b'samu: nothing to do' not in p.stdout + @functools.lru_cache(maxsize=1) def _rebuild(self) -> Node: # skip editable wheel lookup during rebuild: during the build @@ -317,12 +337,14 @@ def _rebuild(self) -> Node: env[MARKER] = os.pathsep.join((env.get(MARKER, ''), self._build_path)) if self._verbose or bool(env.get(VERBOSE, '')): - print('+ ' + ' '.join(self._build_cmd)) - stdout = None + # We want to show some output only if there is some work to do + if self._work_to_do(env): + module_names = ' '.join(sorted(self._top_level_modules)) + build_command = ' '.join(self._build_cmd) + print(f'meson-python: building {module_names} with {build_command!r}', flush=True) + subprocess.run(self._build_cmd, cwd=self._build_path, env=env) else: - stdout = subprocess.DEVNULL - - subprocess.run(self._build_cmd, cwd=self._build_path, env=env, stdout=stdout, check=True) + subprocess.run(self._build_cmd, cwd=self._build_path, env=env, stdout=subprocess.DEVNULL) install_plan_path = os.path.join(self._build_path, 'meson-info', 'intro-install_plan.json') with open(install_plan_path, 'r', encoding='utf8') as f: diff --git a/tests/test_editable.py b/tests/test_editable.py index 31c2ec3ce..a6c0c0c53 100644 --- a/tests/test_editable.py +++ b/tests/test_editable.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: MIT +import functools import os import pathlib import pkgutil @@ -13,7 +14,7 @@ from mesonpy import _editable -from .test_wheel import EXT_SUFFIX +from .test_wheel import EXT_SUFFIX, tag def test_walk(package_complex): @@ -61,12 +62,17 @@ def test_collect(package_complex): assert tree['complex']['more']['__init__.py'] == os.path.join(root, 'complex', 'more', '__init__.py') -def test_mesonpy_meta_finder(package_complex, tmp_path): +@pytest.mark.parametrize( + 'make_finder', + [_editable.MesonpyMetaFinder, functools.partial(_editable.MesonpyMetaFinder, verbose=True)] +) +def test_mesonpy_meta_finder(package_complex, tmp_path, make_finder): # build a package in a temporary directory mesonpy.Project(package_complex, tmp_path) # point the meta finder to the build directory - finder = _editable.MesonpyMetaFinder({'complex'}, os.fspath(tmp_path), ['ninja']) + build_cmd = ['meson', 'compile'] if sys.platform == 'win32' else ['ninja'] + finder = make_finder({'complex'}, os.fspath(tmp_path), build_cmd) # check repr assert repr(finder) == f'MesonpyMetaFinder({str(tmp_path)!r})' @@ -99,6 +105,9 @@ def test_mesonpy_meta_finder(package_complex, tmp_path): finally: # remove finder from the meta path del sys.meta_path[0] + # unload complex module and all its submodules to be able to run parametrized tests without side-effects + for module in ['complex', 'complex.test', 'complex.namespace', 'complex.namespace.foo']: + sys.modules.pop(module, None) def test_mesonpy_traversable(): @@ -194,6 +203,29 @@ def test_editble_reentrant(venv, editable_imports_itself_during_build): path.write_text(code) +@pytest.mark.skipif(tag.platform.startswith('musllinux'), reason='ninja -n segfaults on Alpine container') +def test_editable_verbose(venv, editable_complex, monkeypatch): + monkeypatch.setenv(_editable.VERBOSE, '1') + venv.pip('install', os.fspath(editable_complex)) + + # First import to make sure that the project is built + venv.python('-c', 'import complex') + + # Second import should have no output since the project has already been built + assert venv.python('-c', 'import complex').strip() == '' + + # Touch the pyx and make sure that the building info is seen + complex_package_dir = venv.python( + '-c', 'import os; import complex; print(os.path.dirname(complex.__file__))').strip() + cython_path = pathlib.Path(complex_package_dir).parent / 'test.pyx' + cython_path.touch() + output = venv.python('-c', 'import complex').strip() + assert output.startswith('meson-python: building complex with') + + # Another import without file changes should not show any output + assert venv.python('-c', 'import complex') == '' + + def test_editable_pkgutils_walk_packages(package_complex, tmp_path): # build a package in a temporary directory mesonpy.Project(package_complex, tmp_path)