Reland "Add a wrapper class PartitionBuildProp""

This reverts commit 6022545272a7360f7faa5e00ad0e7a8f5ed9d5c3.

The build prop for a partition used to be a simple key:value
dictionary. But we need more fields to hold the alternative build
props overriden by the 'import' statement. Therefore, add a new
class as a wrapper for these props first.

Bug: 152167826
Change-Id: I5c952cd2a976ba1a09ddc66d56c2b8b55a61986b
Test: unittests pass
diff --git a/tools/releasetools/add_img_to_target_files.py b/tools/releasetools/add_img_to_target_files.py
index 7db506c..70ea967 100755
--- a/tools/releasetools/add_img_to_target_files.py
+++ b/tools/releasetools/add_img_to_target_files.py
@@ -548,17 +548,19 @@
       care_map_list += care_map
 
       # adds fingerprint field to the care_map
-      build_props = OPTIONS.info_dict.get(partition + ".build.prop", {})
+      # TODO(xunchang) revisit the fingerprint calculation for care_map.
+      partition_props = OPTIONS.info_dict.get(partition + ".build.prop")
       prop_name_list = ["ro.{}.build.fingerprint".format(partition),
                         "ro.{}.build.thumbprint".format(partition)]
 
-      present_props = [x for x in prop_name_list if x in build_props]
+      present_props = [x for x in prop_name_list if
+                       partition_props and partition_props.GetProp(x)]
       if not present_props:
         logger.warning("fingerprint is not present for partition %s", partition)
         property_id, fingerprint = "unknown", "unknown"
       else:
         property_id = present_props[0]
-        fingerprint = build_props[property_id]
+        fingerprint = partition_props.GetProp(property_id)
       care_map_list += [property_id, fingerprint]
 
   if not care_map_list:
diff --git a/tools/releasetools/build_image.py b/tools/releasetools/build_image.py
index f30f787..7567346 100755
--- a/tools/releasetools/build_image.py
+++ b/tools/releasetools/build_image.py
@@ -509,9 +509,9 @@
   d = {}
 
   if "build.prop" in glob_dict:
-    bp = glob_dict["build.prop"]
-    if "ro.build.date.utc" in bp:
-      d["timestamp"] = bp["ro.build.date.utc"]
+    timestamp = glob_dict["build.prop"].GetProp("ro.build.date.utc")
+    if timestamp:
+      d["timestamp"] = timestamp
 
   def copy_prop(src_p, dest_p):
     """Copy a property from the global dictionary.
diff --git a/tools/releasetools/check_target_files_vintf.py b/tools/releasetools/check_target_files_vintf.py
index b3d491f..95d09cc 100755
--- a/tools/releasetools/check_target_files_vintf.py
+++ b/tools/releasetools/check_target_files_vintf.py
@@ -80,8 +80,9 @@
            '--property', 'ro.boot.product.vendor.sku=' + vendor_sku]
           for odm_sku in odm_skus for vendor_sku in vendor_skus]
 
+
 def GetArgsForShippingApiLevel(info_dict):
-  shipping_api_level = info_dict['vendor.build.prop'].get(
+  shipping_api_level = info_dict['vendor.build.prop'].GetProp(
       'ro.product.first_api_level')
   if not shipping_api_level:
     logger.warning('Cannot determine ro.product.first_api_level')
diff --git a/tools/releasetools/common.py b/tools/releasetools/common.py
index 1abf5a5..71ae21f 100644
--- a/tools/releasetools/common.py
+++ b/tools/releasetools/common.py
@@ -423,6 +423,14 @@
   def items(self):
     return self.info_dict.items()
 
+  def _GetRawBuildProp(self, prop, partition):
+    prop_file = '{}.build.prop'.format(
+        partition) if partition else 'build.prop'
+    partition_props = self.info_dict.get(prop_file)
+    if not partition_props:
+      return None
+    return partition_props.GetProp(prop)
+
   def GetPartitionBuildProp(self, prop, partition):
     """Returns the inquired build property for the provided partition."""
     # If provided a partition for this property, only look within that
@@ -431,31 +439,33 @@
       prop = prop.replace("ro.product", "ro.product.{}".format(partition))
     else:
       prop = prop.replace("ro.", "ro.{}.".format(partition))
-    try:
-      return self.info_dict.get("{}.build.prop".format(partition), {})[prop]
-    except KeyError:
-      raise ExternalError("couldn't find %s in %s.build.prop" %
-                          (prop, partition))
+
+    prop_val = self._GetRawBuildProp(prop, partition)
+    if prop_val is not None:
+      return prop_val
+    raise ExternalError("couldn't find %s in %s.build.prop" %
+                        (prop, partition))
 
   def GetBuildProp(self, prop):
     """Returns the inquired build property from the standard build.prop file."""
     if prop in BuildInfo._RO_PRODUCT_RESOLVE_PROPS:
       return self._ResolveRoProductBuildProp(prop)
 
-    try:
-      return self.info_dict.get("build.prop", {})[prop]
-    except KeyError:
-      raise ExternalError("couldn't find %s in build.prop" % (prop,))
+    prop_val = self._GetRawBuildProp(prop, None)
+    if prop_val is not None:
+      return prop_val
+
+    raise ExternalError("couldn't find %s in build.prop" % (prop,))
 
   def _ResolveRoProductBuildProp(self, prop):
     """Resolves the inquired ro.product.* build property"""
-    prop_val = self.info_dict.get("build.prop", {}).get(prop)
+    prop_val = self._GetRawBuildProp(prop, None)
     if prop_val:
       return prop_val
 
     default_source_order = self._GetRoProductPropsDefaultSourceOrder()
-    source_order_val = self.info_dict.get("build.prop", {}).get(
-        "ro.product.property_source_order")
+    source_order_val = self._GetRawBuildProp(
+        "ro.product.property_source_order", None)
     if source_order_val:
       source_order = source_order_val.split(",")
     else:
@@ -466,11 +476,10 @@
       raise ExternalError(
           "Invalid ro.product.property_source_order '{}'".format(source_order))
 
-    for source in source_order:
+    for source_partition in source_order:
       source_prop = prop.replace(
-          "ro.product", "ro.product.{}".format(source), 1)
-      prop_val = self.info_dict.get(
-          "{}.build.prop".format(source), {}).get(source_prop)
+          "ro.product", "ro.product.{}".format(source_partition), 1)
+      prop_val = self._GetRawBuildProp(source_prop, source_partition)
       if prop_val:
         return prop_val
 
@@ -479,11 +488,9 @@
   def _GetRoProductPropsDefaultSourceOrder(self):
     # NOTE: refer to CDDs and android.os.Build.VERSION for the definition and
     # values of these properties for each Android release.
-    android_codename = self.info_dict.get("build.prop", {}).get(
-        "ro.build.version.codename")
+    android_codename = self._GetRawBuildProp("ro.build.version.codename", None)
     if android_codename == "REL":
-      android_version = self.info_dict.get("build.prop", {}).get(
-          "ro.build.version.release")
+      android_version = self._GetRawBuildProp("ro.build.version.release", None)
       if android_version == "10":
         return BuildInfo._RO_PRODUCT_PROPS_DEFAULT_SOURCE_ORDER_ANDROID_10
       # NOTE: float() conversion of android_version will have rounding error.
@@ -566,6 +573,20 @@
       script.AssertOemProperty(prop, values, oem_no_mount)
 
 
+def ReadFromInputFile(input_file, fn):
+  """Reads the contents of fn from input zipfile or directory."""
+  if isinstance(input_file, zipfile.ZipFile):
+    return input_file.read(fn).decode()
+  else:
+    path = os.path.join(input_file, *fn.split("/"))
+    try:
+      with open(path) as f:
+        return f.read()
+    except IOError as e:
+      if e.errno == errno.ENOENT:
+        raise KeyError(fn)
+
+
 def LoadInfoDict(input_file, repacking=False):
   """Loads the key/value pairs from the given input target_files.
 
@@ -603,16 +624,7 @@
         "input_file must be a path str when doing repacking"
 
   def read_helper(fn):
-    if isinstance(input_file, zipfile.ZipFile):
-      return input_file.read(fn).decode()
-    else:
-      path = os.path.join(input_file, *fn.split("/"))
-      try:
-        with open(path) as f:
-          return f.read()
-      except IOError as e:
-        if e.errno == errno.ENOENT:
-          raise KeyError(fn)
+    return ReadFromInputFile(input_file, fn)
 
   try:
     d = LoadDictionaryFromLines(read_helper("META/misc_info.txt").split("\n"))
@@ -675,13 +687,8 @@
   # system and vendor.
   for partition in PARTITIONS_WITH_CARE_MAP:
     partition_prop = "{}.build.prop".format(partition)
-    d[partition_prop] = LoadBuildProp(
-        read_helper, "{}/build.prop".format(partition.upper()))
-    # Some partition might use /<partition>/etc/build.prop as the new path.
-    # TODO: try new path first when majority of them switch to the new path.
-    if not d[partition_prop]:
-      d[partition_prop] = LoadBuildProp(
-          read_helper, "{}/etc/build.prop".format(partition.upper()))
+    d[partition_prop] = PartitionBuildProps.FromInputFile(
+        input_file, partition)
   d["build.prop"] = d["system.build.prop"]
 
   # Set up the salt (based on fingerprint) that will be used when adding AVB
@@ -696,15 +703,6 @@
   return d
 
 
-def LoadBuildProp(read_helper, prop_file):
-  try:
-    data = read_helper(prop_file)
-  except KeyError:
-    logger.warning("Failed to read %s", prop_file)
-    data = ""
-  return LoadDictionaryFromLines(data.split("\n"))
-
-
 def LoadListFromFile(file_path):
   with open(file_path) as f:
     return f.read().splitlines()
