Add a script to check VINTF compat of target files package.

Instead of checking META/{system,vendor}_{manifest,matrix}.xml (which
is error-prone because ODM SKU-specific manifests are not checked),
this script read the target files package, remaps its directory
structure so that checkvintf understands it, and check VINTF
compatibility.

Also, put it in otatools.zip.

Test: run it on an extracted target files package
Bug: 131425279
Change-Id: I06036f9a8d7242d4bc11524028be40e780c508e8
diff --git a/core/Makefile b/core/Makefile
index 99b86ee..6f6eee2 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -2698,12 +2698,19 @@
 # -- Kernel version and configurations.
 ifeq ($(PRODUCT_OTA_ENFORCE_VINTF_KERNEL_REQUIREMENTS),true)
 
+intermediates := $(call intermediates-dir-for,ETC,$(notdir $(BUILT_ASSEMBLED_VENDOR_MANIFEST)))
+BUILT_KERNEL_CONFIGS_FILE := $(intermediates)/kernel_configs.txt
+BUILT_KERNEL_VERSION_FILE := $(intermediates)/kernel_version.txt
+
 # BOARD_KERNEL_CONFIG_FILE and BOARD_KERNEL_VERSION can be used to override the values extracted
 # from INSTALLED_KERNEL_TARGET.
 ifdef BOARD_KERNEL_CONFIG_FILE
 ifdef BOARD_KERNEL_VERSION
-$(BUILT_ASSEMBLED_VENDOR_MANIFEST): $(BOARD_KERNEL_CONFIG_FILE)
-$(BUILT_ASSEMBLED_VENDOR_MANIFEST): PRIVATE_FLAGS += --kernel $(BOARD_KERNEL_VERSION):$(BOARD_KERNEL_CONFIG_FILE)
+$(BUILT_KERNEL_CONFIGS_FILE): $(BOARD_KERNEL_CONFIG_FILE)
+	cp $< $@
+$(BUILT_KERNEL_VERSION_FILE):
+	echo $(BOARD_KERNEL_VERSION) > $@
+
 my_board_extracted_kernel := true
 endif # BOARD_KERNEL_VERSION
 endif # BOARD_KERNEL_CONFIG_FILE
@@ -2718,7 +2725,6 @@
     BOARD_KERNEL_VERSION manually; or (3) unsetting PRODUCT_OTA_ENFORCE_VINTF_KERNEL_REQUIREMENTS \
     manually.)
 else
-intermediates := $(call intermediates-dir-for,ETC,$(notdir $(BUILT_ASSEMBLED_VENDOR_MANIFEST)))
 
 # Tools for decompression that is not in PATH.
 # Check $(EXTRACT_KERNEL) for decompression algorithms supported by the script.
@@ -2726,29 +2732,25 @@
 my_decompress_tools := \
     lz4:$(HOST_OUT_EXECUTABLES)/lz4 \
 
-my_kernel_configs := $(intermediates)/kernel_configs.txt
-my_kernel_version := $(intermediates)/kernel_version.txt
-$(my_kernel_configs): .KATI_IMPLICIT_OUTPUTS := $(my_kernel_version)
-$(my_kernel_configs): PRIVATE_KERNEL_VERSION_FILE := $(my_kernel_version)
-$(my_kernel_configs): PRIVATE_DECOMPRESS_TOOLS := $(my_decompress_tools)
-$(my_kernel_configs): $(foreach pair,$(my_decompress_tools),$(call word-colon,2,$(pair)))
-$(my_kernel_configs): $(EXTRACT_KERNEL) $(INSTALLED_KERNEL_TARGET)
+$(BUILT_KERNEL_CONFIGS_FILE): .KATI_IMPLICIT_OUTPUTS := $(BUILT_KERNEL_VERSION_FILE)
+$(BUILT_KERNEL_CONFIGS_FILE): PRIVATE_DECOMPRESS_TOOLS := $(my_decompress_tools)
+$(BUILT_KERNEL_CONFIGS_FILE): $(foreach pair,$(my_decompress_tools),$(call word-colon,2,$(pair)))
+$(BUILT_KERNEL_CONFIGS_FILE): $(EXTRACT_KERNEL) $(INSTALLED_KERNEL_TARGET)
 	$< --tools $(PRIVATE_DECOMPRESS_TOOLS) --input $(INSTALLED_KERNEL_TARGET) \
 	  --output-configs $@ \
