Skip to content

Commit afe4ef2

Browse files
committed
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 append it to the ImportError exception message. Fixes #820.
1 parent 070c597 commit afe4ef2

2 files changed

Lines changed: 62 additions & 4 deletions

File tree

mesonpy/_editable.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ def _work_to_do(self, env: dict[str, str]) -> bool:
319319
dry_run_build_cmd = self._build_cmd + ['-n']
320320
# Check adapted from
321321
# https://github.com/mesonbuild/meson/blob/a35d4d368a21f4b70afa3195da4d6292a649cb4c/mesonbuild/mtest.py#L1635-L1636
322-
p = subprocess.run(dry_run_build_cmd, cwd=self._build_path, env=env, capture_output=True)
322+
p = subprocess.run(dry_run_build_cmd, cwd=self._build_path, env=env, check=False, capture_output=True)
323323
return b'ninja: no work to do.' not in p.stdout and b'samu: nothing to do' not in p.stdout
324324

325325
@functools.lru_cache(maxsize=1)
@@ -332,15 +332,39 @@ def _rebuild(self) -> Node:
332332
env[MARKER] = os.pathsep.join((env.get(MARKER, ''), self._build_path))
333333

334334
if self._verbose or bool(env.get(VERBOSE, '')):
335+
log_path = None
335336
# We want to show some output only if there is some work to do.
336337
if self._work_to_do(env):
337338
build_command = ' '.join(self._build_cmd)
338339
print(f'meson-python: building {self._name}: {build_command}', flush=True)
339340
subprocess.run(self._build_cmd, cwd=self._build_path, env=env, check=True)
340341
else:
341-
subprocess.run(self._build_cmd, cwd=self._build_path, env=env, stdout=subprocess.DEVNULL, check=True)
342+
# Redirect build log to file.
343+
log_path = os.path.join(self._build_path, 'meson-python-build-log.txt')
344+
with open(log_path, 'w') as log:
345+
subprocess.run(self._build_cmd, cwd=self._build_path, env=env, check=True,
346+
stderr=subprocess.STDOUT, stdout=log)
342347
except subprocess.CalledProcessError as exc:
343-
raise ImportError(f're-building the {self._name} meson-python editable wheel package failed') from exc
348+
msg = f're-building the {self._name} meson-python editable wheel package failed'
349+
if log_path:
350+
with open(log_path, 'r') as log:
351+
# Skip to the error.
352+
for line in log:
353+
if line.startswith('FAILED: '):
354+
break
355+
else:
356+
# When no `FAILED: ` line is found, rewind to the
357+
# beginning of the log.
358+
log.seek(0)
359+
if line.strip().endswith(' build.ninja'):
360+
# When the error occureed when rebuilding `ninja.build`,
361+
# the meson output appears before the `FAILED: ` line.
362+
# Rewind the build log to the beginning to report the
363+
# error.
364+
log.seek(0)
365+
error = log.read()
366+
msg = f'{msg}:\n{error}'
367+
raise ImportError(msg) from exc
344368

345369
install_plan_path = os.path.join(self._build_path, 'meson-info', 'intro-install_plan.json')
346370
with open(install_plan_path, 'r', encoding='utf8') as f:

tests/test_editable.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,43 @@ def test_editable_rebuild_error(package_purelib_and_platlib, tmp_path, verbose):
335335
# Import module and trigger rebuild: the build fails and ImportErrror is raised
336336
stdout = io.StringIO()
337337
with redirect_stdout(stdout):
338-
with pytest.raises(ImportError, match='re-building the purelib-and-platlib '):
338+
with pytest.raises(ImportError, match='re-building the purelib-and-platlib ') as exc:
339339
import plat # noqa: F401
340340
assert not verbose or stdout.getvalue().startswith('meson-python: building ')
341+
assert verbose or 'ninja: build stopped: subcommand failed.' in exc.value.msg
342+
343+
finally:
344+
del sys.meta_path[0]
345+
sys.modules.pop('pure', None)
346+
path.write_text(code)
347+
348+
349+
def test_editable_reconfigure_error(package_purelib_and_platlib, tmp_path):
350+
with mesonpy._project({'builddir': os.fspath(tmp_path)}) as project:
351+
352+
finder = _editable.MesonpyMetaFinder(
353+
project._metadata.name, {'plat', 'pure'},
354+
os.fspath(tmp_path), project._build_command,
355+
verbose=False,
356+
)
357+
path = package_purelib_and_platlib / 'meson.build'
358+
code = path.read_text()
359+
360+
try:
361+
# Install editable hooks
362+
sys.meta_path.insert(0, finder)
363+
364+
# Emit an error during reconfigure
365+
with open(path, 'a') as f:
366+
f.write('\n\nerror(\'injected error\')\n')
367+
368+
# Import module and trigger rebuild: the build fails and ImportErrror is raised
369+
stdout = io.StringIO()
370+
with redirect_stdout(stdout):
371+
with pytest.raises(ImportError, match='re-building the purelib-and-platlib ') as exc:
372+
import plat # noqa: F401
373+
assert 'ERROR: Problem encountered: injected error' in exc.value.msg
374+
assert 'ninja: error: rebuilding \'build.ninja\': subcommand failed' in exc.value.msg
341375

342376
finally:
343377
del sys.meta_path[0]

0 commit comments

Comments
 (0)