@@ -727,6 +725,61 @@
   return d
 
 
+class PartitionBuildProps(object):
+  """The class holds the build prop of a particular partition.
+
+  This class loads the build.prop and holds the build properties for a given
+  partition. It also partially recognizes the 'import' statement in the
+  build.prop; and calculates alternative values of some specific build
+  properties during runtime.
+
+  Attributes:
+    input_file: a zipped target-file or an unzipped target-file directory.
+    partition: name of the partition.
+    props_allow_override: a list of build properties to search for the
+        alternative values during runtime.
+    build_props: a dictionary of build properties for the given partition.
+    prop_overrides: a dict of list. And each list holds the overridden values
+        for props_allow_override.
+  """
+
+  def __init__(self, input_file, name):
+    self.input_file = input_file
+    self.partition = name
+    self.props_allow_override = [props.format(name) for props in [
+        'ro.product.{}.name', 'ro.product.{}.device']]
+    self.build_props = {}
+    self.prop_overrides = {}
+
+  @staticmethod
+  def FromDictionary(name, build_props):
+    """Constructs an instance from a build prop dictionary."""
+
+    props = PartitionBuildProps("unknown", name)
+    props.build_props = build_props.copy()
+    return props
+
+  @staticmethod
+  def FromInputFile(input_file, name):
+    """Loads the build.prop file and builds the attributes."""
+
+    data = ''
+    for prop_file in ['{}/etc/build.prop'.format(name.upper()),
+                      '{}/build.prop'.format(name.upper())]:
+      try:
+        data = ReadFromInputFile(input_file, prop_file)
+        break
+      except KeyError:
+        logger.warning('Failed to read %s', prop_file)
+
+    props = PartitionBuildProps(input_file, name)
+    props.build_props = LoadDictionaryFromLines(data.split('\n'))
+    return props
+
+  def GetProp(self, prop):
+    return self.build_props.get(prop)
+
+
 def LoadRecoveryFSTab(read_helper, fstab_version, recovery_fstab_path,
                       system_root_image=False):
   class Partition(object):
diff --git a/tools/releasetools/test_add_img_to_target_files.py b/tools/releasetools/test_add_img_to_target_files.py
index 3d0766f..c82a40b 100644
--- a/tools/releasetools/test_add_img_to_target_files.py
+++ b/tools/releasetools/test_add_img_to_target_files.py
@@ -128,13 +128,16 @@
         'vendor_image_size' : 40960,
         'system_verity_block_device': '/dev/block/system',
         'vendor_verity_block_device': '/dev/block/vendor',
-        'system.build.prop': {
-            'ro.system.build.fingerprint':
-                'google/sailfish/12345:user/dev-keys',
-        },
-        'vendor.build.prop': {
-            'ro.vendor.build.fingerprint': 'google/sailfish/678:user/dev-keys',
-        },
+        'system.build.prop': common.PartitionBuildProps.FromDictionary(
+            'system', {
+                'ro.system.build.fingerprint':
+                'google/sailfish/12345:user/dev-keys'}
+        ),
+        'vendor.build.prop': common.PartitionBuildProps.FromDictionary(
+            'vendor', {
+                'ro.vendor.build.fingerprint':
+                'google/sailfish/678:user/dev-keys'}
+        ),
     }
 
     # Prepare the META/ folder.
@@ -206,18 +209,21 @@
     """Tests the case for device using AVB."""
     image_paths = self._test_AddCareMapForAbOta()
     OPTIONS.info_dict = {
-        'extfs_sparse_flag' : '-s',
-        'system_image_size' : 65536,
-        'vendor_image_size' : 40960,
-        'avb_system_hashtree_enable' : 'true',
-        'avb_vendor_hashtree_enable' : 'true',
-        'system.build.prop': {
-            'ro.system.build.fingerprint':
-                'google/sailfish/12345:user/dev-keys',
-        },
-        'vendor.build.prop': {
-            'ro.vendor.build.fingerprint': 'google/sailfish/678:user/dev-keys',
-        }
+        'extfs_sparse_flag': '-s',
+        'system_image_size': 65536,
+        'vendor_image_size': 40960,
+        'avb_system_hashtree_enable': 'true',
+        'avb_vendor_hashtree_enable': 'true',
+        'system.build.prop': common.PartitionBuildProps.FromDictionary(
+            'system', {
+                'ro.system.build.fingerprint':
+                'google/sailfish/12345:user/dev-keys'}
+        ),
+        'vendor.build.prop': common.PartitionBuildProps.FromDictionary(
+            'vendor', {
+                'ro.vendor.build.fingerprint':
+                'google/sailfish/678:user/dev-keys'}
+        ),
     }
 
     AddCareMapForAbOta(None, ['system', 'vendor'], image_paths)
@@ -258,17 +264,21 @@
     """Tests the case for partitions with thumbprint."""
     image_paths = self._test_AddCareMapForAbOta()
     OPTIONS.info_dict = {
-        'extfs_sparse_flag' : '-s',
-        'system_image_size' : 65536,
-        'vendor_image_size' : 40960,
+        'extfs_sparse_flag': '-s',
+        'system_image_size': 65536,
+        'vendor_image_size': 40960,
         'system_verity_block_device': '/dev/block/system',
         'vendor_verity_block_device': '/dev/block/vendor',
-        'system.build.prop': {
-            'ro.system.build.thumbprint': 'google/sailfish/123:user/dev-keys',
-        },
-        'vendor.build.prop' : {
-            'ro.vendor.build.thumbprint': 'google/sailfish/456:user/dev-keys',
-        },
+        'system.build.prop': common.PartitionBuildProps.FromDictionary(
+            'system', {
+                'ro.system.build.thumbprint':
+                'google/sailfish/123:user/dev-keys'}
+        ),
+        'vendor.build.prop': common.PartitionBuildProps.FromDictionary(
+            'vendor', {
+                'ro.vendor.build.thumbprint':
+                'google/sailfish/456:user/dev-keys'}
+        ),
     }
 
     AddCareMapForAbOta(None, ['system', 'vendor'], image_paths)
