diff --git a/mesonpy/_editable.py b/mesonpy/_editable.py index 7568d069..36534c78 100644 --- a/mesonpy/_editable.py +++ b/mesonpy/_editable.py @@ -319,7 +319,7 @@ def _work_to_do(self, env: dict[str, str]) -> bool: dry_run_build_cmd = self._build_cmd + ['-n'] # Check adapted from # https://github.com/mesonbuild/meson/blob/a35d4d368a21f4b70afa3195da4d6292a649cb4c/mesonbuild/mtest.py#L1635-L1636 - p = subprocess.run(dry_run_build_cmd, cwd=self._build_path, env=env, capture_output=True) + p = subprocess.run(dry_run_build_cmd, cwd=self._build_path, env=env, check=False, capture_output=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) @@ -332,15 +332,39 @@ def _rebuild(self) -> Node: env[MARKER] = os.pathsep.join((env.get(MARKER, ''), self._build_path)) if self._verbose or bool(env.get(VERBOSE, '')): + log_path = None # We want to show some output only if there is some work to do. if self._work_to_do(env): build_command = ' '.join(self._build_cmd) print(f'meson-python: building {self._name}: {build_command}', flush=True) subprocess.run(self._build_cmd, cwd=self._build_path, env=env, check=True) else: - subprocess.run(self._build_cmd, cwd=self._build_path, env=env, stdout=subprocess.DEVNULL, check=True) + # Redirect build log to file. + log_path = os.path.join(self._build_path, 'meson-logs', 'meson-python-build-log.txt') + with open(log_path, 'w') as log: + subprocess.run(self._build_cmd, cwd=self._build_path, env=env, check=True, + stderr=subprocess.STDOUT, stdout=log) except subprocess.CalledProcessError as exc: - raise ImportError(f're-building the {self._name} meson-python editable wheel package failed') from exc + msg = f're-building the {self._name} meson-python editable wheel package failed' + if log_path: + with open(log_path, 'r', encoding='utf8') as log: + # Skip to the error. + for line in log: + if line.startswith('FAILED: '): + break + else: + # When no `FAILED: ` line is found, rewind to the + # beginning of the log. + log.seek(0) + if line.strip().endswith(' build.ninja'): + # When the error occureed when rebuilding `ninja.build`, + # the meson output appears before the `FAILED: ` line. + # Rewind the build log to the beginning to report the + # error. + log.seek(0) + error = log.read() + msg = f'{msg}:\n{error}' + raise ImportError(msg) from exc 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 b8a14695..07c2c81e 100644 --- a/tests/test_editable.py +++ b/tests/test_editable.py @@ -335,9 +335,43 @@ def test_editable_rebuild_error(package_purelib_and_platlib, tmp_path, verbose): # Import module and trigger rebuild: the build fails and ImportErrror is raised stdout = io.StringIO() with redirect_stdout(stdout): - with pytest.raises(ImportError, match='re-building the purelib-and-platlib '): + with pytest.raises(ImportError, match='re-building the purelib-and-platlib ') as exc: import plat # noqa: F401 assert not verbose or stdout.getvalue().startswith('meson-python: building ') + assert verbose or 'ninja: build stopped: subcommand failed.' in exc.value.msg + + finally: + del sys.meta_path[0] + sys.modules.pop('pure', None) + path.write_text(code) + + +def test_editable_reconfigure_error(package_purelib_and_platlib, tmp_path): + with mesonpy._project({'builddir': os.fspath(tmp_path)}) as project: + + finder = _editable.MesonpyMetaFinder( + project._metadata.name, {'plat', 'pure'}, + os.fspath(tmp_path), project._build_command, + verbose=False, + ) + path = package_purelib_and_platlib / 'meson.build' + code = path.read_text() + + try: + # Install editable hooks + sys.meta_path.insert(0, finder) + + # Emit an error (with unicode characters) during reconfigure + with open(path, 'a', encoding='utf8') as f: + f.write('\n\nerror(\'injected error \N{BOMB}\')\n') + + # Import module and trigger rebuild: the build fails and ImportErrror is raised + stdout = io.StringIO() + with redirect_stdout(stdout): + with pytest.raises(ImportError, match='re-building the purelib-and-platlib ') as exc: + import plat # noqa: F401 + assert 'ERROR: Problem encountered: injected error' in exc.value.msg + assert 'ninja: error: rebuilding \'build.ninja\': subcommand failed' in exc.value.msg finally: del sys.meta_path[0]