Skip to content
This repository has been archived by the owner on Jul 23, 2024. It is now read-only.

Commit

Permalink
Bug 1134072 - Support for sub-contexts; r=glandium
Browse files Browse the repository at this point in the history
As content in moz.build files has grown, it has become clear that
storing everything in one global namespace (the "context") per moz.build
file will not scale. This approach (which is carried over from
Makefile.in patterns) limits our ability to do things like declare
multiple instances of things (like libraries) per file.

A few months ago, templates were introduced to moz.build files. These
started the process of introducing separate contexts / containers in
each moz.build file. But it stopped short of actually emitting multiple
contexts per container. Instead, results were merged with the main
context.

This patch takes sub-contexts to the next level.

Introduced is the "SubContext" class. It is a Context derived from
another context. SubContexts are special in that they are context
managers. With the context manager is entered, the SubContext becomes
the main context associated with the executing sandbox, temporarily
masking the existence of the main context. This means that UPPERCASE
variable accesses and writes will be handled by the active SubContext.
This allows SubContext instances to define different sets of variables.

When a SubContext is spawned, it is attached to the sandbox executing
it. The moz.build reader will now emit not only the main context, but
also every SubContext that was derived from it.

To aid with the creation and declaration of sub-contexts, we introduce
the SUBCONTEXTS variable. This variable holds a list of classes that
define sub-contexts.

Sub-contexts behave a lot like templates. Their class names becomes the
symbol name in the sandbox.

--HG--
extra : rebase_source : 5df4bcf073ce46605b972021f1e918ce4affa6f3
  • Loading branch information
indygreg committed Feb 24, 2015
1 parent b732d60 commit 0cb191a
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 9 deletions.
61 changes: 56 additions & 5 deletions python/mozbuild/mozbuild/frontend/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ class Context(KeyedDefaultDict):
lots of empty/default values, you have a data structure with only the
values that were read or touched.
Instances of variables classes are created by invoking class_name(),
except when class_name derives from ContextDerivedValue, in which
case class_name(instance_of_the_context) is invoked.
A value is added to those calls when instances are created during
assignment (setitem).
Instances of variables classes are created by invoking ``class_name()``,
except when class_name derives from ``ContextDerivedValue`` or
``SubContext``, in which case ``class_name(instance_of_the_context)`` or
``class_name(self)`` is invoked. A value is added to those calls when
instances are created during assignment (setitem).
allowed_variables is a dict of the variables that can be set and read in
this context instance. Keys in this dict are the strings representing keys
Expand All @@ -84,6 +84,7 @@ def __init__(self, allowed_variables={}, config=None):
self._all_paths = []
self.config = config
self.execution_time = 0
self._sandbox = None
KeyedDefaultDict.__init__(self, self._factory)

def push_source(self, path):
Expand Down Expand Up @@ -272,6 +273,35 @@ def _validate(self, key, value):
return Context._validate(self, key, value, True)


class SubContext(Context, ContextDerivedValue):
"""A Context derived from another Context.
Sub-contexts are intended to be used as context managers.
Sub-contexts inherit paths and other relevant state from the parent
context.
"""
def __init__(self, parent):
assert isinstance(parent, Context)

Context.__init__(self, allowed_variables=self.VARIABLES,
config=parent.config)

# Copy state from parent.
for p in parent.source_stack:
self.push_source(p)
self._sandbox = parent._sandbox

def __enter__(self):
if not self._sandbox or self._sandbox() is None:
raise Exception('a sandbox is required')

self._sandbox().push_subcontext(self)

def __exit__(self, exc_type, exc_value, traceback):
self._sandbox().pop_subcontext(self)


class FinalTargetValue(ContextDerivedValue, unicode):
def __new__(cls, context, value=""):
if not value:
Expand Down Expand Up @@ -366,6 +396,27 @@ def __new__(cls, obj):
return _TypedList


# This defines functions that create sub-contexts.
#
# Values are classes that are SubContexts. The class name will be turned into
# a function that when called emits an instance of that class.
#
# Arbitrary arguments can be passed to the class constructor. The first
# argument is always the parent context. It is up to each class to perform
# argument validation.
SUBCONTEXTS = [
]

for cls in SUBCONTEXTS:
if not issubclass(cls, SubContext):
raise ValueError('SUBCONTEXTS entry not a SubContext class: %s' % cls)

if not hasattr(cls, 'VARIABLES'):
raise ValueError('SUBCONTEXTS entry does not have VARIABLES: %s' % cls)

SUBCONTEXTS = {cls.__name__: cls for cls in SUBCONTEXTS}


# This defines the set of mutable global variables.
#
# Each variable is a tuple of:
Expand Down
18 changes: 15 additions & 3 deletions python/mozbuild/mozbuild/frontend/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
VARIABLES,
DEPRECATION_HINTS,
SPECIAL_VARIABLES,
SUBCONTEXTS,
TemplateContext,
)