diff --git a/tools/releasetools/test_common.py b/tools/releasetools/test_common.py
index 665eb51..c0c2d3f 100644
--- a/tools/releasetools/test_common.py
+++ b/tools/releasetools/test_common.py
@@ -48,109 +48,124 @@
 class BuildInfoTest(test_utils.ReleaseToolsTestCase):
 
   TEST_INFO_DICT = {
-      'build.prop' : {
-          'ro.product.device' : 'product-device',
-          'ro.product.name' : 'product-name',
-          'ro.build.fingerprint' : 'build-fingerprint',
-          'ro.build.foo' : 'build-foo',
-      },
-      'system.build.prop' : {
-          'ro.product.system.brand' : 'product-brand',
-          'ro.product.system.name' : 'product-name',
-          'ro.product.system.device' : 'product-device',
-          'ro.system.build.version.release' : 'version-release',
-          'ro.system.build.id' : 'build-id',
-          'ro.system.build.version.incremental' : 'version-incremental',
-          'ro.system.build.type' : 'build-type',
-          'ro.system.build.tags' : 'build-tags',
-          'ro.system.build.foo' : 'build-foo',
-      },
-      'vendor.build.prop' : {
-          'ro.product.vendor.brand' : 'vendor-product-brand',
-          'ro.product.vendor.name' : 'vendor-product-name',
-          'ro.product.vendor.device' : 'vendor-product-device',
-          'ro.vendor.build.version.release' : 'vendor-version-release',
-          'ro.vendor.build.id' : 'vendor-build-id',
-          'ro.vendor.build.version.incremental' : 'vendor-version-incremental',
-          'ro.vendor.build.type' : 'vendor-build-type',
-          'ro.vendor.build.tags' : 'vendor-build-tags',
-      },
-      'property1' : 'value1',
-      'property2' : 4096,
+      'build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.product.device': 'product-device',
+              'ro.product.name': 'product-name',
+              'ro.build.fingerprint': 'build-fingerprint',
+              'ro.build.foo': 'build-foo'}
+      ),
+      'system.build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.product.system.brand': 'product-brand',
+              'ro.product.system.name': 'product-name',
+              'ro.product.system.device': 'product-device',
+              'ro.system.build.version.release': 'version-release',
+              'ro.system.build.id': 'build-id',
+              'ro.system.build.version.incremental': 'version-incremental',
+              'ro.system.build.type': 'build-type',
+              'ro.system.build.tags': 'build-tags',
+              'ro.system.build.foo': 'build-foo'}
+      ),
+      'vendor.build.prop': common.PartitionBuildProps.FromDictionary(
+          'vendor', {
+              'ro.product.vendor.brand': 'vendor-product-brand',
+              'ro.product.vendor.name': 'vendor-product-name',
+              'ro.product.vendor.device': 'vendor-product-device',
+              'ro.vendor.build.version.release': 'vendor-version-release',
+              'ro.vendor.build.id': 'vendor-build-id',
+              'ro.vendor.build.version.incremental':
+              'vendor-version-incremental',
+              'ro.vendor.build.type': 'vendor-build-type',
+              'ro.vendor.build.tags': 'vendor-build-tags'}
+      ),
+      'property1': 'value1',
+      'property2': 4096,
   }
 
   TEST_INFO_DICT_USES_OEM_PROPS = {
-      'build.prop' : {
-          'ro.product.name' : 'product-name',
-          'ro.build.thumbprint' : 'build-thumbprint',
-          'ro.build.bar' : 'build-bar',
-      },
-      'vendor.build.prop' : {
-          'ro.vendor.build.fingerprint' : 'vendor-build-fingerprint',
-      },
-      'property1' : 'value1',
-      'property2' : 4096,
-      'oem_fingerprint_properties' : 'ro.product.device ro.product.brand',
+      'build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.product.name': 'product-name',
+              'ro.build.thumbprint': 'build-thumbprint',
+              'ro.build.bar': 'build-bar'}
+      ),
+      'vendor.build.prop': common.PartitionBuildProps.FromDictionary(
+          'vendor', {
+              'ro.vendor.build.fingerprint': 'vendor-build-fingerprint'}
+      ),
+      'property1': 'value1',
+      'property2': 4096,
+      'oem_fingerprint_properties': 'ro.product.device ro.product.brand',
   }
 
   TEST_OEM_DICTS = [
       {
-          'ro.product.brand' : 'brand1',
-          'ro.product.device' : 'device1',
+          'ro.product.brand': 'brand1',
+          'ro.product.device': 'device1',
       },
       {
-          'ro.product.brand' : 'brand2',
-          'ro.product.device' : 'device2',
+          'ro.product.brand': 'brand2',
+          'ro.product.device': 'device2',
       },
       {
-          'ro.product.brand' : 'brand3',
-          'ro.product.device' : 'device3',
+          'ro.product.brand': 'brand3',
+          'ro.product.device': 'device3',
       },
   ]
 
   TEST_INFO_DICT_PROPERTY_SOURCE_ORDER = {
-      'build.prop' : {
-          'ro.build.fingerprint' : 'build-fingerprint',
-          'ro.product.property_source_order' :
-              'product,odm,vendor,system_ext,system',
-      },
-      'system.build.prop' : {
-          'ro.product.system.device' : 'system-product-device',
-      },
-      'vendor.build.prop' : {
-          'ro.product.vendor.device' : 'vendor-product-device',
-      },
+      'build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.build.fingerprint': 'build-fingerprint',
+              'ro.product.property_source_order':
+                  'product,odm,vendor,system_ext,system'}
+      ),
+      'system.build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.product.system.device': 'system-product-device'}
+      ),
+      'vendor.build.prop': common.PartitionBuildProps.FromDictionary(
+          'vendor', {
+              'ro.product.vendor.device': 'vendor-product-device'}
+      ),
   }
 
   TEST_INFO_DICT_PROPERTY_SOURCE_ORDER_ANDROID_10 = {
-      'build.prop' : {
-          'ro.build.fingerprint' : 'build-fingerprint',
-          'ro.product.property_source_order' :
-              'product,product_services,odm,vendor,system',
-          'ro.build.version.release' : '10',
-          'ro.build.version.codename' : 'REL',
-      },
-      'system.build.prop' : {
-          'ro.product.system.device' : 'system-product-device',
-      },
-      'vendor.build.prop' : {
-          'ro.product.vendor.device' : 'vendor-product-device',
-      },
+      'build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.build.fingerprint': 'build-fingerprint',
+              'ro.product.property_source_order':
+                  'product,product_services,odm,vendor,system',
+              'ro.build.version.release': '10',
+              'ro.build.version.codename': 'REL'}
+      ),
+      'system.build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.product.system.device': 'system-product-device'}
+      ),
+      'vendor.build.prop': common.PartitionBuildProps.FromDictionary(
+          'vendor', {
+              'ro.product.vendor.device': 'vendor-product-device'}
+      ),
   }
 
   TEST_INFO_DICT_PROPERTY_SOURCE_ORDER_ANDROID_9 = {
-      'build.prop' : {
-          'ro.product.device' : 'product-device',
-          'ro.build.fingerprint' : 'build-fingerprint',
-          'ro.build.version.release' : '9',
-          'ro.build.version.codename' : 'REL',
-      },
-      'system.build.prop' : {
-          'ro.product.system.device' : 'system-product-device',
-      },
-      'vendor.build.prop' : {
-          'ro.product.vendor.device' : 'vendor-product-device',
-      },
+      'build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.product.device': 'product-device',
+              'ro.build.fingerprint': 'build-fingerprint',
+              'ro.build.version.release': '9',
+              'ro.build.version.codename': 'REL'}
+      ),
+      'system.build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.product.system.device': 'system-product-device'}
+      ),
+      'vendor.build.prop': common.PartitionBuildProps.FromDictionary(
+          'vendor', {
+              'ro.product.vendor.device': 'vendor-product-device'}
+      ),
   }
 
   def test_init(self):
@@ -178,25 +193,27 @@
 
   def test_init_badFingerprint(self):
     info_dict = copy.deepcopy(self.TEST_INFO_DICT)
-    info_dict['build.prop']['ro.build.fingerprint'] = 'bad fingerprint'
+    info_dict['build.prop'].build_props[
+        'ro.build.fingerprint'] = 'bad fingerprint'
     self.assertRaises(ValueError, common.BuildInfo, info_dict, None)
 
