From 7dd87117e923e1b3e0251ce2893eed1fec9f2974 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 28 Jun 2026 15:44:37 +0200 Subject: [PATCH 1/3] ENH: capture and report build errors when rebuilding editable wheels When editable-verbose is not enabled, redirect the build output to a file. When the build fails, parse this file to look for the build error and add it as a note to the ImportError exception. For Python versions that do not support exception notes, the error is appended to the exception message. Fixes #820. --- mesonpy/_editable.py | 36 ++++++++++++++++++++++++++++++++---- tests/test_editable.py | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/mesonpy/_editable.py b/mesonpy/_editable.py index 7568d069..5442db6c 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,43 @@ 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) - except subprocess.CalledProcessError as exc: - raise ImportError(f're-building the {self._name} meson-python editable wheel package failed') from exc + # 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 err: + 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() + if sys.version_info >= (3, 11): + exc = ImportError(msg) + exc.add_note(error) + raise exc from err + msg = f'{msg}:\n{error}' + raise ImportError(msg) from err 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..1ddc3e6a 100644 --- a/tests/test_editable.py +++ b/tests/test_editable.py @@ -335,10 +335,50 @@ 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 ') + if sys.version_info >= (3, 11): + assert verbose or hasattr(exc.value, '__notes__') + assert verbose or 'ninja: build stopped: subcommand failed.' in exc.value.__notes__[0] + else: + 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 + msg = exc.value.__notes__[0] if sys.version_info >= (3, 11) else exc.value.msg + assert 'ERROR: Problem encountered: injected error' in msg + assert 'ninja: error: rebuilding \'build.ninja\': subcommand failed' in msg + finally: del sys.meta_path[0] sys.modules.pop('pure', None) From d6319b09f37520c2a7709f52ed7a16af1baa9a14 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Thu, 23 Jul 2026 10:57:56 +0200 Subject: [PATCH 2/3] ENH: tweak exception message on editable re-build failure --- mesonpy/_editable.py | 2 +- tests/test_editable.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mesonpy/_editable.py b/mesonpy/_editable.py index 5442db6c..7e00b236 100644 --- a/mesonpy/_editable.py +++ b/mesonpy/_editable.py @@ -345,7 +345,7 @@ def _rebuild(self) -> Node: subprocess.run(self._build_cmd, cwd=self._build_path, env=env, check=True, stderr=subprocess.STDOUT, stdout=log) except subprocess.CalledProcessError as err: - msg = f're-building the {self._name} meson-python editable wheel package failed' + msg = f'rebuilding 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. diff --git a/tests/test_editable.py b/tests/test_editable.py index 1ddc3e6a..b64124bb 100644 --- a/tests/test_editable.py +++ b/tests/test_editable.py @@ -335,7 +335,7 @@ 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 ') as exc: + with pytest.raises(ImportError, match='rebuilding the "purelib-and-platlib" ') as exc: import plat # noqa: F401 assert not verbose or stdout.getvalue().startswith('meson-python: building ') @@ -373,7 +373,7 @@ def test_editable_reconfigure_error(package_purelib_and_platlib, tmp_path): # 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: + with pytest.raises(ImportError, match='rebuilding the "purelib-and-platlib" ') as exc: import plat # noqa: F401 msg = exc.value.__notes__[0] if sys.version_info >= (3, 11) else exc.value.msg assert 'ERROR: Problem encountered: injected error' in msg From 8d1bc1864db37a78b8075a40fe602da7968e57ea Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Thu, 23 Jul 2026 11:19:38 +0200 Subject: [PATCH 3/3] MAINT: fix typo --- tests/test_editable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_editable.py b/tests/test_editable.py index b64124bb..8d55cca4 100644 --- a/tests/test_editable.py +++ b/tests/test_editable.py @@ -332,7 +332,7 @@ def test_editable_rebuild_error(package_purelib_and_platlib, tmp_path, verbose): # Insert invalid code in the extension module source code path.write_text('return') - # Import module and trigger rebuild: the build fails and ImportErrror is raised + # Import module and trigger rebuild: the build fails and ImportError is raised stdout = io.StringIO() with redirect_stdout(stdout): with pytest.raises(ImportError, match='rebuilding the "purelib-and-platlib" ') as exc: