Bug 1402010 - Define evaluation flags on moz.build contexts; r?chmanchester draft
authorGregory Szorc <gps@mozilla.com>
Thu, 21 Sep 2017 11:04:56 -0700
changeset 668487 23dbfc6d8ebf99c5c3c49656acd9ba644a6a2637
parent 668486 51607c9defa7b2bcb537f9f7c6d9158f1357ef9f
child 668488 74dd02043e00065649486bffe96dc00659b02427
child 668490 8d29a05600bfadc5f592bd6973efdd3d4124af36
push id81062
push usergszorc@mozilla.com
push dateThu, 21 Sep 2017 18:37:24 +0000
reviewerschmanchester
bugs1402010
milestone58.0a1
Bug 1402010 - Define evaluation flags on moz.build contexts; r?chmanchester We have different reasons for evaluating moz.build files. Today, the evaluation portion of the moz.build context/sandbox is mostly the same: the differences in behavior reside at the reader layer. This is good because we want evaluation to be as similar as possible to avoid surprises. As a subsequent commit will show, there are areas where evaluating the same way everywhere has problems and there are compelling reasons to tweak behavior under special scenarios. In this commit, we add an annotation to all moz.build context instances denoting what special evaluation modes are in effect. MozReview-Commit-ID: 2gMZ4JkJCST
python/mozbuild/mozbuild/frontend/context.py
python/mozbuild/mozbuild/frontend/reader.py
--- a/python/mozbuild/mozbuild/frontend/context.py
+++ b/python/mozbuild/mozbuild/frontend/context.py
@@ -84,27 +84,32 @@ class Context(KeyedDefaultDict):
     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
     in this context which are valid. Values are tuples of stored type,
     assigned type, default value, a docstring describing the purpose of the
     variable, and a tier indicator (see comment above the VARIABLES declaration
     in this module).
 
     config is the ConfigEnvironment for this context.
+
+    eval_flags indicates special evaluation modes that are in effect.
+    See the documentation in reader.py for more.
     """
-    def __init__(self, allowed_variables=None, config=None, finder=None):
+    def __init__(self, allowed_variables=None, config=None, finder=None,
+                 eval_flags=None):
         self._allowed_variables = allowed_variables or {}
         self.main_path = None
         self.current_path = None
         # There aren't going to be enough paths for the performance of scanning
         # a list to be a problem.
         self._all_paths = []
         self.config = config
         self._sandbox = None
         self._finder = finder
+        self.eval_flags = eval_flags or set()
         KeyedDefaultDict.__init__(self, self._factory)
 
     def push_source(self, path):
         """Adds the given path as source of the data from this context and make
         it the current path for the context."""
         assert os.path.isabs(path)
         if not self.main_path:
             self.main_path = path
@@ -249,19 +254,21 @@ class Context(KeyedDefaultDict):
             if not isinstance(value, stored_type):
                 update[key] = stored_type(value)
             else:
                 update[key] = value
         KeyedDefaultDict.update(self, update)
 
 
 class TemplateContext(Context):
-    def __init__(self, template=None, allowed_variables=None, config=None):
+    def __init__(self, template=None, allowed_variables=None, config=None,
+                 eval_flags=None):
         self.template = template
-        super(TemplateContext, self).__init__(allowed_variables, config)
+        super(TemplateContext, self).__init__(allowed_variables, config,
+                                              eval_flags=eval_flags)
 
     def _validate(self, key, value):
         return Context._validate(self, key, value, True)
 
 
 class SubContext(Context, ContextDerivedValue):
     """A Context derived from another Context.
 
@@ -269,17 +276,17 @@ class SubContext(Context, ContextDerived
 
     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)
+                         config=parent.config, eval_flags=parent.eval_flags)
 
         # 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:
