From 878c1604e6348f98602bbdd16d92fc63ed15ebea Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Sun, 26 Feb 2023 01:36:47 -0500 Subject: [PATCH] handle meson_version even when the build file fails to parse If the meson.build file is sufficiently "broken", even attempting to lex and parse it will totally fail, and we error out without getting the opportunity to evalaute the project() function. This can fairly easily happen if we add new grammar to the syntax, which old versions of meson cannot understand. Setting a minimum meson_version doesn't help, because people with a too-old version of meson get parser errors instead of advice about upgrading meson. Examples of this include adding dict support to meson. There are two general approaches to solving this issue, one of which projects are empowered to do: - refactor the project to place too-new syntax in a subdir() loaded build file, so the root file can be interpreted - teach meson to catch errors in building the initial AST, and just load enough of the AST to check for meson_version advice This implements the latter, allowing to future-proof the build grammar. --- mesonbuild/interpreter/interpreter.py | 23 +++++++++++++++---- mesonbuild/interpreterbase/interpreterbase.py | 13 ++++++++--- .../failing/130 invalid ast/meson.build | 3 +++ test cases/failing/130 invalid ast/test.json | 9 ++++++++ 4 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 test cases/failing/130 invalid ast/meson.build create mode 100644 test cases/failing/130 invalid ast/test.json diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 9e20446b3..c6602a5b9 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -525,6 +525,24 @@ class Interpreter(InterpreterBase, HoldableObject): else: raise InterpreterException(f'Module returned a value of unknown type {v!r}.') + def handle_meson_version(self, pv: str, location: mparser.BaseNode) -> None: + if not mesonlib.version_compare(coredata.version, pv): + raise InterpreterException.from_node(f'Meson version is {coredata.version} but project requires {pv}', node=location) + + def handle_meson_version_from_ast(self) -> None: + if not self.ast.lines: + return + project = self.ast.lines[0] + # first line is always project() + if not isinstance(project, mparser.FunctionNode): + return + for kw, val in project.args.kwargs.items(): + assert isinstance(kw, mparser.IdNode), 'for mypy' + if kw.value == 'meson_version': + # mypy does not understand "and isinstance" + if isinstance(val, mparser.StringNode): + self.handle_meson_version(val.value, val) + def get_build_def_files(self) -> mesonlib.OrderedSet[str]: return self.build_def_files @@ -1151,10 +1169,7 @@ class Interpreter(InterpreterBase, HoldableObject): # This needs to be evaluated as early as possible, as meson uses this # for things like deprecation testing. if kwargs['meson_version']: - cv = coredata.version - pv = kwargs['meson_version'] - if not mesonlib.version_compare(cv, pv): - raise InterpreterException(f'Meson version is {cv} but project requires {pv}') + self.handle_meson_version(kwargs['meson_version'], node) mesonlib.project_meson_versions[self.subproject] = kwargs['meson_version'] if os.path.exists(self.option_file): diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py index c8ef30309..666045fe4 100644 --- a/mesonbuild/interpreterbase/interpreterbase.py +++ b/mesonbuild/interpreterbase/interpreterbase.py @@ -16,8 +16,7 @@ # or an interpreter-based tool. from __future__ import annotations -from .. import mparser, mesonlib -from .. import environment +from .. import environment, mparser, mesonlib from .baseobjects import ( InterpreterObject, @@ -103,6 +102,10 @@ class InterpreterBase: # current meson version target within that if-block. self.tmp_meson_version = None # type: T.Optional[str] + def handle_meson_version_from_ast(self, strict: bool = True) -> None: + # do nothing in an AST interpreter + return + def load_root_meson_file(self) -> None: mesonfile = os.path.join(self.source_root, self.subdir, environment.build_filename) if not os.path.isfile(mesonfile): @@ -114,8 +117,12 @@ class InterpreterBase: assert isinstance(code, str) try: self.ast = mparser.Parser(code, mesonfile).parse() - except mesonlib.MesonException as me: + except mparser.ParseException as me: me.file = mesonfile + # try to detect parser errors from new syntax added by future + # meson versions, and just tell the user to update meson + self.ast = me.ast + self.handle_meson_version_from_ast() raise me def parse_project(self) -> None: diff --git a/test cases/failing/130 invalid ast/meson.build b/test cases/failing/130 invalid ast/meson.build new file mode 100644 index 000000000..06011c2ca --- /dev/null +++ b/test cases/failing/130 invalid ast/meson.build @@ -0,0 +1,3 @@ +project('invalid ast crash', meson_version: '0.1.0') + += >%@ diff --git a/test cases/failing/130 invalid ast/test.json b/test cases/failing/130 invalid ast/test.json new file mode 100644 index 000000000..025d50cca --- /dev/null +++ b/test cases/failing/130 invalid ast/test.json @@ -0,0 +1,9 @@ +{ + "stdout": [ + { + "match": "re", + "line": "test cases/failing/130 invalid ast/meson.build:1:44: ERROR: Meson version is [0-9.]+ but project requires 0.1.0" + } + ] +} +