Skip to content

Commit

Permalink
interpreter: don't create new Interpreter instances for subprojects
Browse files Browse the repository at this point in the history
Instead, replace the state with new state. This means we don't have to
re-calculate any of the methods or tell the sub-interpreter about the
state of the super-interpreter, it's already there. Since all of the
state that is not an implementation detail of the Interpreter itself is
no longer bound to the interpreter this is all very trivial.

This fixes a number of potential bugs created by side-effects from a
failing, optional, subproject (Assume A => B => C, where C configures
successfully but B fails after that)

 1. If B called `meson_override_dependency()` or
    `meson.override_find_program()` those overrides would remain, even
    though the targets would not exist.
 2. Targets from C would be built, even though B will not be, and A does
    not use them.
 3. If C called `meson.override_dependency()` or
    `meson.override_find_program()` these overrides would remain, and
    could affect A
 4. If B is optional, but a subproject D is called if B fails, and D
    calls C with different `default_options` than B, D will get a copy
    of C using those different options
 5. Summary options for C will be printed, even though it is unused
  • Loading branch information
dcbaker committed Feb 14, 2024
1 parent 07f851a commit 264647d
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 45 deletions.
80 changes: 41 additions & 39 deletions mesonbuild/interpreter/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,38 +269,40 @@ def __init__(
ast: T.Optional[mparser.CodeBlockNode] = None,
relaxations: T.Optional[T.Set[InterpreterRuleRelaxation]] = None,
user_defined_options: T.Optional[coredata.SharedCMDOptions] = None,
world: T.Optional[GlobalInterpreterState] = None,
) -> None:
self.state = InterpreterState(
LocalInterpreterState(
subproject, subdir,
rule_relaxations=relaxations or set(),
),
world or GlobalInterpreterState(
GlobalInterpreterState(
_build.environment.get_source_dir(), _build,
user_defined_options, backend)
)
super().__init__()
self.modules: T.Dict[str, NewExtensionModule] = {}
if ast is None:
self.load_root_meson_file()
else:
self.state.local.ast = ast
self.sanity_check_ast()
self.builtin.update({'meson': MesonMain(self)})
# Passed from the outside, only used in subprojects.
if default_project_options:
self.state.local.default_subproject_options.update(default_project_options)
self.build_func_dict()
self.build_holder_map()
self.__init(ast)
self._redetect_machines()

# build_def_files needs to be defined before parse_project is called
#
# For non-meson subprojects, we'll be using the ast. Even if it does
# exist we don't want to add a dependency on it, it's autogenerated
# from the actual build files, and is just for reference.
def __init(self, ast: T.Optional[mparser.CodeBlockNode]) -> None:
"""Set and check the AST, then parse the `project()` function.
Helper used in initial setup and in subproject setup.
:param ast: A pre-calculated AST instance (from a translated build system) or nothing
"""
if ast is None:
self.load_root_meson_file()
else:
self.state.local.ast = ast
self.sanity_check_ast()
self.parse_project()
self._redetect_machines()

@property
def environment(self) -> environment.Environment:
Expand Down Expand Up @@ -922,13 +924,20 @@ def do_subproject(self, subp_name: str, kwargs: kwtypes.DoSubproject, force_meth
'cargo': self._do_subproject_cargo,
}

# snapshot the state, this allows us to rellback the state if the subproject fails
# The GlobalState may be altered by the subproject, and the local state
# may not have been returned to this project's state
snapshot = self.state.copy()
try:
return methods_map[method](subp_name, subdir, default_options, kwargs)
# Invalid code is always an error
except InvalidCode:
raise
except Exception as e:
if not required:
# Restore the original global state
self.state = snapshot

with mlog.nested(subp_name):
# Suppress the 'ERROR:' prefix because this exception is not
# fatal and VS CI treat any logs with "ERROR:" as fatal.
Expand Down Expand Up @@ -957,46 +966,39 @@ def _do_subproject_meson(self, subp_name: str, subdir: str,
mlog.log('Generated Meson AST:', meson_filename)
mlog.cmd_ci_include(meson_filename)

old_build = self.state.world.build
self.state.world.build = old_build.copy()
subi = Interpreter(self.state.world.build, self.state.world.backend, subp_name, subdir,
default_options, ast=ast,
relaxations=relaxations,
user_defined_options=self.state.world.user_defined_options,
world=self.state.world)
# Those lists are shared by all interpreters. That means that
# even if the subproject fails, any modification that the subproject
# made to those lists will affect the parent project.
subi.state.world.subprojects = self.state.world.subprojects
subi.modules = self.modules
subi.holder_map = self.holder_map
subi.bound_holder_map = self.bound_holder_map

subi.state.local.subproject_stack = self.state.local.subproject_stack + [subp_name]
current_active = self.state.local.project_name
# Create a new local state for the new subproject
current_state = self.state.local
stack = current_state.subproject_stack.copy()
if self.is_subproject():
stack.append(current_state.project_name)
self.state.local = LocalInterpreterState(
subp_name, subdir,
rule_relaxations=relaxations or set(),
default_subproject_options=default_options,
subproject_stack=stack,
)

with mlog.nested_warnings():
subi.run()
self.__init(ast)
self.run()
subi_warnings = mlog.get_warning_count()
mlog.log('Subproject', mlog.bold(subp_name), 'finished.')

mlog.log()

if kwargs['version']:
pv = subi.state.local.project_version
pv = self.state.local.project_version
wanted = kwargs['version']
if pv == 'undefined' or not mesonlib.version_compare_many(pv, wanted)[0]:
raise InterpreterException(f'Subproject {subp_name} version is {pv} but {wanted} required.')
self.state.local.project_name = current_active
self.state.world.subprojects.update(subi.state.world.subprojects)
self.state.world.subprojects[subp_name] = SubprojectState(
subi.state.local, subdir, warnings=subi_warnings, callstack=self.state.local.subproject_stack)
self.state.local, subdir, warnings=subi_warnings, callstack=self.state.local.subproject_stack)
self.state.world.build.subprojects[subp_name] = self.state.local.project_version
# Duplicates are possible when subproject uses files from project root
if build_def_files:
self.state.world.build_def_files.update(build_def_files)
# We always need the subi.build_def_files, to propagate sub-sub-projects
old_build.merge(subi.state.world.build)
self.state.world.build = old_build
self.state.world.build.subprojects[subp_name] = subi.state.local.project_version

self.state.local = current_state
return self.state.world.subprojects[subp_name]

def _do_subproject_cmake(self, subp_name: str, subdir: str,
Expand Down
15 changes: 14 additions & 1 deletion mesonbuild/interpreter/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class LocalInterpreterState(LocalState):
"""

default_subproject_options: T.Dict[OptionKey, str] = dataclasses.field(
default_factory=dict, init=False)
default_factory=dict)
"""Options passed to subprojects via the `dependency(default_options)` keyword argument.
See also :attr:`project_default_options`.
Expand Down Expand Up @@ -119,9 +119,22 @@ def __post_init__(self) -> None:
self.environment = self.build.environment
self.coredata = self.environment.coredata

def copy(self) -> GlobalInterpreterState:
c = GlobalInterpreterState(
self.source_root, self.build.copy(),
self.user_defined_options, self.backend)
c.summary = self.summary.copy()
c.subprojects = self.subprojects.copy()
c.args_frozen = self.args_frozen
c.build_def_files = OrderedSet(self.build_def_files)
return c


@dataclasses.dataclass
class InterpreterState(State):

local: LocalInterpreterState
world: GlobalInterpreterState

def copy(self) -> InterpreterState:
return InterpreterState(self.local, self.world.copy())
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@ project('test default_options in find_program')
dummy_exe = find_program('dummy', default_options: ['subproject_option=true'])

test('test_dummy', dummy_exe)

# Test that a suproject that fails does not alter global state, specifically if
# A => B, where B alters that state, but A fails after successfully configuring B
subproject('will_fail', required : false)

p = find_program('python')
assert(p.version().version_compare('< 1000'), 'failed subproject altered global state')
5 changes: 0 additions & 5 deletions unittests/allplatformstests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3701,10 +3701,6 @@ def test_summary(self):
integer: 1
boolean: true
subsub undefined
Something: Some value
My Project 1.0
Configuration
Expand Down Expand Up @@ -3736,7 +3732,6 @@ def test_summary(self):
Subprojects
sub : YES
sub2 : NO Problem encountered: This subproject failed
subsub : YES (from sub2)
User defined options
backend : ''' + self.backend_name + '''
Expand Down

0 comments on commit 264647d

Please sign in to comment.