--- a/python/mozbuild/mozbuild/frontend/reader.py
+++ b/python/mozbuild/mozbuild/frontend/reader.py
@@ -362,17 +362,18 @@ class MozbuildSandbox(Sandbox):
         That function creates a new sandbox for execution of the template.
         After the template is executed, the data from its execution is merged
         with the context of the calling sandbox.
         """
         def template_wrapper(*args, **kwargs):
             context = TemplateContext(
                 template=template.name,
                 allowed_variables=self._context._allowed_variables,
-                config=self._context.config)
+                config=self._context.config,
+                eval_flags=self._context.eval_flags)
             context.add_source(self._context.current_path)
             for p in self._context.all_paths:
                 context.add_source(p)
 
             sandbox = MozbuildSandbox(context, metadata={
                 # We should arguably set these defaults to something else.
                 # Templates, for example, should arguably come from the state
                 # of the sandbox from when the template was declared, not when
@@ -1025,17 +1026,18 @@ class BuildReader(object):
                 source = fh.read()
 
             tree = ast.parse(source, full)
             Visitor().visit(tree)
 
             for name, key, value in assignments:
                 yield p, name, key, value
 
-    def read_mozbuild(self, path, config, descend=True, metadata=None):
+    def read_mozbuild(self, path, config, descend=True, metadata=None,
+                      eval_flags=None):
         """Read and process a mozbuild file, descending into children.
 
         This starts with a single mozbuild file, executes it, and descends into
         other referenced files per our traversal logic.
 
         The traversal logic is to iterate over the *DIRS variables, treating
         each element as a relative directory path. For each encountered
         directory, we will open the moz.build file located in that
@@ -1045,23 +1047,29 @@ class BuildReader(object):
         directories and files per variable values.
 
         Arbitrary metadata in the form of a dict can be passed into this
         function. This feature is intended to facilitate the build reader
         injecting state and annotations into moz.build files that is
         independent of the sandbox's execution context.
 
         Traversal is performed depth first (for no particular reason).
+
+        ``eval_flags`` is a set specifying additional flags to control
+        evaluation. The flags are passed down to the Context, where they
+        can be used to influence run-time behavior.
         """
         metadata = metadata or {}
+        eval_flags = eval_flags or set()
 
         self._execution_stack.append(path)
         try:
             for s in self._read_mozbuild(path, config, descend=descend,
-                                         metadata=metadata):
+                                         metadata=metadata,
+                                         eval_flags=eval_flags):
                 yield s
 
         except BuildReaderError as bre:
             raise bre
 
         except SandboxCalledError as sce:
             raise BuildReaderError(list(self._execution_stack),
                 sys.exc_info()[2], sandbox_called_error=sce)
@@ -1077,17 +1085,17 @@ class BuildReader(object):
         except SandboxValidationError as ve:
             raise BuildReaderError(list(self._execution_stack),
                 sys.exc_info()[2], validation_error=ve)
 
         except Exception as e:
             raise BuildReaderError(list(self._execution_stack),
                 sys.exc_info()[2], other_error=e)
 
-    def _read_mozbuild(self, path, config, descend, metadata):
+    def _read_mozbuild(self, path, config, descend, metadata, eval_flags):
         path = mozpath.normpath(path)
         log(self._log, logging.DEBUG, 'read_mozbuild', {'path': path},
             'Reading file: {path}')
 
         if path in self._read_files:
             log(self._log, logging.WARNING, 'read_already', {'path': path},
                 'File already read. Skipping: {path}')
             return
@@ -1111,17 +1119,18 @@ class BuildReader(object):
 
         if mozpath.dirname(relpath) == 'js/src' and \
                 not config.substs.get('JS_STANDALONE'):
             config = ConfigEnvironment.from_config_status(
                 mozpath.join(topobjdir, reldir, 'config.status'))
             config.topobjdir = topobjdir
             config.external_source_dir = None
 
-        context = Context(VARIABLES, config, self.finder)
+        context = Context(VARIABLES, config=config, finder=self.finder,
+                          eval_flags=eval_flags)
         sandbox = MozbuildSandbox(context, metadata=metadata,
                                   finder=self.finder)
         sandbox.exec_file(path)
         self._execution_time += time.time() - time_start
         self._file_count += len(context.all_paths)
 
         # Yield main context before doing any processing. This gives immediate
         # consumers an opportunity to change state before our remaining
@@ -1201,17 +1210,18 @@ class BuildReader(object):
                 raise SandboxValidationError(
                     'Attempting to process file outside of allowed paths: %s' %
                         child_path, context)
 
             if not descend:
                 continue
 
             for res in self.read_mozbuild(child_path, context.config,
-                                          metadata=child_metadata):
+                                          metadata=child_metadata,
+                                          eval_flags=eval_flags):
                 yield res
 
         self._execution_stack.pop()
 
     def _find_relevant_mozbuilds(self, paths):
         """Given a set of filesystem paths, find all relevant moz.build files.
 
         We assume that a moz.build file in the directory ancestry of a given path