-    info_dict['build.prop']['ro.build.fingerprint'] = 'bad\x80fingerprint'
+    info_dict['build.prop'].build_props[
+        'ro.build.fingerprint'] = 'bad\x80fingerprint'
     self.assertRaises(ValueError, common.BuildInfo, info_dict, None)
 
   def test___getitem__(self):
     target_info = common.BuildInfo(self.TEST_INFO_DICT, None)
     self.assertEqual('value1', target_info['property1'])
     self.assertEqual(4096, target_info['property2'])
-    self.assertEqual('build-foo', target_info['build.prop']['ro.build.foo'])
+    self.assertEqual('build-foo',
+                     target_info['build.prop'].GetProp('ro.build.foo'))
 
   def test___getitem__with_oem_props(self):
     target_info = common.BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS,
                                    self.TEST_OEM_DICTS)
     self.assertEqual('value1', target_info['property1'])
     self.assertEqual(4096, target_info['property2'])
-    self.assertRaises(KeyError,
-                      lambda: target_info['build.prop']['ro.build.foo'])
+    self.assertIsNone(target_info['build.prop'].GetProp('ro.build.foo'))
 
   def test___setitem__(self):
     target_info = common.BuildInfo(copy.deepcopy(self.TEST_INFO_DICT), None)
@@ -204,9 +221,11 @@
     target_info['property1'] = 'value2'
     self.assertEqual('value2', target_info['property1'])
 
-    self.assertEqual('build-foo', target_info['build.prop']['ro.build.foo'])
-    target_info['build.prop']['ro.build.foo'] = 'build-bar'
-    self.assertEqual('build-bar', target_info['build.prop']['ro.build.foo'])
+    self.assertEqual('build-foo',
+                     target_info['build.prop'].GetProp('ro.build.foo'))
+    target_info['build.prop'].build_props['ro.build.foo'] = 'build-bar'
+    self.assertEqual('build-bar',
+                     target_info['build.prop'].GetProp('ro.build.foo'))
 
   def test_get(self):
     target_info = common.BuildInfo(self.TEST_INFO_DICT, None)
@@ -214,7 +233,8 @@
     self.assertEqual(4096, target_info.get('property2'))
     self.assertEqual(4096, target_info.get('property2', 1024))
     self.assertEqual(1024, target_info.get('property-nonexistent', 1024))
-    self.assertEqual('build-foo', target_info.get('build.prop')['ro.build.foo'])
+    self.assertEqual('build-foo',
+                     target_info.get('build.prop').GetProp('ro.build.foo'))
 
   def test_get_with_oem_props(self):
     target_info = common.BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS,
@@ -223,9 +243,7 @@
     self.assertEqual(4096, target_info.get('property2'))
     self.assertEqual(4096, target_info.get('property2', 1024))
     self.assertEqual(1024, target_info.get('property-nonexistent', 1024))
-    self.assertIsNone(target_info.get('build.prop').get('ro.build.foo'))
-    self.assertRaises(KeyError,
-                      lambda: target_info.get('build.prop')['ro.build.foo'])
+    self.assertIsNone(target_info.get('build.prop').GetProp('ro.build.foo'))
 
   def test_items(self):
     target_info = common.BuildInfo(self.TEST_INFO_DICT, None)
@@ -262,7 +280,8 @@
 
   def test_GetPartitionFingerprint_uses_fingerprint_prop_if_available(self):
     info_dict = copy.deepcopy(self.TEST_INFO_DICT)
