Merge "Remove mentions of deleted script construct_context.sh in comments."
diff --git a/Changes.md b/Changes.md
index 453ea6c..61e6bb6 100644
--- a/Changes.md
+++ b/Changes.md
@@ -1,5 +1,92 @@
 # Build System Changes for Android.mk Writers
 
+## Changes in system properties settings
+
+### Product variables
+
+System properties for each of the partition is supposed to be set via following
+product config variables.
+
+For system partititon,
+
+* `PRODUCT_SYSTEM_PROPERITES`
+* `PRODUCT_SYSTEM_DEFAULT_PROPERTIES` is highly discouraged. Will be deprecated.
+
+For vendor partition,
+
+* `PRODUCT_VENDOR_PROPERTIES`
+* `PRODUCT_PROPERTY_OVERRIDES` is highly discouraged. Will be deprecated.
+* `PRODUCT_DEFAULT_PROPERTY_OVERRIDES` is also discouraged. Will be deprecated.
+
+For odm partition,
+
+* `PRODUCT_ODM_PROPERTIES`
+
+For system_ext partition,
+
+* `PRODUCT_SYSTEM_EXT_PROPERTIES`
+
+For product partition,
+
+* `PRODUCT_PRODUCT_PROPERTIES`
+
+### Duplication is not allowed within a partition
+
+For each partition, having multiple sysprop assignments for the same name is
+prohibited. For example, the following will now trigger an error:
+
+`PRODUCT_VENDOR_PROPERTIES += foo=true foo=false`
+
+Having duplication across partitions are still allowed. So, the following is
+not an error:
+
+`PRODUCT_VENDOR_PROPERTIES += foo=true`
+`PRODUCT_SYSTEM_PROPERTIES += foo=false`
+
+In that case, the final value is determined at runtime. The precedence is
+
+* product
+* odm
+* vendor
+* system_ext
+* system
+
+So, `foo` becomes `true` because vendor has higher priority than system.
+
+To temporarily turn the build-time restriction off, use
+
+`BUILD_BROKEN_DUP_SYSPROP := true`
+
+### Optional assignments
+
+System properties can now be set as optional using the new syntax:
+
+`name ?= value`
+
+Then the system property named `name` gets the value `value` only when there
+is no other non-optional assignments having the same name. For example, the
+following is allowed and `foo` gets `true`
+
+`PRODUCT_VENDOR_PROPERTIES += foo=true foo?=false`
+
+Note that the order between the optional and the non-optional assignments
+doesn't matter. The following gives the same result as above.
+
+`PRODUCT_VENDOR_PROPERTIES += foo?=false foo=true`
+
+Optional assignments can be duplicated and in that case their order matters.
+Specifically, the last one eclipses others.
+
+`PRODUCT_VENDOR_PROPERTIES += foo?=apple foo?=banana foo?=mango`
+
+With above, `foo` becomes `mango` since its the last one.
+
+Note that this behavior is different from the previous behavior of preferring
+the first one. To go back to the original behavior for compatability reason,
+use:
+
+`BUILD_BROKEN_DUP_SYSPROP := true`
+
 ## ELF prebuilts in PRODUCT_COPY_FILES
 
 ELF prebuilts in PRODUCT_COPY_FILES that are installed into these paths are an
diff --git a/core/Makefile b/core/Makefile
index 11d2796..2f5b621 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -407,6 +407,16 @@
 
 endef
 
+# -----------------------------------------------------------------
+# Merge an individual apkcerts output into the final apkcerts.txt output.
+# Use a macro to make it compatible with _apkcerts_write_line
+# $1 apkcerts file to be merged
+# $2 output file
+define _apkcerts_merge
+$(hide) cat $1 >> $2
+
+endef
+
 name := $(TARGET_PRODUCT)
 ifeq ($(TARGET_BUILD_TYPE),debug)
   name := $(name)_debug
@@ -415,6 +425,8 @@
 intermediates := \
 	$(call intermediates-dir-for,PACKAGING,apkcerts)
 APKCERTS_FILE := $(intermediates)/$(name).txt
+all_apkcerts_files := $(sort $(foreach p,$(PACKAGES),$(PACKAGES.$(p).APKCERTS_FILE)))
+$(APKCERTS_FILE): $(all_apkcerts_files)
 # We don't need to really build all the modules.
 # TODO: rebuild APKCERTS_FILE if any app change its cert.
 $(APKCERTS_FILE):
@@ -422,9 +434,11 @@
 	@mkdir -p $(dir $@)
 	@rm -f $@
 	$(foreach p,$(sort $(PACKAGES)),\
-	  $(if $(PACKAGES.$(p).EXTERNAL_KEY),\
-	    $(call _apkcerts_write_line,$(PACKAGES.$(p).STEM),"EXTERNAL","",$(PACKAGES.$(p).COMPRESSED),$(PACKAGES.$(p).PARTITION),$@),\
-	    $(call _apkcerts_write_line,$(PACKAGES.$(p).STEM),$(PACKAGES.$(p).CERTIFICATE),$(PACKAGES.$(p).PRIVATE_KEY),$(PACKAGES.$(p).COMPRESSED),$(PACKAGES.$(p).PARTITION),$@)))
+	  $(if $(PACKAGES.$(p).APKCERTS_FILE),\
+	    $(call _apkcerts_merge,$(PACKAGES.$(p).APKCERTS_FILE), $@),\
+	    $(if $(PACKAGES.$(p).EXTERNAL_KEY),\
+	      $(call _apkcerts_write_line,$(PACKAGES.$(p).STEM),"EXTERNAL","",$(PACKAGES.$(p).COMPRESSED),$(PACKAGES.$(p).PARTITION),$@),\
+	      $(call _apkcerts_write_line,$(PACKAGES.$(p).STEM),$(PACKAGES.$(p).CERTIFICATE),$(PACKAGES.$(p).PRIVATE_KEY),$(PACKAGES.$(p).COMPRESSED),$(PACKAGES.$(p).PARTITION),$@))))
 	# In case value of PACKAGES is empty.
 	$(hide) touch $@
 
@@ -3445,8 +3459,8 @@
 $(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) \
+$(BUILT_KERNEL_CONFIGS_FILE): $(EXTRACT_KERNEL) $(firstword $(INSTALLED_KERNEL_TARGET))
+	$< --tools $(PRIVATE_DECOMPRESS_TOOLS) --input $(firstword $(INSTALLED_KERNEL_TARGET)) \
 	  --output-configs $@ \
 	  --output-version $(BUILT_KERNEL_VERSION_FILE)
 