-	  --output-version $(PRIVATE_KERNEL_VERSION_FILE)
-
-$(BUILT_ASSEMBLED_VENDOR_MANIFEST): $(my_kernel_configs) $(my_kernel_version)
-$(BUILT_ASSEMBLED_VENDOR_MANIFEST): PRIVATE_FLAGS += --kernel $$(cat $(my_kernel_version)):$(my_kernel_configs)
+	  --output-version $(BUILT_KERNEL_VERSION_FILE)
 
 intermediates :=
-my_kernel_configs :=
-my_kernel_version :=
 my_decompress_tools :=
 
 endif # my_board_extracted_kernel
 my_board_extracted_kernel :=
 
 endif # INSTALLED_KERNEL_TARGET
+
+$(BUILT_ASSEMBLED_VENDOR_MANIFEST): $(BUILT_KERNEL_CONFIGS_FILE) $(BUILT_KERNEL_VERSION_FILE)
+$(BUILT_ASSEMBLED_VENDOR_MANIFEST): PRIVATE_FLAGS += --kernel $$(cat $(BUILT_KERNEL_VERSION_FILE)):$(BUILT_KERNEL_CONFIGS_FILE)
+
 endif # PRODUCT_OTA_ENFORCE_VINTF_KERNEL_REQUIREMENTS
 
 $(BUILT_ASSEMBLED_VENDOR_MANIFEST):
@@ -3614,6 +3616,7 @@
   care_map_generator \
   check_ota_package_signature \
   check_target_files_signatures \
+  check_target_files_vintf \
   checkvintf \
   delta_generator \
   e2fsck \
@@ -3846,6 +3849,13 @@
 endif # BOARD_AVB_ENABLE
 endif # BOARD_PREBUILT_DTBOIMAGE
 	$(call dump-dynamic-partitions-info,$@)
+	@# VINTF checks
+ifeq ($(PRODUCT_ENFORCE_VINTF_MANIFEST),true)
+	$(hide) echo "vintf_enforce=true" >> $@
+endif
+ifdef ODM_MANIFEST_SKUS
+	$(hide) echo "vintf_odm_manifest_skus=$(ODM_MANIFEST_SKUS)" >> $@
+endif
 
 .PHONY: misc_info
 misc_info: $(INSTALLED_MISC_INFO_TARGET)
@@ -4005,6 +4015,8 @@
 	    $(BUILT_ASSEMBLED_VENDOR_MANIFEST) \
 	    $(BUILT_SYSTEM_MATRIX) \
 	    $(BUILT_VENDOR_MATRIX) \
+	    $(BUILT_KERNEL_CONFIGS_FILE) \
+	    $(BUILT_KERNEL_VERSION_FILE) \
 	    | $(ACP)
 	@echo "Package target files: $@"
 	$(call create-system-vendor-symlink)
@@ -4253,6 +4265,12 @@
 ifdef BUILT_VENDOR_MATRIX
 	$(hide) cp $(BUILT_VENDOR_MATRIX) $(zip_root)/META/vendor_matrix.xml
 endif
+ifdef BUILT_KERNEL_CONFIGS_FILE
+	$(hide) cp $(BUILT_KERNEL_CONFIGS_FILE) $(zip_root)/META/kernel_configs.txt
+endif
+ifdef BUILT_KERNEL_VERSION_FILE
+	$(hide) cp $(BUILT_KERNEL_VERSION_FILE) $(zip_root)/META/kernel_version.txt
+endif
 ifneq ($(BOARD_SUPER_PARTITION_GROUPS),)
 	$(hide) echo "super_partition_groups=$(BOARD_SUPER_PARTITION_GROUPS)" > $(zip_root)/META/dynamic_partitions_info.txt
 	@# Remove 'vendor' from the group partition list if the image is not available. This should only