-    info_dict['vendor.build.prop']['ro.vendor.build.fingerprint'] = 'vendor:fingerprint'
+    info_dict['vendor.build.prop'].build_props[
+        'ro.vendor.build.fingerprint'] = 'vendor:fingerprint'
     target_info = common.BuildInfo(info_dict, None)
     self.assertEqual(
         target_info.GetPartitionFingerprint('vendor'),
@@ -303,14 +322,15 @@
 
   def test_ResolveRoProductProperty_FromSystem(self):
     info_dict = copy.deepcopy(self.TEST_INFO_DICT_PROPERTY_SOURCE_ORDER)
-    del info_dict['vendor.build.prop']['ro.product.vendor.device']
+    del info_dict['vendor.build.prop'].build_props['ro.product.vendor.device']
     info = common.BuildInfo(info_dict, None)
     self.assertEqual('system-product-device',
                      info.GetBuildProp('ro.product.device'))
 
   def test_ResolveRoProductProperty_InvalidPropertySearchOrder(self):
     info_dict = copy.deepcopy(self.TEST_INFO_DICT_PROPERTY_SOURCE_ORDER)
-    info_dict['build.prop']['ro.product.property_source_order'] = 'bad-source'
+    info_dict['build.prop'].build_props[
+        'ro.product.property_source_order'] = 'bad-source'
     with self.assertRaisesRegexp(common.ExternalError,
         'Invalid ro.product.property_source_order'):
       info = common.BuildInfo(info_dict, None)
@@ -1513,12 +1533,14 @@
     common.OPTIONS.info_dict = {
         'ab_update': 'true',
         'avb_avbtool': 'avbtool',
-        'build.prop': {
-            'ro.build.version.incremental': '6285659',
-            'ro.product.device': 'coral',
-            'ro.build.fingerprint': 'google/coral/coral:R/RP1A.200311.002/'
-                                    '6285659:userdebug/dev-keys'
-        }
+        'build.prop': common.PartitionBuildProps.FromDictionary(
+            'system', {
+                'ro.build.version.incremental': '6285659',
+                'ro.product.device': 'coral',
+                'ro.build.fingerprint':
+                'google/coral/coral:R/RP1A.200311.002/'
+                '6285659:userdebug/dev-keys'}
+        ),
     }
     common.OPTIONS.aftl_tool_path = 'aftltool'
     common.OPTIONS.aftl_server = 'log.endpoints.aftl-dev.cloud.goog:9000'
@@ -1551,12 +1573,14 @@
     common.OPTIONS.info_dict = {
         'ab_update': 'true',
         'avb_avbtool': 'avbtool',
-        'build.prop': {
-            'ro.build.version.incremental': '6285659',
-            'ro.product.device': 'coral',
-            'ro.build.fingerprint': 'google/coral/coral:R/RP1A.200311.002/'
-                                    '6285659:userdebug/dev-keys'
-        }
+        'build.prop': common.PartitionBuildProps.FromDictionary(
+            'system', {
+                'ro.build.version.incremental': '6285659',
+                'ro.product.device': 'coral',
+                'ro.build.fingerprint':
+                'google/coral/coral:R/RP1A.200311.002/'
+                '6285659:userdebug/dev-keys'}
+        )
     }
     common.OPTIONS.aftl_tool_path = "aftltool"
     common.OPTIONS.aftl_server = "log.endpoints.aftl-dev.cloud.goog:9000"
@@ -1871,3 +1895,47 @@
 
     lines = self.get_op_list(self.output_path)
     self.assertEqual(lines, ["remove foo"])
+
+
+class PartitionBuildPropsTest(test_utils.ReleaseToolsTestCase):
+  def setUp(self):
+    self.build_prop = [
+        'ro.odm.build.date.utc=1578430045',
+        'ro.odm.build.fingerprint='
+        'google/coral/coral:10/RP1A.200325.001/6337676:user/dev-keys',
+        'ro.product.odm.device=coral',
+        'import /odm/etc/build_${ro.boot.product.device_name}.prop',
+    ]
+
+  @staticmethod
+  def _BuildZipFile(entries):
+    input_file = common.MakeTempFile(prefix='target_files-', suffix='.zip')
+    with zipfile.ZipFile(input_file, 'w') as input_zip:
+      for name, content in entries.items():
+        input_zip.writestr(name, content)
+
+    return input_file
+
+  def test_parseBuildProps_noImportStatement(self):
+    build_prop = [
+      'ro.odm.build.date.utc=1578430045',
+      'ro.odm.build.fingerprint='
+      'google/coral/coral:10/RP1A.200325.001/6337676:user/dev-keys',
+      'ro.product.odm.device=coral',
+    ]
+    input_file = self._BuildZipFile({
+      'ODM/etc/build.prop': '\n'.join(build_prop),
+    })
+
+    with zipfile.ZipFile(input_file, 'r') as input_zip:
+      partition_props = common.PartitionBuildProps.FromInputFile(
+          input_zip, 'odm')
+
+    self.assertEqual({
+      'ro.odm.build.date.utc': '1578430045',
+      'ro.odm.build.fingerprint':
+      'google/coral/coral:10/RP1A.200325.001/6337676:user/dev-keys',
+      'ro.product.odm.device': 'coral',
+    }, partition_props.build_props)
+
+    self.assertEqual({}, partition_props.prop_overrides)
diff --git a/tools/releasetools/test_ota_from_target_files.py b/tools/releasetools/test_ota_from_target_files.py
index 38faf64..e007863 100644
--- a/tools/releasetools/test_ota_from_target_files.py
+++ b/tools/releasetools/test_ota_from_target_files.py
@@ -108,55 +108,58 @@
 
 
 class OtaFromTargetFilesTest(test_utils.ReleaseToolsTestCase):
-
   TEST_TARGET_INFO_DICT = {
-      'build.prop' : {
-          'ro.product.device' : 'product-device',
-          'ro.build.fingerprint' : 'build-fingerprint-target',
-          'ro.build.version.incremental' : 'build-version-incremental-target',
-          'ro.build.version.sdk' : '27',
-          'ro.build.version.security_patch' : '2017-12-01',
-          'ro.build.date.utc' : '1500000000',
-      },
+      'build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.product.device': 'product-device',
+              'ro.build.fingerprint': 'build-fingerprint-target',
+              'ro.build.version.incremental': 'build-version-incremental-target',
+              'ro.build.version.sdk': '27',
+              'ro.build.version.security_patch': '2017-12-01',
+              'ro.build.date.utc': '1500000000'}
+      )
   }
 
   TEST_SOURCE_INFO_DICT = {
-      'build.prop' : {
-          'ro.product.device' : 'product-device',
-          'ro.build.fingerprint' : 'build-fingerprint-source',
-          'ro.build.version.incremental' : 'build-version-incremental-source',
-          'ro.build.version.sdk' : '25',
-          'ro.build.version.security_patch' : '2016-12-01',
-          'ro.build.date.utc' : '1400000000',
-      },
+      'build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.product.device': 'product-device',
+              'ro.build.fingerprint': 'build-fingerprint-source',
+              'ro.build.version.incremental': 'build-version-incremental-source',
+              'ro.build.version.sdk': '25',
+              'ro.build.version.security_patch': '2016-12-01',
+              'ro.build.date.utc': '1400000000'}
+      )
   }
 
   TEST_INFO_DICT_USES_OEM_PROPS = {
-      'build.prop' : {
-          'ro.product.name' : 'product-name',
-          'ro.build.thumbprint' : 'build-thumbprint',
-          'ro.build.bar' : 'build-bar',
-      },
-      'vendor.build.prop' : {
-          'ro.vendor.build.fingerprint' : 'vendor-build-fingerprint',
-      },
-      'property1' : 'value1',
-      'property2' : 4096,
-      'oem_fingerprint_properties' : 'ro.product.device ro.product.brand',
+      'build.prop': common.PartitionBuildProps.FromDictionary(
+          'system', {
+              'ro.product.name': 'product-name',
+              'ro.build.thumbprint': 'build-thumbprint',
+              'ro.build.bar': 'build-bar'}
+      ),
+      'vendor.build.prop': common.PartitionBuildProps.FromDictionary(
+          'vendor', {
+               'ro.vendor.build.fingerprint': 'vendor-build-fingerprint'}
+      ),
+      'property1': 'value1',
+      'property2': 4096,
+      'oem_fingerprint_properties': 'ro.product.device ro.product.brand',
   }
 
   TEST_OEM_DICTS = [
       {
-          'ro.product.brand' : 'brand1',
-          'ro.product.device' : 'device1',
+          'ro.product.brand': 'brand1',
+          'ro.product.device': 'device1',
       },
       {
-          'ro.product.brand' : 'brand2',
-          'ro.product.device' : 'device2',
+          'ro.product.brand': 'brand2',
+          'ro.product.device': 'device2',
       },
       {
-          'ro.product.brand' : 'brand3',
-          'ro.product.device' : 'device3',
+          'ro.product.brand': 'brand3',
+          'ro.product.device': 'device3',
       },
   ]
 