Expand Down Expand Up @@ -140,12 +141,14 @@ def __getitem__(self, key):
return SPECIAL_VARIABLES[key][0](self._context)
if key in FUNCTIONS:
return self._create_function(FUNCTIONS[key])
if key in SUBCONTEXTS:
return self._create_subcontext(SUBCONTEXTS[key])
if key in self.templates:
return self._create_template_function(self.templates[key])
return Sandbox.__getitem__(self, key)

def __setitem__(self, key, value):
if key in SPECIAL_VARIABLES or key in FUNCTIONS:
if key in SPECIAL_VARIABLES or key in FUNCTIONS or key in SUBCONTEXTS:
raise KeyError()
if key in self.exports:
self._context[key] = value
Expand Down Expand Up @@ -310,6 +313,14 @@ def _template_decorator(self, func):

self.templates[name] = func, code, self._context.current_path

@memoize
def _create_subcontext(self, cls):
"""Return a function object that creates SubContext instances."""
def fn(*args, **kwargs):
return cls(self._context, *args, **kwargs)

return fn

@memoize
def _create_function(self, function_def):
"""Returns a function object for use within the sandbox for the given
Expand Down Expand Up @@ -1003,11 +1014,12 @@ def _read_mozbuild(self, path, config, descend, metadata):

for gyp_context in gyp_contexts:
context['DIRS'].append(mozpath.relpath(gyp_context.objdir, context.objdir))
sandbox.subcontexts.append(gyp_context)

yield context

for gyp_context in gyp_contexts:
yield gyp_context
for subcontext in sandbox.subcontexts:
yield subcontext

# Traverse into referenced files.

Expand Down
38 changes: 37 additions & 1 deletion python/mozbuild/mozbuild/frontend/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import copy
import os
import sys
import weakref

from contextlib import contextmanager

Expand Down Expand Up @@ -116,12 +117,22 @@ def __init__(self, context, builtins=None):
assert isinstance(self._builtins, ReadOnlyDict)
assert isinstance(context, Context)

self._context = context
# Contexts are modeled as a stack because multiple context managers
# may be active.
self._active_contexts = [context]

# Seen sub-contexts. Will be populated with other Context instances
# that were related to execution of this instance.
self.subcontexts = []

# We need to record this because it gets swallowed as part of
# evaluation.
self._last_name_error = None

@property
def _context(self):
return self._active_contexts[-1]

def exec_file(self, path):
"""Execute code at a path in the sandbox.
Expand Down Expand Up @@ -153,6 +164,9 @@ def exec_source(self, source, path=''):
if path:
self._context.push_source(path)

old_sandbox = self._context._sandbox
self._context._sandbox = weakref.ref(self)

# We don't have to worry about bytecode generation here because we are
# too low-level for that. However, we could add bytecode generation via
# the marshall module if parsing performance were ever an issue.
Expand Down Expand Up @@ -190,9 +204,31 @@ def exec_source(self, source, path=''):
raise SandboxExecutionError(self._context.source_stack, exc[0],
exc[1], exc[2])
finally:
self._context._sandbox = old_sandbox
if path:
self._context.pop_source()

def push_subcontext(self, context):
"""Push a SubContext onto the execution stack.
When called, the active context will be set to the specified context,
meaning all variable accesses will go through it. We also record this
SubContext as having been executed as part of this sandbox.
"""
self._active_contexts.append(context)
if context not in self.subcontexts:
self.subcontexts.append(context)

def pop_subcontext(self, context):
"""Pop a SubContext off the execution stack.
SubContexts must be pushed and popped in opposite order. This is
validated as part of the function call to ensure proper consumer API
use.
"""
popped = self._active_contexts.pop()
assert popped == context

def __getitem__(self, key):
if key.isupper():
try:
Expand Down
15 changes: 15 additions & 0 deletions python/mozbuild/mozbuild/sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ def special_reference(v, func, typ, doc):

def format_module(m):
lines = []

for subcontext, cls in sorted(m.SUBCONTEXTS.items()):
lines.extend([
'.. _mozbuild_subcontext_%s:' % subcontext,
'',
'Sub-Context: %s' % subcontext,
'=============' + '=' * len(subcontext),
'',
prepare_docstring(cls.__doc__)[0],
'',
])

for k, v in sorted(cls.VARIABLES.items()):
lines.extend(variable_reference(k, *v))

lines.extend([
'Variables',
'=========',
Expand Down
7 changes: 7 additions & 0 deletions python/mozbuild/mozbuild/test/frontend/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Context,
FUNCTIONS,
SPECIAL_VARIABLES,
SUBCONTEXTS,
VARIABLES,
)

Expand Down Expand Up @@ -256,6 +257,12 @@ def test_documentation_formatting(self):
for func, typ, doc in SPECIAL_VARIABLES.values():
self._verify_doc(doc)

for name, cls in SUBCONTEXTS.items():
self._verify_doc(cls.__doc__)

for name, v in cls.VARIABLES.items():
self._verify_doc(v[2])


if __name__ == '__main__':
main()

0 comments on commit 0cb191a

Please sign in to comment.