diff --git a/tools/releasetools/Android.bp b/tools/releasetools/Android.bp
index 0302856..43f52be 100644
--- a/tools/releasetools/Android.bp
+++ b/tools/releasetools/Android.bp
@@ -76,6 +76,19 @@
 }
 
 python_defaults {
+    name: "releasetools_check_target_files_vintf_defaults",
+    srcs: [
+        "check_target_files_vintf.py",
+    ],
+    libs: [
+        "releasetools_common",
+    ],
+    required: [
+        "checkvintf",
+    ],
+}
+
+python_defaults {
     name: "releasetools_ota_from_target_files_defaults",
     srcs: [
         "edify_generator.py",
@@ -266,6 +279,14 @@
 }
 
 python_binary_host {
+    name: "check_target_files_vintf",
+    defaults: [
+        "releasetools_binary_defaults",
+        "releasetools_check_target_files_vintf_defaults"
+    ],
+}
+
+python_binary_host {
     name: "img_from_target_files",
     defaults: [
         "releasetools_binary_defaults",
diff --git a/tools/releasetools/check_target_files_vintf.py b/tools/releasetools/check_target_files_vintf.py
new file mode 100755
index 0000000..543147c
--- /dev/null
+++ b/tools/releasetools/check_target_files_vintf.py
@@ -0,0 +1,232 @@
+#!/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.
+
+"""
+Check VINTF compatibility from a target files package.
+
+Usage: check_target_files_vintf target_files
+
+target_files can be a ZIP file or an extracted target files directory.
+"""
+
+import logging
+import subprocess
+import sys
+import os
+import zipfile
+
+import common
+
+logger = logging.getLogger(__name__)
+
+OPTIONS = common.OPTIONS
+
+# Keys are paths that VINTF searches. Must keep in sync with libvintf's search
+# paths (VintfObject.cpp).
+# These paths are stored in different directories in target files package, so
+# we have to search for the correct path and tell checkvintf to remap them.
+DIR_SEARCH_PATHS = {
+    '/system': ('SYSTEM',),
+    '/vendor': ('VENDOR', 'SYSTEM/vendor'),
+    '/product': ('PRODUCT', 'SYSTEM/product'),
+    '/odm': ('ODM', 'VENDOR/odm'),
+}
+
+UNZIP_PATTERN = ['META/*', '*/build.prop']
+
+
+def GetDirmap(input_tmp):
+  dirmap = {}
+  for device_path, target_files_rel_paths in DIR_SEARCH_PATHS.items():
+    for target_files_rel_path in target_files_rel_paths:
+      target_files_path = os.path.join(input_tmp, target_files_rel_path)
+      if os.path.isdir(target_files_path):
+        dirmap[device_path] = target_files_path
+        break
+    if device_path not in dirmap:
+      raise ValueError("Can't determine path for device path " + device_path +
+                       ". Searched the following:" +
+                       ("\n".join(target_files_rel_paths)))
+  return dirmap
+
+
+def GetArgsForSkus(info_dict):
+  skus = info_dict.get('vintf_odm_manifest_skus', '').strip().split()
+  if not skus:
+    logger.info("ODM_MANIFEST_SKUS is not defined. Check once without SKUs.")
+    skus = ['']
+  return [['--property', 'ro.boot.product.hardware.sku=' + sku]
+          for sku in skus]
+
+
+def GetArgsForShippingApiLevel(info_dict):
+  shipping_api_level = info_dict['vendor.build.prop'].get(
+      'ro.product.first_api_level')
+  if not shipping_api_level:
+    logger.warning('Cannot determine ro.product.first_api_level')
+    return []
+  return ['--property', 'ro.product.first_api_level=' + shipping_api_level]
+
+
+def GetArgsForKernel(input_tmp):
+  version_path = os.path.join(input_tmp, 'META/kernel_version.txt')
+  config_path = os.path.join(input_tmp, 'META/kernel_configs.txt')
+
+  if not os.path.isfile(version_path) or not os.path.isfile(config_path):
+    logger.info('Skipping kernel config checks because ' +
+                'PRODUCT_OTA_ENFORCE_VINTF_KERNEL_REQUIREMENTS is not set')
+    return []
+
+  with open(version_path) as f:
+    version = f.read().strip()
+
+  return ['--kernel', '{}:{}'.format(version, config_path)]
+
+
+def CheckVintfFromExtractedTargetFiles(input_tmp, info_dict=None):
+  """
+  Checks VINTF metadata of an extracted target files directory.
+
+  Args:
+    inp: path to the directory that contains the extracted target files archive.
+    info_dict: The build-time info dict. If None, it will be loaded from inp.
+
+  Returns:
+    True if VINTF check is skipped or compatible, False if incompatible. Raise
+    a RuntimeError if any error occurs.
+  """
+
+  if info_dict is None:
+    info_dict = common.LoadInfoDict(input_tmp)
+
+  if info_dict.get('vintf_enforce') != 'true':
+    logger.warning('PRODUCT_ENFORCE_VINTF_MANIFEST is not set, skipping checks')
+    return True
+
+  dirmap = GetDirmap(input_tmp)
+  args_for_skus = GetArgsForSkus(info_dict)
+  shipping_api_level_args = GetArgsForShippingApiLevel(info_dict)
+  kernel_args = GetArgsForKernel(input_tmp)
+
+  common_command = [
+      'checkvintf',
+      '--check-compat',
+  ]
+  for device_path, real_path in dirmap.items():
+    common_command += ['--dirmap', '{}:{}'.format(device_path, real_path)]
+  common_command += kernel_args
+  common_command += shipping_api_level_args
+
+  success = True
+  for sku_args in args_for_skus:
+    command = common_command + sku_args
+    proc = common.Run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    out, err = proc.communicate()
+    if proc.returncode == 0:
+      logger.info("Command `%s` returns 'compatible'", ' '.join(command))
+    elif out.strip() == "INCOMPATIBLE":
+      logger.info("Command `%s` returns 'incompatible'", ' '.join(command))
+      success = False
+    else:
+      raise common.ExternalError(
+          "Failed to run command '{}' (exit code {}):\nstdout:{}\nstderr:{}"
+          .format(' '.join(command), proc.returncode, out, err))
+    logger.info("stdout: %s", out)
+    logger.info("stderr: %s", err)
+
+  return success
+
+
+def GetVintfFileList():
+  """
+  Returns a list of VINTF metadata files that should be read from a target files
+  package before executing checkvintf.
+  """
+  def PathToPatterns(path):
+    if path[-1] == '/':
+      path += '*'
+    for device_path, target_files_rel_paths in DIR_SEARCH_PATHS.items():
+      if path.startswith(device_path):
+        suffix = path[len(device_path):]
+        return [rel_path + suffix for rel_path in target_files_rel_paths]
+    raise RuntimeError('Unrecognized path from checkvintf --dump-file-list: ' +
+                       path)
+
+  out = common.RunAndCheckOutput(['checkvintf', '--dump-file-list'])
+  paths = out.strip().split('\n')
+  paths = sum((PathToPatterns(path) for path in paths if path), [])
+  return paths
+
+
+def CheckVintfFromTargetFiles(inp, info_dict=None):
+  """
+  Checks VINTF metadata of a target files zip.
+
+  Args:
+    inp: path to the target files archive.
+    info_dict: The build-time info dict. If None, it will be loaded from inp.
+
+  Returns:
+    True if VINTF check is skipped or compatible, False if incompatible. Raise
+    a RuntimeError if any error occurs.
+  """
+  input_tmp = common.UnzipTemp(inp, GetVintfFileList() + UNZIP_PATTERN)
+  return CheckVintfFromExtractedTargetFiles(input_tmp, info_dict)
+
+
+def CheckVintf(inp, info_dict=None):
+  """
+  Checks VINTF metadata of a target files zip or extracted target files
+  directory.
+
+  Args:
+    inp: path to the (possibly extracted) target files archive.
+    info_dict: The build-time info dict. If None, it will be loaded from inp.
+
+  Returns:
+    True if VINTF check is skipped or compatible, False if incompatible. Raise
+    a RuntimeError if any error occurs.
+  """
+  if os.path.isdir(inp):
+    logger.info('Checking VINTF compatibility extracted target files...')
+    return CheckVintfFromExtractedTargetFiles(inp, info_dict)
+
+  if zipfile.is_zipfile(inp):
+    logger.info('Checking VINTF compatibility target files...')
+    return CheckVintfFromTargetFiles(inp, info_dict)
+
+  raise ValueError('{} is not a valid directory or zip file'.format(inp))
+
+
+def main(argv):
+  args = common.ParseOptions(argv, __doc__)
+  if len(args) != 1:
+    common.Usage(__doc__)
+    sys.exit(1)
+  common.InitLogging()
+  if not CheckVintf(args[0]):
+    sys.exit(1)
+
+
+if __name__ == '__main__':
+  try:
+    common.CloseInheritedPipes()
+    main(sys.argv[1:])
+  except common.ExternalError:
+    logger.exception('\n   ERROR:\n')
+    sys.exit(1)
+  finally:
+    common.Cleanup()