Adds new merge builds script for use in merging two non-dist builds.

Bug: 137853921
Test: python -m unittest test_common
Test: python -m unittest test_merge_target_files
Test: Built two partial builds without dist. Ran out/host/linux-x86/bin/merge_builds.
Flashed using `fastboot flashall`. Device boots.
Change-Id: Iffd0a447cdf19a7775a813b4b896178aa6f861f3
diff --git a/tools/releasetools/Android.bp b/tools/releasetools/Android.bp
index d4c4673..40cdce8 100644
--- a/tools/releasetools/Android.bp
+++ b/tools/releasetools/Android.bp
@@ -106,6 +106,19 @@
     ],
 }
 
+python_binary_host {
+    name: "merge_builds",
+    defaults: ["releasetools_binary_defaults"],
+    srcs: [
+        "build_super_image.py",
+        "merge_builds.py",
+    ],
+    main: "merge_builds.py",
+    libs: [
+        "releasetools_common",
+    ],
+}
+
 python_defaults {
     name: "releasetools_test_defaults",
     srcs: [
diff --git a/tools/releasetools/common.py b/tools/releasetools/common.py
index 913601f..1175688 100644
--- a/tools/releasetools/common.py
+++ b/tools/releasetools/common.py
@@ -554,6 +554,64 @@
     logger.info("%-25s = (%s) %s", k, type(v).__name__, v)
 
 
+def MergeDynamicPartitionInfoDicts(framework_dict,
+                                   vendor_dict,
+                                   include_dynamic_partition_list=True,
+                                   size_prefix="",
+                                   size_suffix="",
+                                   list_prefix="",
+                                   list_suffix=""):
+  """Merges dynamic partition info variables.
+
+  Args:
+    framework_dict: The dictionary of dynamic partition info variables from the
+      partial framework target files.
+    vendor_dict: The dictionary of dynamic partition info variables from the
+      partial vendor target files.
+    include_dynamic_partition_list: If true, merges the dynamic_partition_list
+      variable. Not all use cases need this variable merged.
+    size_prefix: The prefix in partition group size variables that precedes the
+      name of the partition group. For example, partition group 'group_a' with
+      corresponding size variable 'super_group_a_group_size' would have the
+      size_prefix 'super_'.
+    size_suffix: Similar to size_prefix but for the variable's suffix. For
+      example, 'super_group_a_group_size' would have size_suffix '_group_size'.
+    list_prefix: Similar to size_prefix but for the partition group's
+      partition_list variable.
+    list_suffix: Similar to size_suffix but for the partition group's
+      partition_list variable.
+
+  Returns:
+    The merged dynamic partition info dictionary.
+  """
+  merged_dict = {}
+  # Partition groups and group sizes are defined by the vendor dict because
+  # these values may vary for each board that uses a shared system image.
+  merged_dict["super_partition_groups"] = vendor_dict["super_partition_groups"]
+  if include_dynamic_partition_list:
+    framework_dynamic_partition_list = framework_dict.get(
+        "dynamic_partition_list", "")
+    vendor_dynamic_partition_list = vendor_dict.get("dynamic_partition_list",
+                                                    "")
+    merged_dict["dynamic_partition_list"] = (
+        "%s %s" % (framework_dynamic_partition_list,
+                   vendor_dynamic_partition_list)).strip()
+  for partition_group in merged_dict["super_partition_groups"].split(" "):
+    # Set the partition group's size using the value from the vendor dict.
+    key = "%s%s%s" % (size_prefix, partition_group, size_suffix)
+    if key not in vendor_dict:
+      raise ValueError("Vendor dict does not contain required key %s." % key)
+    merged_dict[key] = vendor_dict[key]
+
+    # Set the partition group's partition list using a concatenation of the
+    # framework and vendor partition lists.
+    key = "%s%s%s" % (list_prefix, partition_group, list_suffix)
+    merged_dict[key] = (
+        "%s %s" %
+        (framework_dict.get(key, ""), vendor_dict.get(key, ""))).strip()
+  return merged_dict
+
+
 def AppendAVBSigningArgs(cmd, partition):
   """Append signing arguments for avbtool."""
   # e.g., "--key path/to/signing_key --algorithm SHA256_RSA4096"
diff --git a/tools/releasetools/merge_builds.py b/tools/releasetools/merge_builds.py
new file mode 100644
index 0000000..7724d6f
--- /dev/null
+++ b/tools/releasetools/merge_builds.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+#
+"""Merges two non-dist partial builds together.
+
+Given two partial builds, a framework build and a vendor build, merge the builds
+together so that the images can be flashed using 'fastboot flashall'.
+
+To support both DAP and non-DAP vendor builds with a single framework partial
+build, the framework partial build should always be built with DAP enabled. The
+vendor partial build determines whether the merged result supports DAP.
+
+This script does not require builds to be built with 'make dist'.
+This script assumes that images other than super_empty.img do not require
+regeneration, including vbmeta images.
+TODO(b/137853921): Add support for regenerating vbmeta images.
+
+Usage: merge_builds.py [args]
+
+  --framework_images comma_separated_image_list
+      Comma-separated list of image names that should come from the framework
+      build.
+
+  --product_out_framework product_out_framework_path
+      Path to out/target/product/<framework build>.
+
+  --product_out_vendor product_out_vendor_path
+      Path to out/target/product/<vendor build>.
+"""
+from __future__ import print_function
+
+import logging
+import os
+import sys
+
+import build_super_image
+import common
+
+logger = logging.getLogger(__name__)
+
+OPTIONS = common.OPTIONS
+OPTIONS.framework_images = ("system",)
+OPTIONS.product_out_framework = None
+OPTIONS.product_out_vendor = None
+
+
+def CreateImageSymlinks():
+  for image in OPTIONS.framework_images:
+    image_path = os.path.join(OPTIONS.product_out_framework, "%s.img" % image)
+    symlink_path = os.path.join(OPTIONS.product_out_vendor, "%s.img" % image)
+    if os.path.exists(symlink_path):
+      if os.path.islink(symlink_path):
+        os.remove(symlink_path)
+      else:
+        raise ValueError("Attempting to overwrite built image: %s" %
+                         symlink_path)
+    os.symlink(image_path, symlink_path)
+
+
+def BuildSuperEmpty():
+  framework_dict = common.LoadDictionaryFromFile(
+      os.path.join(OPTIONS.product_out_framework, "misc_info.txt"))
+  vendor_dict = common.LoadDictionaryFromFile(
+      os.path.join(OPTIONS.product_out_vendor, "misc_info.txt"))
+  # Regenerate super_empty.img if both partial builds enable DAP. If only the
+  # the vendor build enables DAP, the vendor build's existing super_empty.img
+  # will be reused. If only the framework build should enable DAP, super_empty
+  # should be included in the --framework_images flag to copy the existing
+  # super_empty.img from the framework build.
+  if (framework_dict.get("use_dynamic_partitions") == "true") and (
+      vendor_dict.get("use_dynamic_partitions") == "true"):
+    merged_dict = dict(vendor_dict)
+    merged_dict.update(
+        common.MergeDynamicPartitionInfoDicts(
+            framework_dict=framework_dict,
+            vendor_dict=vendor_dict,
+            size_prefix="super_",
+            size_suffix="_group_size",
+            list_prefix="super_",
+            list_suffix="_partition_list"))
+    output_super_empty_path = os.path.join(OPTIONS.product_out_vendor,
+                                           "super_empty.img")
+    build_super_image.BuildSuperImage(merged_dict, output_super_empty_path)
+
+
+def MergeBuilds():
+  CreateImageSymlinks()
+  BuildSuperEmpty()
+  # TODO(b/137853921): Add support for regenerating vbmeta images.
+
+
+def main():
+  common.InitLogging()
+
+  def option_handler(o, a):
+    if o == "--framework_images":
+      OPTIONS.framework_images = [i.strip() for i in a.split(",")]
+    elif o == "--product_out_framework":
+      OPTIONS.product_out_framework = a
+    elif o == "--product_out_vendor":
+      OPTIONS.product_out_vendor = a
+    else:
+      return False
+    return True
+
+  args = common.ParseOptions(
+      sys.argv[1:],
+      __doc__,
+      extra_long_opts=[
+          "framework_images=",
+          "product_out_framework=",
+          "product_out_vendor=",
+      ],
+      extra_option_handler=option_handler)
+
+  if (args or OPTIONS.product_out_framework is None or
+      OPTIONS.product_out_vendor is None):
+    common.Usage(__doc__)
+    sys.exit(1)
+
+  MergeBuilds()
+
+
+if __name__ == "__main__":
+  main()
diff --git a/tools/releasetools/merge_target_files.py b/tools/releasetools/merge_target_files.py
index 7343f38..81b95c8 100755
--- a/tools/releasetools/merge_target_files.py
+++ b/tools/releasetools/merge_target_files.py
@@ -402,64 +402,6 @@
             'selabel=u:object_r:install_recovery_exec:s0 capabilities=0x0\n')
 
 
-def merge_dynamic_partition_info_dicts(framework_dict,
-                                       vendor_dict,
-                                       include_dynamic_partition_list=True,
-                                       size_prefix='',
-                                       size_suffix='',
-                                       list_prefix='',
-                                       list_suffix=''):
-  """Merges dynamic partition info variables.
-
-  Args:
-    framework_dict: The dictionary of dynamic partition info variables from the
-      partial framework target files.
-    vendor_dict: The dictionary of dynamic partition info variables from the
-      partial vendor target files.
-    include_dynamic_partition_list: If true, merges the dynamic_partition_list
-      variable. Not all use cases need this variable merged.
-    size_prefix: The prefix in partition group size variables that precedes the
-      name of the partition group. For example, partition group 'group_a' with
-      corresponding size variable 'super_group_a_group_size' would have the
-      size_prefix 'super_'.
-    size_suffix: Similar to size_prefix but for the variable's suffix. For
-      example, 'super_group_a_group_size' would have size_suffix '_group_size'.
-    list_prefix: Similar to size_prefix but for the partition group's
-      partition_list variable.
-    list_suffix: Similar to size_suffix but for the partition group's
-      partition_list variable.
-
-  Returns:
-    The merged dynamic partition info dictionary.
-  """
-  merged_dict = {}
-  # Partition groups and group sizes are defined by the vendor dict because
-  # these values may vary for each board that uses a shared system image.
-  merged_dict['super_partition_groups'] = vendor_dict['super_partition_groups']
-  if include_dynamic_partition_list:
-    framework_dynamic_partition_list = framework_dict.get(
-        'dynamic_partition_list', '')
-    vendor_dynamic_partition_list = vendor_dict.get('dynamic_partition_list',
-                                                    '')
-    merged_dict['dynamic_partition_list'] = (
-        '%s %s' % (framework_dynamic_partition_list,
-                   vendor_dynamic_partition_list)).strip()
-  for partition_group in merged_dict['super_partition_groups'].split(' '):
-    # Set the partition group's size using the value from the vendor dict.
-    key = '%s%s%s' % (size_prefix, partition_group, size_suffix)
-    if key not in vendor_dict:
-      raise ValueError('Vendor dict does not contain required key %s.' % key)
-    merged_dict[key] = vendor_dict[key]
-
-    # Set the partition group's partition list using a concatenation of the
-    # framework and vendor partition lists.
-    key = '%s%s%s' % (list_prefix, partition_group, list_suffix)
-    merged_dict[key] = (
-        '%s %s' %
-        (framework_dict.get(key, ''), vendor_dict.get(key, ''))).strip()
-  return merged_dict
-
-
 def process_misc_info_txt(framework_target_files_temp_dir,
                           vendor_target_files_temp_dir,
                           output_target_files_temp_dir,
@@ -503,7 +445,7 @@
   # Merge misc info keys used for Dynamic Partitions.
   if (merged_dict.get('use_dynamic_partitions') == 'true') and (
       framework_dict.get('use_dynamic_partitions') == 'true'):
-    merged_dynamic_partitions_dict = merge_dynamic_partition_info_dicts(
+    merged_dynamic_partitions_dict = common.MergeDynamicPartitionInfoDicts(
         framework_dict=framework_dict,
         vendor_dict=merged_dict,
         size_prefix='super_',
@@ -566,7 +508,7 @@
   vendor_dynamic_partitions_dict = common.LoadDictionaryFromFile(
       os.path.join(vendor_target_files_dir, *dynamic_partitions_info_path))
 
-  merged_dynamic_partitions_dict = merge_dynamic_partition_info_dicts(
+  merged_dynamic_partitions_dict = common.MergeDynamicPartitionInfoDicts(
       framework_dict=framework_dynamic_partitions_dict,
       vendor_dict=vendor_dynamic_partitions_dict,
       # META/dynamic_partitions_info.txt does not use dynamic_partition_list.
diff --git a/tools/releasetools/test_common.py b/tools/releasetools/test_common.py
index 3a2198c..bcfb1c1 100644
--- a/tools/releasetools/test_common.py
+++ b/tools/releasetools/test_common.py
@@ -1074,6 +1074,69 @@
       self.assertRaises(
           AssertionError, common.LoadInfoDict, target_files_zip, True)
 
+  def test_MergeDynamicPartitionInfoDicts_ReturnsMergedDict(self):
+    framework_dict = {
+        'super_partition_groups': 'group_a',
+        'dynamic_partition_list': 'system',
+        'super_group_a_list': 'system',
+    }
+    vendor_dict = {
+        'super_partition_groups': 'group_a group_b',
+        'dynamic_partition_list': 'vendor product',
+        'super_group_a_list': 'vendor',
+        'super_group_a_size': '1000',
+        'super_group_b_list': 'product',
+        'super_group_b_size': '2000',
+    }
+    merged_dict = common.MergeDynamicPartitionInfoDicts(
+        framework_dict=framework_dict,
+        vendor_dict=vendor_dict,
+        size_prefix='super_',
+        size_suffix='_size',
+        list_prefix='super_',
+        list_suffix='_list')
+    expected_merged_dict = {
+        'super_partition_groups': 'group_a group_b',
+        'dynamic_partition_list': 'system vendor product',
+        'super_group_a_list': 'system vendor',
+        'super_group_a_size': '1000',
+        'super_group_b_list': 'product',
+        'super_group_b_size': '2000',
+    }
+    self.assertEqual(merged_dict, expected_merged_dict)
+
+  def test_MergeDynamicPartitionInfoDicts_IgnoringFrameworkGroupSize(self):
+    framework_dict = {
+        'super_partition_groups': 'group_a',
+        'dynamic_partition_list': 'system',
+        'super_group_a_list': 'system',
+        'super_group_a_size': '5000',
+    }
+    vendor_dict = {
+        'super_partition_groups': 'group_a group_b',
+        'dynamic_partition_list': 'vendor product',
+        'super_group_a_list': 'vendor',
+        'super_group_a_size': '1000',
+        'super_group_b_list': 'product',
+        'super_group_b_size': '2000',
+    }
+    merged_dict = common.MergeDynamicPartitionInfoDicts(
+        framework_dict=framework_dict,
+        vendor_dict=vendor_dict,
+        size_prefix='super_',
+        size_suffix='_size',
+        list_prefix='super_',
+        list_suffix='_list')
+    expected_merged_dict = {
+        'super_partition_groups': 'group_a group_b',
+        'dynamic_partition_list': 'system vendor product',
+        'super_group_a_list': 'system vendor',
+        'super_group_a_size': '1000',
+        'super_group_b_list': 'product',
+        'super_group_b_size': '2000',
+    }
+    self.assertEqual(merged_dict, expected_merged_dict)
+
 
 class InstallRecoveryScriptFormatTest(test_utils.ReleaseToolsTestCase):
   """Checks the format of install-recovery.sh.
diff --git a/tools/releasetools/test_merge_target_files.py b/tools/releasetools/test_merge_target_files.py
index 1b1c725..1abe83c 100644
--- a/tools/releasetools/test_merge_target_files.py
+++ b/tools/releasetools/test_merge_target_files.py
@@ -22,7 +22,6 @@
                                 DEFAULT_FRAMEWORK_ITEM_LIST,
                                 DEFAULT_VENDOR_ITEM_LIST,
                                 DEFAULT_FRAMEWORK_MISC_INFO_KEYS, copy_items,
-                                merge_dynamic_partition_info_dicts,
                                 process_apex_keys_apk_certs_common)
 
 
@@ -126,69 +125,6 @@
                                 framework_misc_info_keys,
                                 DEFAULT_VENDOR_ITEM_LIST))
 
-  def test_merge_dynamic_partition_info_dicts_ReturnsMergedDict(self):
-    framework_dict = {
-        'super_partition_groups': 'group_a',
-        'dynamic_partition_list': 'system',
-        'super_group_a_list': 'system',
-    }
-    vendor_dict = {
-        'super_partition_groups': 'group_a group_b',
-        'dynamic_partition_list': 'vendor product',
-        'super_group_a_list': 'vendor',
-        'super_group_a_size': '1000',
-        'super_group_b_list': 'product',
-        'super_group_b_size': '2000',
-    }
-    merged_dict = merge_dynamic_partition_info_dicts(
-        framework_dict=framework_dict,
-        vendor_dict=vendor_dict,
-        size_prefix='super_',
-        size_suffix='_size',
-        list_prefix='super_',
-        list_suffix='_list')
-    expected_merged_dict = {
-        'super_partition_groups': 'group_a group_b',
-        'dynamic_partition_list': 'system vendor product',
-        'super_group_a_list': 'system vendor',
-        'super_group_a_size': '1000',
-        'super_group_b_list': 'product',
-        'super_group_b_size': '2000',
-    }
-    self.assertEqual(merged_dict, expected_merged_dict)
-
-  def test_merge_dynamic_partition_info_dicts_IgnoringFrameworkGroupSize(self):
-    framework_dict = {
-        'super_partition_groups': 'group_a',
-        'dynamic_partition_list': 'system',
-        'super_group_a_list': 'system',
-        'super_group_a_size': '5000',
-    }
-    vendor_dict = {
-        'super_partition_groups': 'group_a group_b',
-        'dynamic_partition_list': 'vendor product',
-        'super_group_a_list': 'vendor',
-        'super_group_a_size': '1000',
-        'super_group_b_list': 'product',
-        'super_group_b_size': '2000',
-    }
-    merged_dict = merge_dynamic_partition_info_dicts(
-        framework_dict=framework_dict,
-        vendor_dict=vendor_dict,
-        size_prefix='super_',
-        size_suffix='_size',
-        list_prefix='super_',
-        list_suffix='_list')
-    expected_merged_dict = {
-        'super_partition_groups': 'group_a group_b',
-        'dynamic_partition_list': 'system vendor product',
-        'super_group_a_list': 'system vendor',
-        'super_group_a_size': '1000',
-        'super_group_b_list': 'product',
-        'super_group_b_size': '2000',
-    }
-    self.assertEqual(merged_dict, expected_merged_dict)
-
   def test_process_apex_keys_apk_certs_ReturnsTrueIfNoConflicts(self):
     output_dir = common.MakeTempDir()
     os.makedirs(os.path.join(output_dir, 'META'))