@@ -4051,13 +4065,13 @@
   $(foreach device,$(BOARD_SUPER_PARTITION_BLOCK_DEVICES), \
     echo "super_$(device)_device_size=$(BOARD_SUPER_PARTITION_$(call to-upper,$(device))_DEVICE_SIZE)" >> $(1);)
   $(if $(BOARD_SUPER_PARTITION_PARTITION_LIST), \
-    echo "dynamic_partition_list=$(call filter-out-missing-vendor, $(BOARD_SUPER_PARTITION_PARTITION_LIST))" >> $(1))
+    echo "dynamic_partition_list=$(call filter-out-missing-vendor,$(BOARD_SUPER_PARTITION_PARTITION_LIST))" >> $(1))
   $(if $(BOARD_SUPER_PARTITION_GROUPS),
     echo "super_partition_groups=$(BOARD_SUPER_PARTITION_GROUPS)" >> $(1))
   $(foreach group,$(BOARD_SUPER_PARTITION_GROUPS), \
     echo "super_$(group)_group_size=$(BOARD_$(call to-upper,$(group))_SIZE)" >> $(1); \
     $(if $(BOARD_$(call to-upper,$(group))_PARTITION_LIST), \
-      echo "super_$(group)_partition_list=$(call filter-out-missing-vendor, $(BOARD_$(call to-upper,$(group))_PARTITION_LIST))" >> $(1);))
+      echo "super_$(group)_partition_list=$(call filter-out-missing-vendor,$(BOARD_$(call to-upper,$(group))_PARTITION_LIST))" >> $(1);))
   $(if $(filter true,$(TARGET_USERIMAGES_SPARSE_EXT_DISABLED)), \
     echo "build_non_sparse_super_partition=true" >> $(1))
   $(if $(filter true,$(TARGET_USERIMAGES_SPARSE_F2FS_DISABLED)), \
diff --git a/core/binary.mk b/core/binary.mk
index 200724a..a70a047 100644
--- a/core/binary.mk
+++ b/core/binary.mk
@@ -286,8 +286,7 @@
 endif
 
 ifneq ($(LOCAL_SDK_VERSION),)
-  my_all_ndk_libraries := \
-      $(NDK_MIGRATED_LIBS) $(addprefix lib,$(NDK_PREBUILT_SHARED_LIBRARIES))
+  my_all_ndk_libraries := $(NDK_KNOWN_LIBS)
   my_ndk_shared_libraries := \
       $(filter $(my_all_ndk_libraries),\
         $(my_shared_libraries) $(my_system_shared_libraries))
@@ -1354,7 +1353,7 @@
 # lists and use addprefix.
 my_ndk_shared_libraries_fullpath := \
     $(foreach _lib,$(my_ndk_shared_libraries),\
-        $(if $(filter $(NDK_MIGRATED_LIBS),$(_lib)),\
+        $(if $(filter $(NDK_KNOWN_LIBS),$(_lib)),\
             $(my_built_ndk_libs)/$(_lib)$(so_suffix),\
             $(my_ndk_sysroot_lib)/$(_lib)$(so_suffix)))
 
@@ -1555,7 +1554,7 @@
 my_allowed_ldlibs :=
 ifndef LOCAL_IS_HOST_MODULE
   ifneq ($(LOCAL_SDK_VERSION),)
-    my_allowed_ldlibs := $(addprefix -l,$(NDK_PREBUILT_SHARED_LIBRARIES))
+    my_allowed_ldlibs := $(NDK_KNOWN_LIBS:lib%=-l%)
   endif
 else
   my_allowed_ldlibs := $($(my_prefix)AVAILABLE_LIBRARIES)
diff --git a/core/board_config.mk b/core/board_config.mk
index ae1614f..a6e586d 100644
--- a/core/board_config.mk
+++ b/core/board_config.mk
@@ -93,6 +93,7 @@
   BUILD_BROKEN_TREBLE_SYSPROP_NEVERALLOW \
   BUILD_BROKEN_USES_NETWORK \
   BUILD_BROKEN_VINTF_PRODUCT_COPY_FILES \
+  BUILD_BROKEN_DUP_SYSPROP \
 
 _build_broken_var_list += \
   $(foreach m,$(AVAILABLE_BUILD_MODULE_TYPES) \
@@ -593,6 +594,9 @@
 endef
 
 ifdef BOARD_VNDK_VERSION
+  ifeq ($(BOARD_VNDK_VERSION),$(PLATFORM_VNDK_VERSION))
+    $(error BOARD_VNDK_VERSION is equal to PLATFORM_VNDK_VERSION; use BOARD_VNDK_VERSION := current))
+  endif
   ifneq ($(BOARD_VNDK_VERSION),current)
     $(call check_vndk_version,$(BOARD_VNDK_VERSION))
   endif
diff --git a/core/cc_prebuilt_internal.mk b/core/cc_prebuilt_internal.mk
index 99b7d0f..e8e01d8 100644
--- a/core/cc_prebuilt_internal.mk
+++ b/core/cc_prebuilt_internal.mk
@@ -65,7 +65,7 @@
   built_module := $(linked_module)
 
   ifneq ($(LOCAL_SDK_VERSION),)
-    # binary.mk filters out NDK_MIGRATED_LIBS from my_shared_libs, thus those NDK libs are not added
+    # binary.mk filters out NDK_KNOWN_LIBS from my_shared_libs, thus those NDK libs are not added
     # to DEPENDENCIES_ON_SHARED_LIBRARIES. Assign $(my_ndk_shared_libraries_fullpath) to
     # my_check_elf_file_shared_lib_files so that check_elf_file.py can see those NDK stub libs.
     my_check_elf_file_shared_lib_files := $(my_ndk_shared_libraries_fullpath)
diff --git a/core/clear_vars.mk b/core/clear_vars.mk
index c88a1cd..20319a8 100644
--- a/core/clear_vars.mk
+++ b/core/clear_vars.mk
@@ -22,6 +22,7 @@
 LOCAL_APIDIFF_OLDAPI:=
 LOCAL_APK_LIBRARIES:=
 LOCAL_APK_SET_MASTER_FILE:=
+LOCAL_APKCERTS_FILE:=
 LOCAL_ARM_MODE:=
 LOCAL_ASFLAGS:=
 LOCAL_ASSET_DIR:=
diff --git a/core/config.mk b/core/config.mk
index bb182aa..a5b8ef7 100644
--- a/core/config.mk
+++ b/core/config.mk
@@ -978,8 +978,7 @@
     BOARD_SUPER_PARTITION_GROUPS and BOARD_*_PARTITION_LIST)
 endif
 BOARD_SUPER_PARTITION_PARTITION_LIST := \
-    $(foreach group,$(call to-upper,$(BOARD_SUPER_PARTITION_GROUPS)), \
-        $(BOARD_$(group)_PARTITION_LIST))
+    $(foreach group,$(call to-upper,$(BOARD_SUPER_PARTITION_GROUPS)),$(BOARD_$(group)_PARTITION_LIST))
 .KATI_READONLY := BOARD_SUPER_PARTITION_PARTITION_LIST
 
 ifneq ($(BOARD_SUPER_PARTITION_SIZE),)
diff --git a/core/java.mk b/core/java.mk
index 9d42775..2f18ad9 100644
--- a/core/java.mk
+++ b/core/java.mk
@@ -393,7 +393,7 @@
 else
   # For platform build, we can't just raise to the "current" SDK,
   # that would break apps that use APIs removed from the current SDK.
-  my_proguard_sdk_raise := $(call java-lib-header-files,$(TARGET_DEFAULT_BOOTCLASSPATH_LIBRARIES) $(TARGET_DEFAULT_JAVA_LIBRARIES))
+  my_proguard_sdk_raise := $(call java-lib-header-files,$(LEGACY_CORE_PLATFORM_BOOTCLASSPATH_LIBRARIES) $(FRAMEWORK_LIBRARIES))
 endif
 ifdef BOARD_SYSTEMSDK_VERSIONS
 ifneq (,$(filter true,$(LOCAL_VENDOR_MODULE) $(LOCAL_ODM_MODULE) $(LOCAL_PROPRIETARY_MODULE)))
diff --git a/core/java_common.mk b/core/java_common.mk
index b218c0d..658296d 100644
--- a/core/java_common.mk
+++ b/core/java_common.mk
@@ -265,11 +265,11 @@
       # Most users of LOCAL_NO_STANDARD_LIBRARIES really mean no framework libs,
       # and manually add back the core libs.  The ones that don't are in soong
       # now, so just always assume that they want the default system modules
-      my_system_modules := $(DEFAULT_SYSTEM_MODULES)
+      my_system_modules := $(LEGACY_CORE_PLATFORM_SYSTEM_MODULES)
     else  # LOCAL_NO_STANDARD_LIBRARIES
-      full_java_bootclasspath_libs := $(call java-lib-header-files,$(TARGET_DEFAULT_BOOTCLASSPATH_LIBRARIES) $(TARGET_DEFAULT_JAVA_LIBRARIES))
-      LOCAL_JAVA_LIBRARIES := $(filter-out $(TARGET_DEFAULT_BOOTCLASSPATH_LIBRARIES) $(TARGET_DEFAULT_JAVA_LIBRARIES),$(LOCAL_JAVA_LIBRARIES))
-      my_system_modules := $(DEFAULT_SYSTEM_MODULES)
+      full_java_bootclasspath_libs := $(call java-lib-header-files,$(LEGACY_CORE_PLATFORM_BOOTCLASSPATH_LIBRARIES) $(FRAMEWORK_LIBRARIES))
+      LOCAL_JAVA_LIBRARIES := $(filter-out $(LEGACY_CORE_PLATFORM_BOOTCLASSPATH_LIBRARIES) $(FRAMEWORK_LIBRARIES),$(LOCAL_JAVA_LIBRARIES))
+      my_system_modules := $(LEGACY_CORE_PLATFORM_SYSTEM_MODULES)
     endif  # LOCAL_NO_STANDARD_LIBRARIES
 
     ifneq (,$(TARGET_BUILD_APPS_USE_PREBUILT_SDK))
@@ -352,10 +352,10 @@
     ifeq ($(LOCAL_NO_STANDARD_LIBRARIES),true)
       empty_bootclasspath := ""
     else
-      full_java_bootclasspath_libs := $(call java-lib-header-files,$(addsuffix -hostdex,$(TARGET_DEFAULT_BOOTCLASSPATH_LIBRARIES)),true)
+      full_java_bootclasspath_libs := $(call java-lib-header-files,$(addsuffix -hostdex,$(LEGACY_CORE_PLATFORM_BOOTCLASSPATH_LIBRARIES)),true)
     endif
 
-    my_system_modules := $(DEFAULT_SYSTEM_MODULES)
+    my_system_modules := $(LEGACY_CORE_PLATFORM_SYSTEM_MODULES)
     full_shared_java_libs := $(call java-lib-files,$(LOCAL_JAVA_LIBRARIES),true)
     full_shared_java_header_libs := $(call java-lib-header-files,$(LOCAL_JAVA_LIBRARIES),true)
   else # !USE_CORE_LIB_BOOTCLASSPATH
diff --git a/core/main.mk b/core/main.mk
index 2af1f92..cc7cf72 100644
--- a/core/main.mk
+++ b/core/main.mk
@@ -642,9 +642,11 @@
 endef
 
 # TODO(b/7456955): error if a required module doesn't exist.
-# Resolve the required module names in ALL_MODULES.*.REQUIRED_FROM_TARGET,
-# ALL_MODULES.*.REQUIRED_FROM_HOST and ALL_MODULES.*.REQUIRED_FROM_HOST_CROSS
-# to 32-bit or 64-bit variant.
+# Resolve the required module names to 32-bit or 64-bit variant for:
+#   ALL_MODULES.<*>.REQUIRED_FROM_TARGET
+#   ALL_MODULES.<*>.REQUIRED_FROM_HOST
+#   ALL_MODULES.<*>.REQUIRED_FROM_HOST_CROSS
+#
 # If a module is for cross host OS, the required modules are also for that OS.
 # Required modules explicitly suffixed with :32 or :64 resolve to that bitness.
 # Otherwise if the requiring module is native and the required module is shared
@@ -660,27 +662,53 @@
       $(eval r := $(addprefix host_cross_,$(r)))) \
     $(eval module_is_native := \
       $(filter EXECUTABLES SHARED_LIBRARIES NATIVE_TESTS,$(ALL_MODULES.$(m).CLASS))) \
