Fine-tune the error message when trying to build outside the project root

We try to backtrack through the filesystem to find the correct directory
to build in, and suggest this as a possible diagnostic. However, our
current heuristic relies on parsing the raw file with string matching to
see if it starts with `project(`, and this may or may not actually work.

Instead, do a bit of recursion and parse each candidate with mparser,
then check if the first node of *that* file is a project() function.

This makes us resilient to a common case: where the root meson.build is
entirely valid, but, the first line is a comment containing e.g. SPDX
license headers and a simple string comparison simply does not cut it.

Fixes the bad error message from #12441, which was supposed to provide
more guidance but did not.
pull/12442/head
Eli Schwartz 1 year ago
parent 74712f2dbc
commit fb1c6e32f4
No known key found for this signature in database
GPG Key ID: CEB167EFB5722BD6
  1. 20
      mesonbuild/interpreterbase/interpreterbase.py
  2. 8
      unittests/platformagnostictests.py

@ -132,18 +132,28 @@ class InterpreterBase:
self.evaluate_codeblock(self.ast, end=1) self.evaluate_codeblock(self.ast, end=1)
def sanity_check_ast(self) -> None: def sanity_check_ast(self) -> None:
if not isinstance(self.ast, mparser.CodeBlockNode): def _is_project(ast: mparser.CodeBlockNode) -> object:
if not isinstance(ast, mparser.CodeBlockNode):
raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.') raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.')
if not self.ast.lines: if not ast.lines:
raise InvalidCode('No statements in code.') raise InvalidCode('No statements in code.')
first = self.ast.lines[0] first = ast.lines[0]
if not isinstance(first, mparser.FunctionNode) or first.func_name.value != 'project': return isinstance(first, mparser.FunctionNode) and first.func_name.value == 'project'
if not _is_project(self.ast):
p = pathlib.Path(self.source_root).resolve() p = pathlib.Path(self.source_root).resolve()
found = p found = p
for parent in p.parents: for parent in p.parents:
if (parent / 'meson.build').is_file(): if (parent / 'meson.build').is_file():
with open(parent / 'meson.build', encoding='utf-8') as f: with open(parent / 'meson.build', encoding='utf-8') as f:
if f.readline().startswith('project('): code = f.read()
try:
ast = mparser.Parser(code, 'empty').parse()
except mparser.ParseException:
continue
if _is_project(ast):
found = parent found = parent
break break
else: else:

@ -278,3 +278,11 @@ class PlatformAgnosticTests(BasePlatformTests):
self.meson_native_files.append(os.path.join(testdir, 'nativefile.ini')) self.meson_native_files.append(os.path.join(testdir, 'nativefile.ini'))
out = self.init(testdir, allow_fail=True) out = self.init(testdir, allow_fail=True)
self.assertNotIn('Unhandled python exception', out) self.assertNotIn('Unhandled python exception', out)
def test_error_configuring_subdir(self):
testdir = os.path.join(self.common_test_dir, '152 index customtarget')
out = self.init(os.path.join(testdir, 'subdir'), allow_fail=True)
self.assertIn('first statement must be a call to project()', out)
# provide guidance diagnostics by finding a file whose first AST statement is project()
self.assertIn(f'Did you mean to run meson from the directory: "{testdir}"?', out)

Loading…
Cancel
Save