Bug 1397406 - Add a helper function to retrieve a BuildReader; r=dustin
authorGregory Szorc <gps@mozilla.com>
Wed, 06 Sep 2017 12:18:51 -0700
changeset 660398 e9416a307987f144f994dfeb6ed00b3277068279
parent 660397 da68a5ff28d0cd132e031b1a67577a19274c01df
child 660399 56d353e23439a1b77a7011289408fbed894af621
push id78390
push userbmo:emilio@crisal.io
push dateWed, 06 Sep 2017 23:04:15 +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)