-    $(eval r_r := $(foreach r_i,$(r), \
-      $(if $(filter %:32 %:64,$(r_i)), \
-        $(eval r_m := $(call resolve-bitness-for-modules,$(1),$(r_i))), \
-        $(eval r_m := \
-          $(eval r_i_2nd := $(call get-modules-for-2nd-arch,$(1),$(r_i))) \
-          $(eval required_is_shared_library_or_native_test := \
-            $(filter SHARED_LIBRARIES NATIVE_TESTS, \
-              $(ALL_MODULES.$(r_i).CLASS) $(ALL_MODULES.$(r_i_2nd).CLASS))) \
-          $(if $(and $(module_is_native),$(required_is_shared_library_or_native_test)), \
-            $(if $(ALL_MODULES.$(m).FOR_2ND_ARCH),$(r_i_2nd),$(r_i)), \
-            $(r_i) $(r_i_2nd)))) \
-      $(eval ### TODO(b/7456955): error if r_m is empty / does not exist) \
-      $(r_m))) \
+    $(eval r_r := \
+      $(foreach r_i,$(r), \
+        $(if $(filter %:32 %:64,$(r_i)), \
+          $(eval r_m := $(call resolve-bitness-for-modules,$(1),$(r_i))), \
+          $(eval r_m := \
+            $(eval r_i_2nd := $(call get-modules-for-2nd-arch,$(1),$(r_i))) \
+            $(eval required_is_shared_library_or_native_test := \
+              $(filter SHARED_LIBRARIES NATIVE_TESTS, \
+                $(ALL_MODULES.$(r_i).CLASS) $(ALL_MODULES.$(r_i_2nd).CLASS))) \
+            $(if $(and $(module_is_native),$(required_is_shared_library_or_native_test)), \
+              $(if $(ALL_MODULES.$(m).FOR_2ND_ARCH),$(r_i_2nd),$(r_i)), \
+              $(r_i) $(r_i_2nd)))) \
+        $(eval ### TODO(b/7456955): error if r_m is empty / does not exist) \
+        $(r_m))) \
     $(eval ALL_MODULES.$(m).REQUIRED_FROM_$(1) := $(sort $(r_r))) \
   ) \
 )
 endef
 