@@ -288,10 +291,10 @@
 
   @staticmethod
   def _test_GetPackageMetadata_swapBuildTimestamps(target_info, source_info):
-    (target_info['build.prop']['ro.build.date.utc'],
-     source_info['build.prop']['ro.build.date.utc']) = (
-         source_info['build.prop']['ro.build.date.utc'],
-         target_info['build.prop']['ro.build.date.utc'])
+    (target_info['build.prop'].build_props['ro.build.date.utc'],
+     source_info['build.prop'].build_props['ro.build.date.utc']) = (
+         source_info['build.prop'].build_props['ro.build.date.utc'],
+         target_info['build.prop'].build_props['ro.build.date.utc'])
 
   def test_GetPackageMetadata_unintentionalDowngradeDetected(self):
     target_info_dict = copy.deepcopy(self.TEST_TARGET_INFO_DICT)
@@ -528,7 +531,7 @@
   def test_WriteFingerprintAssertion_without_oem_props(self):
     target_info = common.BuildInfo(self.TEST_TARGET_INFO_DICT, None)
     source_info_dict = copy.deepcopy(self.TEST_TARGET_INFO_DICT)
-    source_info_dict['build.prop']['ro.build.fingerprint'] = (
+    source_info_dict['build.prop'].build_props['ro.build.fingerprint'] = (
         'source-build-fingerprint')
     source_info = common.BuildInfo(source_info_dict, None)
 
@@ -567,7 +570,7 @@
     target_info = common.BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS,
                                    self.TEST_OEM_DICTS)
     source_info_dict = copy.deepcopy(self.TEST_INFO_DICT_USES_OEM_PROPS)
-    source_info_dict['build.prop']['ro.build.thumbprint'] = (
+    source_info_dict['build.prop'].build_props['ro.build.thumbprint'] = (
         'source-build-thumbprint')
     source_info = common.BuildInfo(source_info_dict, self.TEST_OEM_DICTS)