@@ -1252,17 +1262,17 @@ class BuildReader(object):
                 if not mozpath.basedir(path, [root]):
                     raise Exception('Path outside topsrcdir: %s' % path)
                 path = mozpath.relpath(path, root)
 
             result[path] = [p for p in itermozbuild(path) if exists(p)]
 
         return result
 
-    def read_relevant_mozbuilds(self, paths):
+    def read_relevant_mozbuilds(self, paths, eval_flags=None):
         """Read and process moz.build files relevant for a set of paths.
 
         For an iterable of relative-to-root filesystem paths ``paths``,
         find all moz.build files that may apply to them based on filesystem
         hierarchy and read those moz.build files.
 
         The return value is a 2-tuple. The first item is a dict mapping each
         input filesystem path to a list of Context instances that are relevant
@@ -1300,17 +1310,18 @@ class BuildReader(object):
 
         metadata = {
             'functions': functions,
         }
 
         contexts = defaultdict(list)
         all_contexts = []
         for context in self.read_mozbuild(mozpath.join(topsrcdir, 'moz.build'),
-                                          self.config, metadata=metadata):
+                                          self.config, metadata=metadata,
+                                          eval_flags=eval_flags):
             # Explicitly set directory traversal variables to override default
             # traversal rules.
             if not isinstance(context, SubContext):
                 for v in ('DIRS', 'GYP_DIRS'):
                     context[v][:] = []
 
                 context['DIRS'] = sorted(dirs[context.main_path])
 
@@ -1336,17 +1347,19 @@ class BuildReader(object):
         1. Determine the set of moz.build files relevant to that file by
            looking for moz.build files in ancestor directories.
         2. Evaluate moz.build files starting with the most distant.
         3. Iterate over Files sub-contexts.
         4. If the file pattern matches the file we're seeking info on,
            apply attribute updates.
         5. Return the most recent value of attributes.
         """
-        paths, _ = self.read_relevant_mozbuilds(paths)
+        eval_flags = {'files-info'}
+
+        paths, _ = self.read_relevant_mozbuilds(paths, eval_flags=eval_flags)
 
         # For thousands of inputs (say every file in a sub-tree),
         # test_defaults_for_path() gets called with the same contexts multiple
         # times (once for every path in a directory that doesn't have any
         # test metadata). So, we cache the function call.
         defaults_cache = {}
         def test_defaults_for_path(ctxs):
             key = tuple(ctx.current_path or ctx.main_path for ctx in ctxs)
@@ -1357,17 +1370,17 @@ class BuildReader(object):
             return defaults_cache[key]
 
         r = {}
 
         for path, ctxs in paths.items():
             # Should be normalized by read_relevant_mozbuilds.
             assert '\\' not in path
 
-            flags = Files(Context())
+            flags = Files(Context(eval_flags=eval_flags))
 
             for ctx in ctxs:
                 if not isinstance(ctx, Files):
                     continue
 
                 # read_relevant_mozbuilds() normalizes paths and ensures that
                 # the contexts have paths in the ancestry of the path. When
                 # iterating over tens of thousands of paths, mozpath.relpath()