+# Resolve the required module names to 32-bit or 64-bit variant for:
+#   ALL_MODULES.<*>.TARGET_REQUIRED_FROM_HOST
+#   ALL_MODULES.<*>.HOST_REQUIRED_FROM_TARGET
+#
+# This is like select-bitness-of-required-modules, but it doesn't have
+# complicated logic for various module types.
+# Calls resolve-bitness-for-modules to resolve module names.
+# $(1): TARGET or HOST
+# $(2): HOST or TARGET
+define select-bitness-of-target-host-required-modules
+$(foreach m,$(ALL_MODULES), \
+  $(eval r := $(ALL_MODULES.$(m).$(1)_REQUIRED_FROM_$(2))) \
+  $(if $(r), \
+    $(eval r_r := \
+      $(foreach r_i,$(r), \
+        $(eval r_m := $(call resolve-bitness-for-modules,$(1),$(r_i))) \
+        $(eval ### TODO(b/7456955): error if r_m is empty / does not exist) \
+        $(r_m))) \
+    $(eval ALL_MODULES.$(m).$(1)_REQUIRED_FROM_$(2) := $(sort $(r_r))) \
+  ) \
+)
+endef
+
 $(call select-bitness-of-required-modules,TARGET)
 $(call select-bitness-of-required-modules,HOST)
 $(call select-bitness-of-required-modules,HOST_CROSS)
+$(call select-bitness-of-target-host-required-modules,TARGET,HOST)
+$(call select-bitness-of-target-host-required-modules,HOST,TARGET)
 
 define add-required-deps
 $(1): | $(2)
diff --git a/core/soong_android_app_set.mk b/core/soong_android_app_set.mk
index 5ed9b2c..c884894 100644
--- a/core/soong_android_app_set.mk
+++ b/core/soong_android_app_set.mk
@@ -31,4 +31,9 @@
 $(LOCAL_INSTALLED_MODULE): PRIVATE_POST_INSTALL_CMD := $(LOCAL_POST_INSTALL_CMD)
 PACKAGES.$(LOCAL_MODULE).OVERRIDES := $(strip $(LOCAL_OVERRIDES_PACKAGES))
 
+PACKAGES := $(PACKAGES) $(LOCAL_MODULE)
+# We can't know exactly what apk files would be outputted yet.
+# Let extract_apks generate apkcerts.txt and merge it later.
+PACKAGES.$(LOCAL_MODULE).APKCERTS_FILE := $(LOCAL_APKCERTS_FILE)
+
 SOONG_ALREADY_CONV += $(LOCAL_MODULE)
diff --git a/core/sysprop.mk b/core/sysprop.mk
index 0cee81c..f1311ed 100644
--- a/core/sysprop.mk
+++ b/core/sysprop.mk
@@ -71,10 +71,22 @@
 define build-properties
 ALL_DEFAULT_INSTALLED_MODULES += $(2)
 
-# TODO(b/117892318): eliminate the call to uniq-pairs-by-first-component when
-# it is guaranteed that there is no dup.
+$(eval # Properties can be assigned using `prop ?= value` or `prop = value` syntax.)
+$(eval # Eliminate spaces around the ?= and = separators.)
 $(foreach name,$(strip $(4)),\
-    $(eval _resolved_$(name) := $$(call collapse-pairs, $$(call uniq-pairs-by-first-component,$$($(name)),=)))\
+    $(eval _temp := $$(call collapse-pairs,$$($(name)),?=))\
+    $(eval _resolved_$(name) := $$(call collapse-pairs,$$(_temp),=))\
+)
+
+$(eval # Implement the legacy behavior when BUILD_BROKEN_DUP_SYSPROP is on.)
+$(eval # Optional assignments are all converted to normal assignments and)
+$(eval # when their duplicates the first one wins)
+$(if $(filter true,$(BUILD_BROKEN_DUP_SYSPROP)),\
+    $(foreach name,$(strip $(4)),\
+        $(eval _temp := $$(subst ?=,=,$$(_resolved_$(name))))\
+        $(eval _resolved_$(name) := $$(call uniq-pairs-by-first-component,$$(_resolved_$(name)),=))\
+    )\
+    $(eval _option := --allow-dup)\
 )
 
 $(2): $(POST_PROCESS_PROPS) $(INTERNAL_BUILD_ID_MAKEFILE) $(API_FINGERPRINT) $(3)
@@ -99,7 +111,7 @@
 	        echo "$$(line)" >> $$@;\
 	    )\
 	)
-	$(hide) $(POST_PROCESS_PROPS) $$@ $(5)
+	$(hide) $(POST_PROCESS_PROPS) $$(_option) $$@ $(5)
 	$(hide) echo "# end of file" >> $$@
 endef
 
@@ -361,7 +373,7 @@
     $(empty)))
 
 # ----------------------------------------------------------------
-# odm/build.prop
+# odm/etc/build.prop
 #
 _prop_files_ := $(if $(TARGET_ODM_PROP),\
     $(TARGET_ODM_PROP),\
@@ -373,7 +385,9 @@
     ADDITIONAL_ODM_PROPERTIES \
     PRODUCT_ODM_PROPERTIES
 
-INSTALLED_ODM_BUILD_PROP_TARGET := $(TARGET_OUT_ODM)/build.prop
+# Note the 'etc' sub directory. For the reason, see
+# I0733c277baa67c549bb45599abb70aba13fbdbcf
+INSTALLED_ODM_BUILD_PROP_TARGET := $(TARGET_OUT_ODM)/etc/build.prop
 $(eval $(call build-properties,\
     odm,\
     $(INSTALLED_ODM_BUILD_PROP_TARGET),\
diff --git a/rbesetup.sh b/rbesetup.sh
index 7e9b2ea..145e1e8 100644
--- a/rbesetup.sh
+++ b/rbesetup.sh
@@ -4,7 +4,7 @@
 # for the build to be executed with RBE.
 function use_rbe() {
   local RBE_LOG_DIR="/tmp"
-  local RBE_BINARIES_DIR="prebuilts/remoteexecution-client/latest/"
+  local RBE_BINARIES_DIR="prebuilts/remoteexecution-client/latest"
   local DOCKER_IMAGE="gcr.io/androidbuild-re-dockerimage/android-build-remoteexec-image@sha256:582efb38f0c229ea39952fff9e132ccbe183e14869b39888010dacf56b360d62"
 
   # Do not set an invocation-ID and let reproxy auto-generate one.
diff --git a/target/board/emulator_arm64/device.mk b/target/board/emulator_arm64/device.mk
index 57675d0..73dc2f4 100644
--- a/target/board/emulator_arm64/device.mk
+++ b/target/board/emulator_arm64/device.mk
@@ -26,7 +26,3 @@
 
 PRODUCT_COPY_FILES += \
     $(LOCAL_KERNEL):kernel
-
-# Adjust the Dalvik heap to be appropriate for a tablet.
-$(call inherit-product-if-exists, frameworks/base/build/tablet-dalvik-heap.mk)
-$(call inherit-product-if-exists, frameworks/native/build/tablet-dalvik-heap.mk)
diff --git a/target/board/generic_arm64/device.mk b/target/board/generic_arm64/device.mk
index 3b7cd44..b34004f 100644
--- a/target/board/generic_arm64/device.mk
+++ b/target/board/generic_arm64/device.mk
@@ -18,7 +18,3 @@
     device/google/cuttlefish_kernel/5.4-arm64/kernel-5.4:kernel-5.4 \
     device/google/cuttlefish_kernel/5.4-arm64/kernel-5.4-gz:kernel-5.4-gz \
     device/google/cuttlefish_kernel/5.4-arm64/kernel-5.4-lz4:kernel-5.4-lz4
-
-# Adjust the Dalvik heap to be appropriate for a tablet.
-$(call inherit-product-if-exists, frameworks/base/build/tablet-dalvik-heap.mk)
-$(call inherit-product-if-exists, frameworks/native/build/tablet-dalvik-heap.mk)
diff --git a/target/product/aosp_product.mk b/target/product/aosp_product.mk
index f22c3a3..a3da1c9 100644
--- a/target/product/aosp_product.mk
+++ b/target/product/aosp_product.mk
@@ -23,9 +23,9 @@
 
 # Additional settings used in all AOSP builds
 PRODUCT_PRODUCT_PROPERTIES += \
-    ro.config.ringtone=Ring_Synth_04.ogg \
-    ro.config.notification_sound=pixiedust.ogg \
-    ro.com.android.dataroaming=true \
+    ro.config.ringtone?=Ring_Synth_04.ogg \
+    ro.config.notification_sound?=pixiedust.ogg \
+    ro.com.android.dataroaming?=true \
 
 # More AOSP packages
 PRODUCT_PACKAGES += \
diff --git a/target/product/base_system.mk b/target/product/base_system.mk
index bec550b..f6770fb 100644
--- a/target/product/base_system.mk
+++ b/target/product/base_system.mk
@@ -348,7 +348,7 @@
 endif
 
 PRODUCT_COPY_FILES += system/core/rootdir/init.zygote32.rc:system/etc/init/hw/init.zygote32.rc
-PRODUCT_SYSTEM_PROPERTIES += ro.zygote=zygote32
+PRODUCT_SYSTEM_PROPERTIES += ro.zygote?=zygote32
 
 PRODUCT_SYSTEM_PROPERTIES += debug.atrace.tags.enableflags=0
 
diff --git a/target/product/full_base.mk b/target/product/full_base.mk
index dfb2204..64f61ff 100644
--- a/target/product/full_base.mk
+++ b/target/product/full_base.mk
@@ -43,8 +43,8 @@
 
 # Additional settings used in all AOSP builds
 PRODUCT_VENDOR_PROPERTIES := \
-    ro.config.ringtone=Ring_Synth_04.ogg \
-    ro.config.notification_sound=pixiedust.ogg
+    ro.config.ringtone?=Ring_Synth_04.ogg \
+    ro.config.notification_sound?=pixiedust.ogg
 
 # Put en_US first in the list, so make it default.
 PRODUCT_LOCALES := en_US
diff --git a/target/product/full_base_telephony.mk b/target/product/full_base_telephony.mk
index 5e18c05..d8a54cd 100644
--- a/target/product/full_base_telephony.mk
+++ b/target/product/full_base_telephony.mk
@@ -20,8 +20,8 @@
 # entirely appropriate to inherit from for on-device configurations.
 
 PRODUCT_VENDOR_PROPERTIES := \
-    keyguard.no_require_sim=true \
-    ro.com.android.dataroaming=true
+    keyguard.no_require_sim?=true \
+    ro.com.android.dataroaming?=true
 
 PRODUCT_COPY_FILES := \
     device/sample/etc/apns-full-conf.xml:system/etc/apns-conf.xml \
diff --git a/target/product/handheld_system.mk b/target/product/handheld_system.mk
index 22c817e..e2c91b6 100644
--- a/target/product/handheld_system.mk
+++ b/target/product/handheld_system.mk
@@ -84,6 +84,6 @@
     frameworks/av/media/libeffects/data/audio_effects.conf:system/etc/audio_effects.conf
 
 PRODUCT_VENDOR_PROPERTIES += \
-    ro.carrier=unknown \
-    ro.config.notification_sound=OnTheHunt.ogg \
-    ro.config.alarm_alert=Alarm_Classic.ogg
+    ro.carrier?=unknown \
+    ro.config.notification_sound?=OnTheHunt.ogg \
+    ro.config.alarm_alert?=Alarm_Classic.ogg
diff --git a/target/product/media_system.mk b/target/product/media_system.mk
index a3fafb3..7a2dd73 100644
--- a/target/product/media_system.mk
+++ b/target/product/media_system.mk
@@ -74,7 +74,7 @@
 # On userdebug builds, collect more tombstones by default.
 ifneq (,$(filter userdebug eng,$(TARGET_BUILD_VARIANT)))
 PRODUCT_VENDOR_PROPERTIES += \
-    tombstoned.max_tombstone_count=50
+    tombstoned.max_tombstone_count?=50
 endif
 
 PRODUCT_VENDOR_PROPERTIES += \
diff --git a/target/product/runtime_libart.mk b/target/product/runtime_libart.mk
index 78a4af0..b96601d 100644
--- a/target/product/runtime_libart.mk
+++ b/target/product/runtime_libart.mk
@@ -57,23 +57,23 @@
 # On eng builds, make "boot" reasons only extract for faster turnaround.
 ifeq (eng,$(TARGET_BUILD_VARIANT))
     PRODUCT_SYSTEM_PROPERTIES += \
-        pm.dexopt.first-boot=extract \
-        pm.dexopt.boot=extract
+        pm.dexopt.first-boot?=extract \
+        pm.dexopt.boot?=extract
 else
     PRODUCT_SYSTEM_PROPERTIES += \
-        pm.dexopt.first-boot=quicken \
-        pm.dexopt.boot=verify
+        pm.dexopt.first-boot?=quicken \
+        pm.dexopt.boot?=verify
 endif
 
 # The install filter is speed-profile in order to enable the use of
 # profiles from the dex metadata files. Note that if a profile is not provided
 # or if it is empty speed-profile is equivalent to (quicken + empty app image).
 PRODUCT_SYSTEM_PROPERTIES += \
-    pm.dexopt.install=speed-profile \
-    pm.dexopt.bg-dexopt=speed-profile \
-    pm.dexopt.ab-ota=speed-profile \
-    pm.dexopt.inactive=verify \
-    pm.dexopt.shared=speed
+    pm.dexopt.install?=speed-profile \
+    pm.dexopt.bg-dexopt?=speed-profile \
+    pm.dexopt.ab-ota?=speed-profile \
+    pm.dexopt.inactive?=verify \
+    pm.dexopt.shared?=speed
 
 # Pass file with the list of updatable boot class path packages to dex2oat.
 PRODUCT_SYSTEM_PROPERTIES += \
diff --git a/tools/Android.bp b/tools/Android.bp
index 159890c..149d06d 100644
--- a/tools/Android.bp
+++ b/tools/Android.bp
@@ -37,3 +37,22 @@
     },
   },
 }
+
+python_test_host {
+  name: "post_process_props_unittest",
+  main: "test_post_process_props.py",
+  srcs: [
+    "post_process_props.py",
+    "test_post_process_props.py",
+  ],
+  version: {
+    py2: {
+      enabled: false,
+    },
+    py3: {
+      enabled: true,
+    },
+  },
+  test_config: "post_process_props_unittest.xml",
+  test_suites: ["general-tests"],
+}
diff --git a/tools/post_process_props.py b/tools/post_process_props.py
index 4fa15bc..d8c9cb1 100755
--- a/tools/post_process_props.py
+++ b/tools/post_process_props.py
@@ -14,10 +14,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import argparse
 import sys
 
-# Usage: post_process_props.py file.prop [blacklist_key, ...]
-# Blacklisted keys are removed from the property file, if present
+# Usage: post_process_props.py file.prop [disallowed_key, ...]
+# Disallowed keys are removed from the property file, if present
 
 # See PROP_VALUE_MAX in system_properties.h.
 # The constant in system_properties.h includes the terminating NUL,
@@ -29,8 +30,8 @@
 def mangle_build_prop(prop_list):
   # If ro.debuggable is 1, then enable adb on USB by default
   # (this is for userdebug builds)
-  if prop_list.get("ro.debuggable") == "1":
-    val = prop_list.get("persist.sys.usb.config")
+  if prop_list.get_value("ro.debuggable") == "1":
+    val = prop_list.get_value("persist.sys.usb.config")
     if "adb" not in val:
       if val == "":
         val = "adb"
@@ -40,52 +41,132 @@
   # UsbDeviceManager expects a value here.  If it doesn't get it, it will
   # default to "adb". That might not the right policy there, but it's better
   # to be explicit.
-  if not prop_list.get("persist.sys.usb.config"):
+  if not prop_list.get_value("persist.sys.usb.config"):
     prop_list.put("persist.sys.usb.config", "none");
 
 def validate(prop_list):
   """Validate the properties.
 
+  If the value of a sysprop exceeds the max limit (91), it's an error, unless
+  the sysprop is a read-only one.
+
+  Checks if there is no optional prop assignments.
+
   Returns:
     True if nothing is wrong.
   """
   check_pass = True
-  for p in prop_list.get_all():
+  for p in prop_list.get_all_props():
     if len(p.value) > PROP_VALUE_MAX and not p.name.startswith("ro."):
       check_pass = False
       sys.stderr.write("error: %s cannot exceed %d bytes: " %
                        (p.name, PROP_VALUE_MAX))
       sys.stderr.write("%s (%d)\n" % (p.value, len(p.value)))
+
+    if p.is_optional():
+      check_pass = False
+      sys.stderr.write("error: found unresolved optional prop assignment:\n")
+      sys.stderr.write(str(p) + "\n")
+
   return check_pass
 
+def override_optional_props(prop_list, allow_dup=False):
+  """Override a?=b with a=c, if the latter exists
+
+  Overriding is done by deleting a?=b
+  When there are a?=b and a?=c, then only the last one survives
+  When there are a=b and a=c, then it's an error.
+
+  Returns:
+    True if the override was successful
+  """
+  success = True
+  for name in prop_list.get_all_names():
+    props = prop_list.get_props(name)
+    optional_props = [p for p in props if p.is_optional()]
+    overriding_props = [p for p in props if not p.is_optional()]
+    if len(overriding_props) > 1:
+      # duplicated props are allowed when the all have the same value
+      if all(overriding_props[0].value == p.value for p in overriding_props):
+        for p in optional_props:
+          p.delete("overridden by %s" % str(overriding_props[0]))
+        continue
+      # or if dup is explicitly allowed for compat reason
+      if allow_dup:
+        # this could left one or more optional props unresolved.
+        # Convert them into non-optional because init doesn't understand ?=
+        # syntax
+        for p in optional_props:
+          p.optional = False
+        continue
+
+      success = False
+      sys.stderr.write("error: found duplicate sysprop assignments:\n")
+      for p in overriding_props:
+        sys.stderr.write("%s\n" % str(p))
+    elif len(overriding_props) == 1:
+      for p in optional_props:
+        p.delete("overridden by %s" % str(overriding_props[0]))
+    else:
+      if len(optional_props) > 1:
+        for p in optional_props[:-1]:
+          p.delete("overridden by %s" % str(optional_props[-1]))
+      # Make the last optional one as non-optional
+      optional_props[-1].optional = False
+
+  return success
+
 class Prop:
 
-  def __init__(self, name, value, comment=None):
+  def __init__(self, name, value, optional=False, comment=None):
     self.name = name.strip()
     self.value = value.strip()
-    self.comment = comment
+    if comment != None:
+      self.comments = [comment]
+    else:
+      self.comments = []
+    self.optional = optional
 
   @staticmethod
   def from_line(line):
     line = line.rstrip('\n')
     if line.startswith("#"):
-      return Prop("", "", line)
+      return Prop("", "", comment=line)
+    elif "?=" in line:
+      name, value = line.split("?=", 1)
+      return Prop(name, value, optional=True)
     elif "=" in line:
       name, value = line.split("=", 1)
-      return Prop(name, value)
+      return Prop(name, value, optional=False)
     else:
       # don't fail on invalid line
       # TODO(jiyong) make this a hard error
-      return Prop("", "", line)
+      return Prop("", "", comment=line)
 
   def is_comment(self):
-    return self.comment != None
+    return bool(self.comments and not self.name)
+
+  def is_optional(self):
+    return (not self.is_comment()) and self.optional
+
+  def make_as_comment(self):
+    # Prepend "#" to the last line which is the prop assignment
+    if not self.is_comment():
+      assignment = str(self).rsplit("\n", 1)[-1]
+      self.comments.append("#" + assignment)
+      self.name = ""
+      self.value = ""
+
+  def delete(self, reason):
+    self.comments.append("# Removed by post_process_props.py because " + reason)
+    self.make_as_comment()
 
   def __str__(self):
-    if self.is_comment():
-      return self.comment
-    else:
-      return self.name + "=" + self.value
+    assignment = []
+    if not self.is_comment():
+      operator = "?=" if self.is_optional() else "="
+      assignment.append(self.name + operator + self.value)
+    return "\n".join(self.comments + assignment)
 
 class PropList:
 
@@ -94,47 +175,65 @@
       self.props = [Prop.from_line(l)
                     for l in f.readlines() if l.strip() != ""]
 
-  def get_all(self):
+  def get_all_props(self):
     return [p for p in self.props if not p.is_comment()]
 
-  def get(self, name):
+  def get_all_names(self):
+    return set([p.name for p in self.get_all_props()])
+
+  def get_props(self, name):
+    return [p for p in self.get_all_props() if p.name == name]
+
+  def get_value(self, name):
+    # Caution: only the value of the first sysprop having the name is returned.
     return next((p.value for p in self.props if p.name == name), "")
 
   def put(self, name, value):
-    index = next((i for i,p in enumerate(self.props) if p.name == name), -1)
+    # Note: when there is an optional prop for the name, its value isn't changed.
+    # Instead a new non-optional prop is appended, which will override the
+    # optional prop. Otherwise, the new value might be overridden by an existing
+    # non-optional prop of the same name.
+    index = next((i for i,p in enumerate(self.props)
+                  if p.name == name and not p.is_optional()), -1)
     if index == -1:
-      self.props.append(Prop(name, value))
+      self.props.append(Prop(name, value,
+                             comment="# Auto-added by post_process_props.py"))
     else:
+      self.props[index].comments.append(
+          "# Value overridden by post_process_props.py. Original value: %s" %
+          self.props[index].value)
       self.props[index].value = value
 
-  def delete(self, name):
-    index = next((i for i,p in enumerate(self.props) if p.name == name), -1)
-    if index != -1:
-      new_comment = "# removed by post_process_props.py\n#" + str(self.props[index])
-      self.props[index] = Prop.from_line(new_comment)
-
   def write(self, filename):
     with open(filename, 'w+') as f:
       for p in self.props:
         f.write(str(p) + "\n")
 
 def main(argv):
-  filename = argv[1]
+  parser = argparse.ArgumentParser(description="Post-process build.prop file")
+  parser.add_argument("--allow-dup", dest="allow_dup", action="store_true",
+                      default=False)
+  parser.add_argument("filename")
+  parser.add_argument("disallowed_keys", metavar="KEY", type=str, nargs="*")
+  args = parser.parse_args()
 
-  if not filename.endswith("/build.prop"):
+  if not args.filename.endswith("/build.prop"):
     sys.stderr.write("bad command line: " + str(argv) + "\n")
     sys.exit(1)
 
-  props = PropList(filename)
+  props = PropList(args.filename)
   mangle_build_prop(props)
+  if not override_optional_props(props, args.allow_dup):
+    sys.exit(1)
   if not validate(props):
     sys.exit(1)
 
-  # Drop any blacklisted keys
-  for key in argv[2:]:
-    props.delete(key)
+  # Drop any disallowed keys
+  for key in args.disallowed_keys:
+    for p in props.get_props(key):
+      p.delete("%s is a disallowed key" % key)
 
-  props.write(filename)
+  props.write(args.filename)
 
 if __name__ == "__main__":
   main(sys.argv)
diff --git a/tools/post_process_props_unittest.xml b/tools/post_process_props_unittest.xml
new file mode 100644
index 0000000..4a6ecc2
--- /dev/null
+++ b/tools/post_process_props_unittest.xml
@@ -0,0 +1,6 @@
+<configuration description="Config to run post_process_props_unittest">
+    <test class="com.android.tradefed.testtype.python.PythonBinaryHostTest" >
+        <option name="par-file-name" value="post_process_props_unittest" />
+        <option name="test-timeout" value="1m" />
+    </test>
+</configuration>
diff --git a/tools/releasetools/edify_generator.py b/tools/releasetools/edify_generator.py
index 99e21f1..b9c9b19 100644
--- a/tools/releasetools/edify_generator.py
+++ b/tools/releasetools/edify_generator.py
@@ -374,12 +374,12 @@
 
   def _CheckSecondTokenNotSlotSuffixed(self, s, fn):
     lst = s.split(':')
-    assert(len(s) == 4), "{} does not contain 4 tokens".format(s)
+    assert(len(lst) == 4), "{} does not contain 4 tokens".format(s)
     if self.fstab:
-      entry = common.GetEntryForDevice(s[1])
+      entry = common.GetEntryForDevice(self.fstab, lst[1])
       if entry is not None:
         assert not entry.slotselect, \
-          "Use %s because %s is slot suffixed" % (fn, s[1])
+          "Use %s because %s is slot suffixed" % (fn, lst[1])
 
   def WriteRawImage(self, mount_point, fn, mapfn=None):
     """Write the given package file into the partition for the given
diff --git a/tools/releasetools/validate_target_files.py b/tools/releasetools/validate_target_files.py
index 69be511..ac469eb 100755
--- a/tools/releasetools/validate_target_files.py
+++ b/tools/releasetools/validate_target_files.py
@@ -371,6 +371,17 @@
             partition, info_dict, key_file)
         cmd.extend(['--expected_chain_partition', chained_partition_arg])
 
+    # Handle the boot image with a non-default name, e.g. boot-5.4.img
+    boot_images = info_dict.get("boot_images")
+    if boot_images:
+      # we used the 1st boot image to generate the vbmeta. Rename the filename
+      # to boot.img so that avbtool can find it correctly.
+      first_image_name = boot_images.split()[0]
+      first_image_path = os.path.join(input_tmp, 'IMAGES', first_image_name)
+      assert os.path.isfile(first_image_path)
+      renamed_boot_image_path = os.path.join(input_tmp, 'IMAGES', 'boot.img')
+      os.rename(first_image_path, renamed_boot_image_path)
+
     proc = common.Run(cmd)
     stdoutdata, _ = proc.communicate()
     assert proc.returncode == 0, \
diff --git a/tools/test_post_process_props.py b/tools/test_post_process_props.py
new file mode 100644
index 0000000..12d52e5
--- /dev/null
+++ b/tools/test_post_process_props.py
@@ -0,0 +1,255 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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.
+
+import contextlib
+import io
+import unittest
+
+from unittest.mock import *
+from post_process_props import *
+
+class PropTestCase(unittest.TestCase):
+  def test_createFromLine(self):
+    p = Prop.from_line("# this is comment")
+    self.assertTrue(p.is_comment())
+    self.assertEqual("", p.name)
+    self.assertEqual("", p.value)
+    self.assertFalse(p.is_optional())
+    self.assertEqual("# this is comment", str(p))
+
+    for line in ["a=b", "a = b", "a= b", "a =b", "  a=b   "]:
+      p = Prop.from_line(line)
+      self.assertFalse(p.is_comment())
+      self.assertEqual("a", p.name)
+      self.assertEqual("b", p.value)
+      self.assertFalse(p.is_optional())
+      self.assertEqual("a=b", str(p))
+
+    for line in ["a?=b", "a ?= b", "a?= b", "a ?=b", "  a?=b   "]:
+      p = Prop.from_line(line)
+      self.assertFalse(p.is_comment())
+      self.assertEqual("a", p.name)
+      self.assertEqual("b", p.value)
+      self.assertTrue(p.is_optional())
+      self.assertEqual("a?=b", str(p))
+
+  def test_makeAsComment(self):
+    p = Prop.from_line("a=b")
+    p.comments.append("# a comment")
+    self.assertFalse(p.is_comment())
+
+    p.make_as_comment()
+    self.assertTrue(p.is_comment())
+    self.assertTrue("# a comment\n#a=b", str(p))
+
+class PropListTestcase(unittest.TestCase):
+  def setUp(self):
+    content = """
+    # comment
+    foo=true
+    bar=false
+    qux?=1
+    # another comment
+    foo?=false
+    """
+    self.patcher = patch("post_process_props.open", mock_open(read_data=content))
+    self.mock_open = self.patcher.start()
+    self.props = PropList("file")
+
+  def tearDown(self):
+    self.patcher.stop()
+    self.props = None
+
+  def test_readFromFile(self):
+    self.assertEqual(4, len(self.props.get_all_props()))
+    expected = [
+        ("foo", "true", False),
+        ("bar", "false", False),
+        ("qux", "1", True),
+        ("foo", "false", True)
+    ]
+    for i,p in enumerate(self.props.get_all_props()):
+      self.assertEqual(expected[i][0], p.name)
+      self.assertEqual(expected[i][1], p.value)
+      self.assertEqual(expected[i][2], p.is_optional())
+      self.assertFalse(p.is_comment())
+
+    self.assertEqual(set(["foo", "bar", "qux"]), self.props.get_all_names())
+
+    self.assertEqual("true", self.props.get_value("foo"))
+    self.assertEqual("false", self.props.get_value("bar"))
+    self.assertEqual("1", self.props.get_value("qux"))
+
+    # there are two assignments for 'foo'
+    self.assertEqual(2, len(self.props.get_props("foo")))
+
+  def test_putNewProp(self):
+    self.props.put("new", "30")
+
+    self.assertEqual(5, len(self.props.get_all_props()))
+    last_prop = self.props.get_all_props()[-1]
+    self.assertEqual("new", last_prop.name)
+    self.assertEqual("30", last_prop.value)
+    self.assertFalse(last_prop.is_optional())
+
+  def test_putExistingNonOptionalProp(self):
+    self.props.put("foo", "NewValue")
+
+    self.assertEqual(4, len(self.props.get_all_props()))
+    foo_prop = self.props.get_props("foo")[0]
+    self.assertEqual("foo", foo_prop.name)
+    self.assertEqual("NewValue", foo_prop.value)
+    self.assertFalse(foo_prop.is_optional())
+    self.assertEqual("# Value overridden by post_process_props.py. " +
+                     "Original value: true\nfoo=NewValue", str(foo_prop))
+
+  def test_putExistingOptionalProp(self):
+    self.props.put("qux", "2")
+
+    self.assertEqual(5, len(self.props.get_all_props()))
+    last_prop = self.props.get_all_props()[-1]
+    self.assertEqual("qux", last_prop.name)
+    self.assertEqual("2", last_prop.value)
+    self.assertFalse(last_prop.is_optional())
+    self.assertEqual("# Auto-added by post_process_props.py\nqux=2",
+                     str(last_prop))
+
+  def test_deleteNonOptionalProp(self):
+    props_to_delete = self.props.get_props("foo")[0]
+    props_to_delete.delete(reason="testing")
+
+    self.assertEqual(3, len(self.props.get_all_props()))
+    self.assertEqual("# Removed by post_process_props.py because testing\n" +
+                     "#foo=true", str(props_to_delete))
+
+  def test_deleteOptionalProp(self):
+    props_to_delete = self.props.get_props("qux")[0]
+    props_to_delete.delete(reason="testing")
+
+    self.assertEqual(3, len(self.props.get_all_props()))
+    self.assertEqual("# Removed by post_process_props.py because testing\n" +
+                     "#qux?=1", str(props_to_delete))
+
+  def test_overridingNonOptional(self):
+    props_to_be_overridden = self.props.get_props("foo")[1]
+    self.assertTrue("true", props_to_be_overridden.value)
+
+    self.assertTrue(override_optional_props(self.props))
+
+    # size reduced to 3 because foo?=false was overridden by foo=true
+    self.assertEqual(3, len(self.props.get_all_props()))
+
+    self.assertEqual(1, len(self.props.get_props("foo")))
+    self.assertEqual("true", self.props.get_props("foo")[0].value)
+
+    self.assertEqual("# Removed by post_process_props.py because " +
+                     "overridden by foo=true\n#foo?=false",
+                     str(props_to_be_overridden))
+
+  def test_overridingOptional(self):
+    content = """
+    # comment
+    qux?=2
+    foo=true
+    bar=false
+    qux?=1
+    # another comment
+    foo?=false
+    """
+    with patch('post_process_props.open', mock_open(read_data=content)) as m:
+      props = PropList("hello")
+
+      props_to_be_overridden = props.get_props("qux")[0]
+      self.assertEqual("2", props_to_be_overridden.value)
+
+      self.assertTrue(override_optional_props(props))
+
+      self.assertEqual(1, len(props.get_props("qux")))
+      self.assertEqual("1", props.get_props("qux")[0].value)
+      # the only left optional assignment becomes non-optional
+      self.assertFalse(props.get_props("qux")[0].is_optional())
+
+      self.assertEqual("# Removed by post_process_props.py because " +
+                       "overridden by qux?=1\n#qux?=2",
+                       str(props_to_be_overridden))
+
+  def test_overridingDuplicated(self):
+    content = """
+    # comment
+    foo=true
+    bar=false
+    qux?=1
+    foo=false
+    # another comment
+    foo?=false
+    """
+    with patch("post_process_props.open", mock_open(read_data=content)) as m:
+      stderr_redirect = io.StringIO()
+      with contextlib.redirect_stderr(stderr_redirect):
+        props = PropList("hello")
+
+        # fails due to duplicated foo=true and foo=false
+        self.assertFalse(override_optional_props(props))
+
+        self.assertEqual("error: found duplicate sysprop assignments:\n" +
+                         "foo=true\nfoo=false\n", stderr_redirect.getvalue())
+
+  def test_overridingDuplicatedWithSameValue(self):
+    content = """
+    # comment
+    foo=true
+    bar=false
+    qux?=1
+    foo=true
+    # another comment
+    foo?=false
+    """
+    with patch("post_process_props.open", mock_open(read_data=content)) as m:
+      stderr_redirect = io.StringIO()
+      with contextlib.redirect_stderr(stderr_redirect):
+        props = PropList("hello")
+        optional_prop = props.get_props("foo")[2] # the last foo?=false one
+
+        # we have duplicated foo=true and foo=true, but that's allowed
+        # since they have the same value
+        self.assertTrue(override_optional_props(props))
+
+        # foo?=false should be commented out
+        self.assertEqual("# Removed by post_process_props.py because " +
+                         "overridden by foo=true\n#foo?=false",
+                         str(optional_prop))
+
+  def test_allowDuplicates(self):
+    content = """
+    # comment
+    foo=true
+    bar=false
+    qux?=1
+    foo=false
+    # another comment
+    foo?=false
+    """
+    with patch("post_process_props.open", mock_open(read_data=content)) as m:
+      stderr_redirect = io.StringIO()
+      with contextlib.redirect_stderr(stderr_redirect):
+        props = PropList("hello")
+
+        # we have duplicated foo=true and foo=false, but that's allowed
+        # because it's explicitly allowed
+        self.assertTrue(override_optional_props(props, allow_dup=True))
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)