From 6c0f5f762c8c9728404f25505421bf0d66fd14c4 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..9cdc146c 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 occurred 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 8d081873240ea4a948193dca1c0c167ae03128d7 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 Use the same spelling for "rebuilding" as the one used by ninja. The exception message and the ninja output may be emitted close to each other and it is odd to have the two spelled differently. Do not mention meson-python in the exception message. The fact that the editable package uses meson-python is an implementation detail for the user of the package and the wording could have been interpreted as a component of meson-python failed to build. --- 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 9cdc146c..cabc06c2 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}" editable 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 93e0edf75e16a8a3275f482c4d5d56d3db0435c9 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: