Creates a combined split-sepolicy file in merge_target_files.py.

This follows the same steps as OpenSplitPolicy() in
system/core/init/selinux.cpp on the device.

Bug: 178864050
Test: merge_target_files for R+S and S+S devices
Test: test_merge_target_files
Change-Id: Ia41a436bfda8e2cb65706122f0ff3805b99d16e1
diff --git a/tools/releasetools/Android.bp b/tools/releasetools/Android.bp
index 81528ae..6d88249 100644
--- a/tools/releasetools/Android.bp
+++ b/tools/releasetools/Android.bp
@@ -451,6 +451,7 @@
     required: [
         "checkvintf",
         "host_init_verifier",
+        "secilc",
     ],
     target: {
         darwin: {
diff --git a/tools/releasetools/merge_target_files.py b/tools/releasetools/merge_target_files.py
index 3d9c717..16cab4f 100755
--- a/tools/releasetools/merge_target_files.py
+++ b/tools/releasetools/merge_target_files.py
@@ -93,6 +93,7 @@
 import subprocess
 import sys
 import zipfile
+from xml.etree import ElementTree
 
 import add_img_to_target_files
 import build_super_image
@@ -658,6 +659,80 @@
       os.path.join(output_target_files_dir, 'META', 'vendor_file_contexts.bin'))
 
 
+def compile_split_sepolicy(product_out, partition_map, output_policy):
+  """Uses secilc to compile a split sepolicy file.
+
+  Depends on various */etc/selinux/* and */etc/vintf/* files within partitions.
+
+  Args:
+    product_out: PRODUCT_OUT directory, containing partition directories.
+    partition_map: A map of partition name -> relative path within product_out.
+    output_policy: The name of the output policy created by secilc.
+
+  Returns:
+    A command list that can be executed to create the compiled sepolicy.
+  """
+
+  def get_file(partition, path):
+    if partition not in partition_map:
+      logger.warning('Cannot load SEPolicy files for missing partition %s',
+                     partition)
+      return None
+    return os.path.join(product_out, partition_map[partition], path)
+
+  # Load the kernel sepolicy version from the FCM. This is normally provided
+  # directly to selinux.cpp as a build flag, but is also available in this file.
+  fcm_file = get_file('system', 'etc/vintf/compatibility_matrix.device.xml')
+  if not fcm_file or not os.path.exists(fcm_file):
+    raise ExternalError('Missing required file for loading sepolicy: %s', fcm)
+  kernel_sepolicy_version = ElementTree.parse(fcm_file).getroot().find(
+      'sepolicy/kernel-sepolicy-version').text
+
+  # Load the vendor's plat sepolicy version. This is the version used for
+  # locating sepolicy mapping files.
+  vendor_plat_version_file = get_file('vendor',
+                                      'etc/selinux/plat_sepolicy_vers.txt')
+  if not vendor_plat_version_file or not os.path.exists(
+      vendor_plat_version_file):
+    raise ExternalError('Missing required sepolicy file %s',
+                        vendor_plat_version_file)
+  with open(vendor_plat_version_file) as f:
+    vendor_plat_version = f.read().strip()
+
+  # Use the same flags and arguments as selinux.cpp OpenSplitPolicy().
+  cmd = ['secilc', '-m', '-M', 'true', '-G', '-N']
+  cmd.extend(['-c', kernel_sepolicy_version])
+  cmd.extend(['-o', output_policy])
+  cmd.extend(['-f', '/dev/null'])
+
+  required_policy_files = (
+      ('system', 'etc/selinux/plat_sepolicy.cil'),
+      ('system', 'etc/selinux/mapping/%s.cil' % vendor_plat_version),
+      ('vendor', 'etc/selinux/vendor_sepolicy.cil'),
+      ('vendor', 'etc/selinux/plat_pub_versioned.cil'),
+  )
+  for policy in (map(lambda partition_and_path: get_file(*partition_and_path),
+                     required_policy_files)):
+    if not policy or not os.path.exists(policy):
+      raise ExternalError('Missing required sepolicy file %s', policy)
+    cmd.append(policy)
+
+  optional_policy_files = (
+      ('system', 'etc/selinux/mapping/%s.compat.cil' % vendor_plat_version),
+      ('system_ext', 'etc/selinux/system_ext_sepolicy.cil'),
+      ('system_ext', 'etc/selinux/mapping/%s.cil' % vendor_plat_version),
+      ('product', 'etc/selinux/product_sepolicy.cil'),
+      ('product', 'etc/selinux/mapping/%s.cil' % vendor_plat_version),
+      ('odm', 'etc/selinux/odm_sepolicy.cil'),
+  )
+  for policy in (map(lambda partition_and_path: get_file(*partition_and_path),
+                     optional_policy_files)):
+    if policy and os.path.exists(policy):
+      cmd.append(policy)
+
+  return cmd
+
+
 def process_special_cases(framework_target_files_temp_dir,
                           vendor_target_files_temp_dir,
                           output_target_files_temp_dir,
@@ -977,17 +1052,28 @@
       raise ValueError('sharedUserId APK error. See %s' %
                        shareduid_violation_modules)
 
-  # Run host_init_verifier on the combined init rc files.
+  # host_init_verifier and secilc check only the following partitions:
   filtered_partitions = {
       partition: path
       for partition, path in partition_map.items()
-      # host_init_verifier checks only the following partitions:
       if partition in ['system', 'system_ext', 'product', 'vendor', 'odm']
   }
+
+  # Run host_init_verifier on the combined init rc files.
   common.RunHostInitVerifier(
       product_out=output_target_files_temp_dir,
       partition_map=filtered_partitions)
 
+  # Check that the split sepolicy from the multiple builds can compile.
+  split_sepolicy_cmd = compile_split_sepolicy(
+      product_out=output_target_files_temp_dir,
+      partition_map=filtered_partitions,
+      output_policy=os.path.join(output_target_files_temp_dir,
+                                 'META/combined.policy'))
+  logger.info('Compiling split sepolicy: %s', ' '.join(split_sepolicy_cmd))
+  common.RunAndCheckOutput(split_sepolicy_cmd)
+  # TODO(b/178864050): Run tests on the combined.policy file.
+
   generate_images(output_target_files_temp_dir, rebuild_recovery)
 
   generate_super_empty_image(output_target_files_temp_dir, output_super_empty)
diff --git a/tools/releasetools/test_merge_target_files.py b/tools/releasetools/test_merge_target_files.py
index 7ea7f96..072bb01 100644
--- a/tools/releasetools/test_merge_target_files.py
+++ b/tools/releasetools/test_merge_target_files.py
@@ -18,12 +18,11 @@
 
 import common
 import test_utils
-from merge_target_files import (validate_config_lists,
-                                DEFAULT_FRAMEWORK_ITEM_LIST,
-                                DEFAULT_VENDOR_ITEM_LIST,
-                                DEFAULT_FRAMEWORK_MISC_INFO_KEYS, copy_items,
-                                item_list_to_partition_set,
-                                process_apex_keys_apk_certs_common)
+from merge_target_files import (
+    validate_config_lists, DEFAULT_FRAMEWORK_ITEM_LIST,
+    DEFAULT_VENDOR_ITEM_LIST, DEFAULT_FRAMEWORK_MISC_INFO_KEYS, copy_items,
+    item_list_to_partition_set, process_apex_keys_apk_certs_common,
+    compile_split_sepolicy)
 
 
 class MergeTargetFilesTest(test_utils.ReleaseToolsTestCase):
@@ -235,3 +234,43 @@
     ]
     partition_set = item_list_to_partition_set(item_list)
     self.assertEqual(set(['product', 'system', 'system_ext']), partition_set)
+
+  def test_compile_split_sepolicy(self):
+    product_out_dir = common.MakeTempDir()
+
+    def write_temp_file(path, data=''):
+      full_path = os.path.join(product_out_dir, path)
+      if not os.path.exists(os.path.dirname(full_path)):
+        os.makedirs(os.path.dirname(full_path))
+      with open(full_path, 'w') as f:
+        f.write(data)
+
+    write_temp_file(
+        'system/etc/vintf/compatibility_matrix.device.xml', """
+      <compatibility-matrix>
+        <sepolicy>
+          <kernel-sepolicy-version>30</kernel-sepolicy-version>
+        </sepolicy>
+      </compatibility-matrix>""")
+    write_temp_file('vendor/etc/selinux/plat_sepolicy_vers.txt', '30.0')
+
+    write_temp_file('system/etc/selinux/plat_sepolicy.cil')
+    write_temp_file('system/etc/selinux/mapping/30.0.cil')
+    write_temp_file('product/etc/selinux/mapping/30.0.cil')
+    write_temp_file('vendor/etc/selinux/vendor_sepolicy.cil')
+    write_temp_file('vendor/etc/selinux/plat_pub_versioned.cil')
+
+    cmd = compile_split_sepolicy(product_out_dir, {
+        'system': 'system',
+        'product': 'product',
+        'vendor': 'vendor',
+    }, os.path.join(product_out_dir, 'policy'))
+    self.assertEqual(' '.join(cmd),
+                     ('secilc -m -M true -G -N -c 30 '
+                      '-o {OTP}/policy -f /dev/null '
+                      '{OTP}/system/etc/selinux/plat_sepolicy.cil '
+                      '{OTP}/system/etc/selinux/mapping/30.0.cil '
+                      '{OTP}/vendor/etc/selinux/vendor_sepolicy.cil '
+                      '{OTP}/vendor/etc/selinux/plat_pub_versioned.cil '
+                      '{OTP}/product/etc/selinux/mapping/30.0.cil').format(
+                          OTP=product_out_dir))