bug 855262 - add MozbuildObject.from_environment. r=glandium
authorTed Mielczarek <ted@mielczarek.org>
Tue, 28 May 2013 15:33:22 -0400
changeset 140572 8a990626f354d8acd2bdadad752aabbbaa5d8023
parent 140571 d71234d65e90c487182729cfce61c98666be0f51
child 140573 3a9f2ac6e47b18cddcff2700956a53b7aaa0b38d
push id3911
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 20:17:26 +0000
treeherdermozilla-aurora@7e26ca8db92b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersglandium
bugs855262
milestone24.0a1
bug 855262 - add MozbuildObject.from_environment. r=glandium
python/mozbuild/mozbuild/base.py
python/mozbuild/mozbuild/test/test_base.py
--- a/python/mozbuild/mozbuild/base.py
+++ b/python/mozbuild/mozbuild/base.py
@@ -1,14 +1,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import print_function, unicode_literals
 
+import json
 import logging
 import mozpack.path
 import multiprocessing
 import os
 import subprocess
 import sys
 import which
 
@@ -20,16 +21,46 @@ from mozfile.mozfile import rmtree
 from .backend.configenvironment import ConfigEnvironment
 from .config import BuildConfig
 from .mozconfig import (
     MozconfigFindException,
     MozconfigLoadException,
     MozconfigLoader,
 )
 
+def ancestors(path):
+    """Emit the parent directories of a path."""
+    while path:
+        yield path
+        newpath = os.path.dirname(path)
+        if newpath == path:
+            break
+        path = newpath
+
+def samepath(path1, path2):
+    if hasattr(os.path, "samepath"):
+        return os.path.samepath(path1, path2)
+    return os.path.normpath(path1) == os.path.normpath(path2)
+
+class BadEnvironmentException(Exception):
+    """Base class for errors raised when the build environment is not sane."""
+
+
+class BuildEnvironmentNotFoundException(BadEnvironmentException):
+    """Raised when we could not find a build environment."""
+
+
+class ObjdirMismatchException(BadEnvironmentException):
+    """Raised when the current dir is an objdir and doesn't match the mozconfig."""
+    def __init__(self, objdir1, objdir2):
+        self.objdir1 = objdir1
+        self.objdir2 = objdir2
+
+    def __str__(self):
+        return "Objdir mismatch: %s != %s" % (self.objdir1, self.objdir2)
 
 class MozbuildObject(ProcessExecutionMixin):
     """Base class providing basic functionality useful to many modules.
 
     Modules in this package typically require common functionality such as
     accessing the current config, getting the location of the source directory,
     running processes, etc. This classes provides that functionality. Other
     modules can inherit from this class to obtain this functionality easily.
@@ -49,16 +80,100 @@ class MozbuildObject(ProcessExecutionMix
         self.log_manager = log_manager
 
         self._make = None
         self._topobjdir = topobjdir
         self._mozconfig = None
         self._config_guess_output = None
         self._config_environment = None
 
+    @classmethod
+    def from_environment(cls):
+        """Create a MozbuildObject by detecting the proper one from the env.
+
+        This examines environment state like the current working directory and
+        creates a MozbuildObject from the found source directory, mozconfig, etc.
+
+        The role of this function is to identify a topsrcdir, topobjdir, and
+        mozconfig file.
+
+        If the current working directory is inside a known objdir, we always
+        use the topsrcdir and mozconfig associated with that objdir. If no
+        mozconfig is associated with that objdir, we fall back to looking for
+        the mozconfig in the usual places.
+
+        If the current working directory is inside a known srcdir, we use that
+        topsrcdir and look for mozconfigs using the default mechanism, which
+        looks inside environment variables.
+
+        If the current Python interpreter is running from a virtualenv inside
+        an objdir, we use that as our objdir.
+
+        If we're not inside a srcdir or objdir, an exception is raised.
+        """
+
+        topsrcdir = None
+        topobjdir = None
+        mozconfig = None
+
+        def load_mozinfo(path):
+            info = json.load(open(path, 'rt'))
+            topsrcdir = info.get('topsrcdir')
+            topobjdir = os.path.dirname(path)
+            mozconfig = info.get('mozconfig')
+            return topsrcdir, topobjdir, mozconfig
+
+        for dir_path in ancestors(os.getcwd()):
+            # If we find a mozinfo.json, we are in the objdir.
+            mozinfo_path = os.path.join(dir_path, 'mozinfo.json')
+            if os.path.isfile(mozinfo_path):
+                topsrcdir, topobjdir, mozconfig = load_mozinfo(mozinfo_path)
+                break
+
+            # We choose an arbitrary file as an indicator that this is a
+            # srcdir. We go with ourself because why not!
+            our_path = os.path.join(dir_path, 'python', 'mozbuild', 'mozbuild', 'base.py')
+            if os.path.isfile(our_path):
+                topsrcdir = dir_path
+                break
+
+        if not topsrcdir:
+            # See if we're running from a Python virtualenv that's inside an objdir.
+            mozinfo_path = os.path.join(os.path.dirname(sys.prefix), "mozinfo.json")
+            if os.path.isfile(mozinfo_path):
+                topsrcdir, topobjdir, mozconfig = load_mozinfo(mozinfo_path)
+
+        # If we were successful, we're only guaranteed to find a topsrcdir. If
+        # we couldn't find that, there's nothing we can do.
+        if not topsrcdir:
+            raise BuildEnvironmentNotFoundException(
+                'Could not find Mozilla source tree or build environment.')
+
+        # Now we try to load the config for this environment. If mozconfig is
+        # None, read_mozconfig() will attempt to find one in the existing
+        # environment. If no mozconfig is present, the config will not have
+        # much defined.
+        loader = MozconfigLoader(topsrcdir)
+        config = loader.read_mozconfig(mozconfig)
+
+        # If we're inside a objdir and the found mozconfig resolves to
+        # another objdir, we abort. The reasoning here is that if you are
+        # inside an objdir you probably want to perform actions on that objdir,
+        # not another one.
+        if topobjdir and config['topobjdir'] \
+            and not samepath(topobjdir, config['topobjdir']):
+
+            raise ObjdirMismatchException(topobjdir, config['topobjdir'])
+
+        topobjdir = os.path.normpath(config['topobjdir'] or topobjdir)
+
+        # If we can't resolve topobjdir, oh well. The constructor will figure
+        # it out via config.guess.
+        return cls(topsrcdir, None, None, topobjdir=topobjdir)
+
     @property
     def topobjdir(self):
         if self._topobjdir is None:
             topobj = self.mozconfig['topobjdir'] or 'obj-@CONFIG_GUESS@'
             if not os.path.isabs(topobj):
                 topobj = os.path.abspath(os.path.join(self.topsrcdir, topobj))
             self._topobjdir = topobj.replace("@CONFIG_GUESS@",
                                              self._config_guess)
--- a/python/mozbuild/mozbuild/test/test_base.py
+++ b/python/mozbuild/mozbuild/test/test_base.py
@@ -20,17 +20,17 @@ from mozbuild.base import (
     PathArgument,
 )
 
 from mozbuild.backend.configenvironment import ConfigEnvironment
 
 
 
 curdir = os.path.dirname(__file__)
-topsrcdir = os.path.normpath(os.path.join(curdir, '..', '..', '..', '..'))
+topsrcdir = os.path.abspath(os.path.join(curdir, '..', '..', '..', '..'))
 log_manager = LoggingManager()
 
 
 class TestMozbuildObject(unittest.TestCase):
     def get_base(self):
         return MozbuildObject(topsrcdir, None, log_manager)
 
     def test_objdir_config_guess(self):