Bug 1397406 - Add a helper function to retrieve a BuildReader; r?dustin draft
authorGregory Szorc <gps@mozilla.com>
Wed, 06 Sep 2017 12:18:51 -0700
changeset 660268 30f638f3ca496580e66761645a6144d4885e99ab
parent 660259 8ca48411de46e2d1d67f93c7a2a0703542e3a4e5
child 660269 da73ea2a49ccf82a3efe28083c3aaec7940812b1
push id78341
push usergszorc@mozilla.com
push dateWed, 06 Sep 2017 19:34:49 +0000
reviewersdustin
bugs1397406, 1383880
milestone57.0a1
Bug 1397406 - Add a helper function to retrieve a BuildReader; r?dustin The code for obtaining a BuildReader for evaluating moz.build files is generic and non-trivial. We already had a custom implementation for `mach file-info` that implemented support for Mercurial integration. Bug 1383880 will introduce a second consumer. So this commit factors out the "obtain a BuildReader" logic into a reusable function on our base MozbuildObject class. This makes it easily available to various parts of the build system and mach commands. As part of the change, we detect when ``.`` is being used as the revision and verify the working directory is clean. This behavior can be disabled via argument if unwanted. But it's useful by default to ensure consumers aren't expecting to read uncommitted changes. MozReview-Commit-ID: LeYFqAb3HAe
python/mozbuild/mozbuild/base.py
python/mozbuild/mozbuild/frontend/mach_commands.py
--- a/python/mozbuild/mozbuild/base.py
+++ b/python/mozbuild/mozbuild/base.py
@@ -12,16 +12,17 @@ import os
 import subprocess
 import sys
 import which
 
 from mach.mixin.process import ProcessExecutionMixin
 from mozversioncontrol import (
     get_repository_from_build_config,
     get_repository_object,
+    InvalidRepoPath,
 )
 
 from .backend.configenvironment import ConfigEnvironment
 from .controller.clobber import Clobberer
 from .mozconfig import (
     MozconfigFindException,
     MozconfigLoadException,
     MozconfigLoader,
@@ -293,16 +294,90 @@ class MozbuildObject(ProcessExecutionMix
         # If we don't have a configure context, fall back to auto-detection.
         try:
             return get_repository_from_build_config(self)
         except BuildEnvironmentNotFoundException:
             pass
 
         return get_repository_object(self.topsrcdir)
 
+    def mozbuild_reader(self, config_mode='build', vcs_revision=None,
+                        vcs_check_clean=True):
+        """Obtain a ``BuildReader`` for evaluating moz.build files.
+
+        Given arguments, returns a ``mozbuild.frontend.reader.BuildReader``
+        that can be used to evaluate moz.build files for this repo.
+
+        ``config_mode`` is either ``build`` or ``empty``. If ``build``,
+        ``self.config_environment`` is used. This requires a configured build
+        system to work. If ``empty``, an empty config is used. ``empty`` is
+        appropriate for file-based traversal mode where ``Files`` metadata is
+        read.
+
+        If ``vcs_revision`` is defined, it specifies a version control revision
+        to use to obtain files content. The default is to use the filesystem.
+        This mode is only supported with Mercurial repositories.
+
+        If ``vcs_revision`` is not defined and the version control checkout is
+        sparse, this implies ``vcs_revision='.'``.
+
+        If ``vcs_revision`` is ``.`` (denotes the parent of the working
+        directory), we will verify that the working directory is clean unless
+        ``vcs_check_clean`` is False. This prevents confusion due to uncommitted
+        file changes not being reflected in the reader.
+        """
+        from mozbuild.frontend.reader import (
+            default_finder,
+            BuildReader,
+            EmptyConfig,
+        )
+        from mozpack.files import (
+            MercurialRevisionFinder,
+        )
+
+        if config_mode == 'build':
+            config = self.config_environment
+        elif config_mode == 'empty':
+            config = EmptyConfig(self.topsrcdir)
+        else:
+            raise ValueError('unknown config_mode value: %s' % config_mode)
+
+        try:
+            repo = self.repository
+        except InvalidRepoPath:
+            repo = None
+
+        if repo and not vcs_revision and repo.sparse_checkout_present():
+            vcs_revision = '.'
+
+        if vcs_revision is None:
+            finder = default_finder
+        else:
+            # If we failed to detect the repo prior, check again to raise its
+            # exception.
+            if not repo:
+                self.repository
+                assert False
+
+            if repo.name != 'hg':
+                raise Exception('do not support VCS reading mode for %s' %
+                                repo.name)
+
+            if vcs_revision == '.' and vcs_check_clean:
+                with repo:
+                    if not repo.working_directory_clean():
+                        raise Exception('working directory is not clean; '
+                                        'refusing to use a VCS-based finder')
+
+            finder = MercurialRevisionFinder(self.topsrcdir, rev=vcs_revision,
+                                             recognize_repo_paths=True)
+
+        return BuildReader(config, finder=finder)
+
+
     @memoized_property
     def python3(self):
         """Obtain info about a Python 3 executable.
 
         Returns a tuple of an executable path and its version (as a tuple).
         Either both entries will have a value or both will be None.
         """
         # Search configured build info first. Then fall back to system.
--- a/python/mozbuild/mozbuild/frontend/mach_commands.py
+++ b/python/mozbuild/mozbuild/frontend/mach_commands.py
@@ -154,70 +154,41 @@ class MozbuildFileCommands(MachCommandBa
                     for p in m.test_flavors:
                         print('\t\t%s' % p)
 
         except InvalidPathException as e:
             print(e.message)
             return 1
 
 
-    def _get_reader(self, finder):
-        from mozbuild.frontend.reader import (
-            BuildReader,
-            EmptyConfig,
-        )
-
-        config = EmptyConfig(self.topsrcdir)
-        return BuildReader(config, finder=finder)
-
     def _get_files_info(self, paths, rev=None):
-        from mozbuild.frontend.reader import default_finder
-        from mozpack.files import FileFinder, MercurialRevisionFinder
+        reader = self.mozbuild_reader(config_mode='empty', vcs_revision=rev)
 
         # Normalize to relative from topsrcdir.
         relpaths = []
         for p in paths:
             a = mozpath.abspath(p)
             if not mozpath.basedir(a, [self.topsrcdir]):
                 raise InvalidPathException('path is outside topsrcdir: %s' % p)
 
             relpaths.append(mozpath.relpath(a, self.topsrcdir))
 
-        repo = None
-        if rev:
-            hg_path = os.path.join(self.topsrcdir, '.hg')
-            if not os.path.exists(hg_path):
-                raise InvalidPathException('a Mercurial repo is required '
-                        'when specifying a revision')
-
-            repo = self.topsrcdir
-
-        # We need two finders because the reader's finder operates on
-        # absolute paths.
-        finder = FileFinder(self.topsrcdir)
-        if repo:
-            reader_finder = MercurialRevisionFinder(repo, rev=rev,
-                                                    recognize_repo_paths=True)
-        else:
-            reader_finder = default_finder
-
         # Expand wildcards.
         # One variable is for ordering. The other for membership tests.
         # (Membership testing on a list can be slow.)
         allpaths = []
         all_paths_set = set()
         for p in relpaths:
             if '*' not in p:
                 if p not in all_paths_set:
                     all_paths_set.add(p)
                     allpaths.append(p)
                 continue
 
-            if repo:
+            if rev:
                 raise InvalidPathException('cannot use wildcard in version control mode')
 
-            for path, f in finder.find(p):
+            for path, f in reader.finder.find(p):
                 if path not in all_paths_set:
                     all_paths_set.add(path)
                     allpaths.append(path)
 
-        reader = self._get_reader(finder=reader_finder)
         return reader.files_info(allpaths)