orchestrator: inner-tree path can be a list.

Shared trees are supported by specifying the inner-tree as a list in the
mcombo file.  This change enables that work.

Bug: None
Test: manual, unittests pass

Change-Id: I161d707d0aada66d134b49b158bf538f0e2a2572
diff --git a/envsetup.sh b/envsetup.sh
index 8856212..ea28c2e 100644
--- a/envsetup.sh
+++ b/envsetup.sh
@@ -455,10 +455,19 @@
 {
     local code
     local results
+    # Lunch must be run in the topdir, but this way we get a clear error
+    # message, instead of FileNotFound.
+    local T=$(multitree_gettop)
+    if [ -n "$T" ]; then
+      "$T/build/build/make/orchestrator/core/orchestrator.py" "$@"
+    else
+      _multitree_lunch_error
+      return 1
+    fi
     if $(echo "$1" | grep -q '^-') ; then
         # Calls starting with a -- argument are passed directly and the function
         # returns with the lunch.py exit code.
-        build/build/make/orchestrator/core/lunch.py "$@"
+        "${T}/build/build/make/orchestrator/core/lunch.py" "$@"
         code=$?
         if [[ $code -eq 2 ]] ; then
           echo 1>&2
@@ -469,7 +478,7 @@
         fi
     else
         # All other calls go through the --lunch variant of lunch.py
-        results=($(build/build/make/orchestrator/core/lunch.py --lunch "$@"))
+        results=($(${T}/build/build/make/orchestrator/core/lunch.py --lunch "$@"))
         code=$?
         if [[ $code -eq 2 ]] ; then
           echo 1>&2
@@ -1813,7 +1822,8 @@
 function _trigger_build()
 (
     local -r bc="$1"; shift
-    if T="$(gettop)"; then
+    local T=$(gettop)
+    if [ -n "$T" ]; then
       _wrap_build "$T/build/soong/soong_ui.bash" --build-mode --${bc} --dir="$(pwd)" "$@"
     else
       >&2 echo "Couldn't locate the top of the tree. Try setting TOP."
@@ -1873,8 +1883,9 @@
 
 function multitree_build()
 {
-    if T="$(multitree_gettop)"; then
-      "$T/build/build/orchestrator/core/orchestrator.py" "$@"
+    local T=$(multitree_gettop)
+    if [ -n "$T" ]; then
+      "$T/build/build/make/orchestrator/core/orchestrator.py" "$@"
     else
       _multitree_lunch_error
       return 1
diff --git a/orchestrator/core/inner_tree.py b/orchestrator/core/inner_tree.py
index d348ee7..dcfb3eb 100644
--- a/orchestrator/core/inner_tree.py
+++ b/orchestrator/core/inner_tree.py
@@ -14,11 +14,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import json
 import os
 import subprocess
 import sys
 import textwrap
 
+
 class InnerTreeKey(object):
     """Trees are identified uniquely by their root and the TARGET_PRODUCT they will use to build.
     If a single tree uses two different prdoucts, then we won't make assumptions about
@@ -26,21 +28,33 @@
     TODO: This is true for soong. It's more likely that bazel could do analysis for two
     products at the same time in a single tree, so there's an optimization there to do
     eventually."""
+
     def __init__(self, root, product):
+        if isinstance(root, list):
+            self.melds = root[1:]
+            root = root[0]
+        else:
+            self.melds = []
         self.root = root
         self.product = product
 
     def __str__(self):
-        return "TreeKey(root=%s product=%s)" % (enquote(self.root), enquote(self.product))
+        return (f"TreeKey(root={enquote(self.root)} "
+                f"product={enquote(self.product)}")
 
     def __hash__(self):
         return hash((self.root, self.product))
 
     def _cmp(self, other):
+        assert isinstance(other, InnerTreeKey)
         if self.root < other.root:
             return -1
         if self.root > other.root:
             return 1
+        if self.melds < other.melds:
+            return -1
+        if self.melds > other.melds:
+            return 1
         if self.product == other.product:
             return 0
         if self.product is None:
@@ -71,13 +85,16 @@
 
 
 class InnerTree(object):
-    def __init__(self, context, root, product):
+    def __init__(self, context, paths, product):
         """Initialize with the inner tree root (relative to the workspace root)"""
-        self.root = root
+        if not isinstance(paths, list):
+            paths = [paths]
+        self.root = paths[0]
+        self.meld_dirs = paths[1:]
         self.product = product
         self.domains = {}
         # TODO: Base directory on OUT_DIR
-        out_root = context.out.inner_tree_dir(root)
+        out_root = context.out.inner_tree_dir(self.root)
         if product:
             out_root += "_" + product
         else:
@@ -85,9 +102,10 @@
         self.out = OutDirLayout(out_root)
 
     def __str__(self):
-        return "InnerTree(root=%s product=%s domains=[%s])" % (enquote(self.root),
-                enquote(self.product),
-                " ".join([enquote(d) for d in sorted(self.domains.keys())]))
+        return (f"InnerTree(root={enquote(self.root)} "
+                f"product={enquote(self.product)} "
+                f"domains={enquote(list(self.domains.keys()))} "
+                f"meld={enquote(self.meld_dirs)})")
 
     def invoke(self, args):
         """Call the inner tree command for this inner tree. Exits on failure."""
@@ -97,8 +115,9 @@
         # so we can print a good error message
         inner_build_tool = os.path.join(self.root, ".inner_build")
         if not os.access(inner_build_tool, os.X_OK):
-            sys.stderr.write(("Unable to execute %s. Is there an inner tree or lunch combo"
-                    + " misconfiguration?\n") % inner_build_tool)
+            sys.stderr.write(
+                f"Unable to execute {inner_build_tool}. Is there an inner tree "
+                "or lunch combo misconfiguration?\n")
             sys.exit(1)
 
         # TODO: This is where we should set up the shared trees
@@ -115,8 +134,9 @@
 
         # TODO: Probably want better handling of inner tree failures
         if process.returncode:
-            sys.stderr.write("Build error in inner tree: %s\nstopping multitree build.\n"
-                    % self.root)
+            sys.stderr.write(
+                f"Build error in inner tree: {self.root}\nstopping "
+                "multitree build.\n")
             sys.exit(1)
 
 
@@ -127,19 +147,19 @@
 
     def __str__(self):
         "Return a debugging dump of this object"
-        return textwrap.dedent("""\
-        InnerTrees {
+
+        def _vals(values):
+            return ("\n" + " " * 16).join(sorted([str(t) for t in values]))
+
+        return textwrap.dedent(f"""\
+        InnerTrees {{
             trees: [
-                %(trees)s
+                {_vals(self.trees.values())}
             ]
             domains: [
-                %(domains)s
+                {_vals(self.domains.values())}
             ]
-        }""" % {
-            "trees": "\n        ".join(sorted([str(t) for t in self.trees.values()])),
-            "domains": "\n        ".join(sorted([str(d) for d in self.domains.values()])),
-        })
-
+        }}""")
 
     def for_each_tree(self, func, cookie=None):
         """Call func for each of the inner trees once for each product that will be built in it.
@@ -153,7 +173,6 @@
             result[key] = func(key, self.trees[key], cookie)
         return result
 
-
     def get(self, tree_key):
         """Get an inner tree for tree_key"""
         return self.trees.get(tree_key)
@@ -188,6 +207,4 @@
 
 
 def enquote(s):
-    return "None" if s is None else "\"%s\"" % s
-
-
+    return json.dumps(s)
diff --git a/orchestrator/core/lunch.py b/orchestrator/core/lunch.py
index 70a2d1d..71ae45b 100755
--- a/orchestrator/core/lunch.py
+++ b/orchestrator/core/lunch.py
@@ -240,9 +240,12 @@
     trees = [("Component", "Path", "Product"),
              ("---------", "----", "-------")]
     entry = config.get("system", None)
+
     def add_config_tuple(trees, entry, name):
         if entry:
-            trees.append((name, entry.get("tree"), entry.get("product", "")))
+            trees.append(
+                (name, entry.get("inner-tree"), entry.get("product", "")))
+
     add_config_tuple(trees, config.get("system"), "system")
     add_config_tuple(trees, config.get("vendor"), "vendor")
     for k, v in config.get("modules", {}).items():
diff --git a/orchestrator/core/orchestrator.py b/orchestrator/core/orchestrator.py
index 508f73a..256850f 100755
--- a/orchestrator/core/orchestrator.py
+++ b/orchestrator/core/orchestrator.py
@@ -55,14 +55,16 @@
 
     system_entry = lunch_config.get("system")
     if system_entry:
-        add(API_DOMAIN_SYSTEM, system_entry["tree"], system_entry["product"])
+        add(API_DOMAIN_SYSTEM, system_entry["inner-tree"],
+            system_entry["product"])
 
     vendor_entry = lunch_config.get("vendor")
     if vendor_entry:
-        add(API_DOMAIN_VENDOR, vendor_entry["tree"], vendor_entry["product"])
+        add(API_DOMAIN_VENDOR, vendor_entry["inner-tree"],
+            vendor_entry["product"])
 
     for module_name, module_entry in lunch_config.get("modules", []).items():
-        add(module_name, module_entry["tree"], None)
+        add(module_name, module_entry["inner-tree"], None)
 
     return inner_tree.InnerTrees(trees, domains)
 
diff --git a/orchestrator/core/utils.py b/orchestrator/core/utils.py
index 41310e0..2ce40a6 100644
--- a/orchestrator/core/utils.py
+++ b/orchestrator/core/utils.py
@@ -28,7 +28,7 @@
     "Context for testing. The real Context is manually constructed in orchestrator.py."
 
     def __init__(self, test_work_dir, test_name):
-        super(MockContext, self).__init__(os.path.join(test_work_dir, test_name),
+        super(TestContext, self).__init__(os.path.join(test_work_dir, test_name),
                 Errors(None))
 
 
diff --git a/orchestrator/multitree_combos/aosp_cf_arm64_phone.mcombo b/orchestrator/multitree_combos/aosp_cf_arm64_phone.mcombo
index 0790226..62fe778 100644
--- a/orchestrator/multitree_combos/aosp_cf_arm64_phone.mcombo
+++ b/orchestrator/multitree_combos/aosp_cf_arm64_phone.mcombo
@@ -1,16 +1,16 @@
 {
     "lunchable": true,
     "system": {
-        "tree": "master",
+        "inner-tree": "aosp-master-with-phones",
         "product": "aosp_cf_arm64_phone"
     },
     "vendor": {
-        "tree": "master",
+        "inner-tree": "aosp-master-with-phones",
         "product": "aosp_cf_arm64_phone"
     },
     "modules": {
         "com.android.bionic": {
-            "tree": "sc-mainline-prod"
+            "inner-tree": "aosp-master-with-phones"
         }
     }
 }
diff --git a/orchestrator/multitree_combos/test.mcombo b/orchestrator/multitree_combos/test.mcombo
index 3ad0717..d601b7d 100644
--- a/orchestrator/multitree_combos/test.mcombo
+++ b/orchestrator/multitree_combos/test.mcombo
@@ -1,16 +1,16 @@
 {
     "lunchable": true,
     "system": {
-        "tree": "inner_tree_system",
+        "inner-tree": "inner_tree_system",
         "product": "system_lunch_product"
     },
     "vendor": {
-        "tree": "inner_tree_vendor",
+        "inner-tree": "inner_tree_vendor",
         "product": "vendor_lunch_product"
     },
     "modules": {
         "com.android.something": {
-            "tree": "inner_tree_module"
+            "inner-tree": ["inner_tree_module", "sc-common"]
         }
     }
 }