Merge "Add tool to find problems with build repeatability."
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..ab2564e
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2021 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.
+
+package {
+    default_applicable_licenses: ["build_make_license"],
+}
+
+// Added automatically by a large-scale-change that took the approach of
+// 'apply every license found to every target'. While this makes sure we respect
+// every license restriction, it may not be entirely correct.
+//
+// e.g. GPL in an MIT project might only apply to the contrib/ directory.
+//
+// Please consider splitting the single license below into multiple licenses,
+// taking care not to lose any license_kind information, and overriding the
+// default license using the 'licenses: [...]' property on targets as needed.
+//
+// For unused files, consider creating a 'fileGroup' with "//visibility:private"
+// to attach the license to, and including a comment whether the files may be
+// used in the current project.
+// See: http://go/android-license-faq
+license {
+    name: "build_make_license",
+    visibility: [":__subpackages__"],
+    license_kinds: [
+        "SPDX-license-identifier-Apache-2.0",
+        "SPDX-license-identifier-BSD",
+        "SPDX-license-identifier-CC-BY",
+        "SPDX-license-identifier-GPL",
+        "SPDX-license-identifier-GPL-2.0",
+        "SPDX-license-identifier-LGPL",
+        "SPDX-license-identifier-MIT",
+        "legacy_not_a_contribution",
+        "legacy_restricted",
+    ],
+    // large-scale-change unable to identify any license_text files
+}
diff --git a/common/json.mk b/common/json.mk
index ba8ffa7..e376aab 100644
--- a/common/json.mk
+++ b/common/json.mk
@@ -24,7 +24,10 @@
 add_json_csv =$= $(call add_json_val,$(1),$(call csv_to_json_list,$(strip $(2))))
 add_json_bool =$= $(call add_json_val,$(1),$(if $(strip $(2)),true,false))
 add_json_map =$= $(eval _json_contents := $$(_json_contents)$$(_json_indent)"$$(strip $$(1))": {$$(newline))$(json_increase_indent)
+add_json_map_anon =$= $(eval _json_contents := $$(_json_contents)$$(_json_indent){$$(newline))$(json_increase_indent)
 end_json_map =$= $(json_decrease_indent)$(eval _json_contents := $$(_json_contents)$$(if $$(filter %$$(comma),$$(lastword $$(_json_contents))),__SV_END)$$(_json_indent)},$$(newline))
+add_json_array =$= $(eval _json_contents := $$(_json_contents)$$(_json_indent)"$$(strip $$(1))": [$$(newline))$(json_increase_indent)
+end_json_array =$= $(json_decrease_indent)$(eval _json_contents := $$(_json_contents)$$(if $$(filter %$$(comma),$$(lastword $$(_json_contents))),__SV_END)$$(_json_indent)],$$(newline))
 
 # Clears _json_contents to start a new json file
 json_start =$= $(eval _json_contents := {$$(newline))$(eval _json_indent := $$(4space))
diff --git a/core/Makefile b/core/Makefile
index 30b6cc9..a7553f0 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -230,7 +230,7 @@
 # $(7): module archive
 # $(8): staging dir for stripped modules
 # $(9): module directory name
-# Returns the a list of src:dest pairs to install the modules using copy-many-files.
+# Returns a list of src:dest pairs to install the modules using copy-many-files.
 define build-image-kernel-modules
   $(if $(9), \
     $(eval _dir := $(9)/), \
@@ -315,6 +315,26 @@
 	@echo '$$(strip $$(notdir $$(PRIVATE_LOAD_MODULES)))' | tr ' ' '\n' > $$(@)
 endef
 
+# $(1): source blocklist file
+# $(2): destination pathname
+# Returns a build rule that checks the syntax of and installs a kernel modules
+# blocklist file. Strip and squeeze any extra space in the blocklist.
+# For use via $(eval).
+define build-image-kernel-modules-blocklist-file
+$(2): $(1)
+	@echo "modprobe blocklist $$(@)"
+	$(hide) mkdir -p "$$(dir $$@)"
+	$(hide) rm -f "$$@"
+	$(hide) awk <"$$<" >"$$@" \
+	  '/^#/ { print; next } \
+	   NF == 0 { next } \
+	   NF != 2 || $$$$1 != "blocklist" \
+	     { print "Invalid blocklist line " FNR ": " $$$$0 >"/dev/stderr"; \
+	       exit_status = 1; next } \
+	   { $$$$1 = $$$$1; print } \
+	   END { exit exit_status }'
+endef
+
 # $(1): image name
 # $(2): build output directory (TARGET_OUT_VENDOR, TARGET_RECOVERY_ROOT_OUT, etc)
 # $(3): mount point
@@ -331,7 +351,12 @@
 $(if $(strip $(BOARD_$(1)_KERNEL_MODULES$(_sep)$(_kver))$(BOARD_$(1)_KERNEL_MODULES_ARCHIVE$(_sep)$(_kver))),\
   $(if $(BOARD_$(1)_KERNEL_MODULES_LOAD$(_sep)$(_kver)),,\
     $(eval BOARD_$(1)_KERNEL_MODULES_LOAD$(_sep)$(_kver) := $(BOARD_$(1)_KERNEL_MODULES$(_sep)$(_kver)))) \
-  $(call copy-many-files,$(call build-image-kernel-modules,$(BOARD_$(1)_KERNEL_MODULES$(_sep)$(_kver)),$(2),$(3),$(call intermediates-dir-for,PACKAGING,depmod_$(1)$(_sep)$(_kver)),$(BOARD_$(1)_KERNEL_MODULES_LOAD$(_sep)$(_kver)),$(4),$(BOARD_$(1)_KERNEL_MODULES_ARCHIVE$(_sep)$(_kver)),$(_stripped_staging_dir),$(_kver))))
+  $(call copy-many-files,$(call build-image-kernel-modules,$(BOARD_$(1)_KERNEL_MODULES$(_sep)$(_kver)),$(2),$(3),$(call intermediates-dir-for,PACKAGING,depmod_$(1)$(_sep)$(_kver)),$(BOARD_$(1)_KERNEL_MODULES_LOAD$(_sep)$(_kver)),$(4),$(BOARD_$(1)_KERNEL_MODULES_ARCHIVE$(_sep)$(_kver)),$(_stripped_staging_dir),$(_kver)))) \
+$(if $(BOARD_$(1)_KERNEL_MODULES_BLOCKLIST_FILE$(_sep)$(_kver)), \
+  $(eval $(call build-image-kernel-modules-blocklist-file, \
+    $(BOARD_$(1)_KERNEL_MODULES_BLOCKLIST_FILE$(_sep)$(_kver)), \
+    $(2)/lib/modules/modules.blocklist)) \
+  $(2)/lib/modules/modules.blocklist)
 endef
 
 # $(1): kernel module directory name (top is an out of band value for no directory)
diff --git a/core/android_soong_config_vars.mk b/core/android_soong_config_vars.mk
index 5b24881..cc369a3 100644
--- a/core/android_soong_config_vars.mk
+++ b/core/android_soong_config_vars.mk
@@ -33,11 +33,17 @@
   $(call add_soong_config_namespace,art_module)
   SOONG_CONFIG_art_module += source_build
 endif
-ifneq (,$(filter true,$(NATIVE_COVERAGE) $(CLANG_COVERAGE)))
+ifneq (,$(findstring .android.art,$(TARGET_BUILD_APPS)))
+  # Build ART modules from source if they are listed in TARGET_BUILD_APPS.
+  SOONG_CONFIG_art_module_source_build := true
+else ifneq (,$(filter true,$(NATIVE_COVERAGE) $(CLANG_COVERAGE)))
   # Always build ART APEXes from source in coverage builds since the prebuilts
   # aren't built with instrumentation.
   # TODO(b/172480617): Find another solution for this.
   SOONG_CONFIG_art_module_source_build := true
+else ifneq (,$(SANITIZE_TARGET)$(SANITIZE_HOST))
+  # Prebuilts aren't built with sanitizers either.
+  SOONG_CONFIG_art_module_source_build := true
 else
   # This sets the default for building ART APEXes from source rather than
   # prebuilts (in packages/modules/ArtPrebuilt and prebuilt/module_sdk/art) in
diff --git a/core/app_prebuilt_internal.mk b/core/app_prebuilt_internal.mk
index 5767996..cb1b453 100644
--- a/core/app_prebuilt_internal.mk
+++ b/core/app_prebuilt_internal.mk
@@ -237,6 +237,7 @@
 $(built_module) : $(my_prebuilt_src_file) | $(ZIPALIGN) $(ZIP2ZIP) $(SIGNAPK_JAR)
 	$(transform-prebuilt-to-target)
 	$(uncompress-prebuilt-embedded-jni-libs)
+	$(remove-unwanted-prebuilt-embedded-jni-libs)
 ifeq (true, $(LOCAL_UNCOMPRESS_DEX))
 	$(uncompress-dexs)
 endif  # LOCAL_UNCOMPRESS_DEX
diff --git a/core/board_config.mk b/core/board_config.mk
index 725c0a5..183bdbb 100644
--- a/core/board_config.mk
+++ b/core/board_config.mk
@@ -129,6 +129,7 @@
   BUILD_BROKEN_PREBUILT_ELF_FILES \
   BUILD_BROKEN_TREBLE_SYSPROP_NEVERALLOW \
   BUILD_BROKEN_USES_NETWORK \
+  BUILD_BROKEN_VENDOR_PROPERTY_NAMESPACE \
   BUILD_BROKEN_VINTF_PRODUCT_COPY_FILES \
 
 _build_broken_var_list += \
diff --git a/core/clang/tidy.mk b/core/clang/tidy.mk
index 868f7bc..8a40878 100644
--- a/core/clang/tidy.mk
+++ b/core/clang/tidy.mk
@@ -36,7 +36,7 @@
 )
 endef
 
-# Default filter contains current directory $1 and DEFAULT_TIDY_HEADER_DIRS.
+# Default filter contains current directory $1 and optional DEFAULT_TIDY_HEADER_DIRS.
 define default_tidy_header_filter
-  -header-filter="($(subst $(space),,$1|$(DEFAULT_TIDY_HEADER_DIRS)))"
+  -header-filter=$(if $(DEFAULT_TIDY_HEADER_DIRS),"($1/|$(DEFAULT_TIDY_HEADER_DIRS))",$1/)
 endef
diff --git a/core/definitions.mk b/core/definitions.mk
index 5f0bf55..de95890 100644
--- a/core/definitions.mk
+++ b/core/definitions.mk
@@ -556,7 +556,7 @@
   $(foreach m, $(ALL_MODULES), \
     $(eval ALL_MODULES.$(m).NOTICE_DEPS := \
       $(sort \
-         $(foreach d,$(ALL_MODULES.$(m).NOTICE_DEPS), \
+         $(foreach d,$(sort $(ALL_MODULES.$(m).NOTICE_DEPS)), \
            $(_lookup.$(d)) \
         ) \
       ) \
@@ -578,7 +578,9 @@
 define license-metadata-rule
 $(strip $(eval _dir := $(call license-metadata-dir)))
 $(strip $(eval _deps := $(sort $(filter-out $(_dir)/$(1).meta_lic,$(foreach d,$(ALL_MODULES.$(1).NOTICE_DEPS), $(_dir)/$(d).meta_lic)))))
-$(foreach b,$(sort $(ALL_MODULES.$(1).BUILT) $(ALL_MODULES.$(1).INSTALLED)),
+$(strip $(eval _notices := $(sort $(ALL_MODULES.$(1).NOTICES))))
+$(strip $(eval _tgts := $(sort $(ALL_MODULES.$(1).BUILT) $(ALL_MODULES.$(1).INSTALLED))))
+$(foreach b,$(_tgts),
 $(_dir)/$(b).meta_module ::
 	mkdir -p $$(dir $$@)
 	echo $(_dir)/$(1).meta_lic >> $$@
@@ -587,31 +589,46 @@
 )
 $(_dir)/$(1).meta_lic: PRIVATE_KINDS := $(sort $(ALL_MODULES.$(1).LICENSE_KINDS))
 $(_dir)/$(1).meta_lic: PRIVATE_CONDITIONS := $(sort $(ALL_MODULES.$(1).LICENSE_CONDITIONS))
-$(_dir)/$(1).meta_lic: PRIVATE_NOTICES := $(sort $(ALL_MODULES.$(1).NOTICES))
+$(_dir)/$(1).meta_lic: PRIVATE_NOTICES := $(_notices)
 $(_dir)/$(1).meta_lic: PRIVATE_NOTICE_DEPS := $(_deps)
-$(_dir)/$(1).meta_lic: PRIVATE_TARGETS := $(sort $(ALL_MODULES.$(1).BUILT) $(ALL_MODULES.$(1).INSTALLED))
-$(_dir)/$(1).meta_lic: PRIVATE_IS_CONTAINER := $(sort $(ALL_MODULES.$(1).IS_CONTAINER))
-$(_dir)/$(1).meta_lic: PRIVATE_PACKAGE_NAME := $(ALL_MODULES.$(1).LICENSE_PACKAGE_NAME)
+$(_dir)/$(1).meta_lic: PRIVATE_TARGETS := $(_tgts)
+$(_dir)/$(1).meta_lic: PRIVATE_IS_CONTAINER := $(ALL_MODULES.$(1).IS_CONTAINER)
+$(_dir)/$(1).meta_lic: PRIVATE_PACKAGE_NAME := $(strip $(ALL_MODULES.$(1).LICENSE_PACKAGE_NAME))
 $(_dir)/$(1).meta_lic: PRIVATE_INSTALL_MAP := $(sort $(ALL_MODULES.$(1).LICENSE_INSTALL_MAP))
-$(_dir)/$(1).meta_lic : $(_deps) $(ALL_MODULES.$(1).NOTICES) $(foreach b,$(sort $(ALL_MODULES.$(1).BUILT) $(ALL_MODULES.$(1).INSTALLED)), $(_dir)/$(b).meta_module) build/make/tools/build-license-metadata.sh
+$(_dir)/$(1).meta_lic : $(_deps) $(_notices) $(foreach b,$(_tgts), $(_dir)/$(b).meta_module) build/make/tools/build-license-metadata.sh
 	rm -f $$@
 	mkdir -p $$(dir $$@)
-	build/make/tools/build-license-metadata.sh -k $$(PRIVATE_KINDS) -c $$(PRIVATE_CONDITIONS) -n $$(PRIVATE_NOTICES) -d $$(PRIVATE_NOTICE_DEPS) -m $$(PRIVATE_INSTALL_MAP) -t $$(PRIVATE_TARGETS) $$(if $$(filter-out false,$$(PRIVATE_IS_CONTAINER)),-is_container) -p $$(PRIVATE_PACKAGE_NAME) -o $$@
-
-$(1) : $(_dir)/$(1).meta_lic
-
-$(if $(ALL_MODULES.$(1).INSTALLED_NOTICE_FILE),$(ALL_MODULES.$(1).INSTALLED_NOTICE_FILE) : $(_dir)/$(1).meta_lic)
+	build/make/tools/build-license-metadata.sh -k $$(PRIVATE_KINDS) -c $$(PRIVATE_CONDITIONS) -n $$(PRIVATE_NOTICES) -d $$(PRIVATE_NOTICE_DEPS) -m $$(PRIVATE_INSTALL_MAP) -t $$(PRIVATE_TARGETS) $$(if $$(PRIVATE_IS_CONTAINER),-is_container) -p $$(PRIVATE_PACKAGE_NAME) -o $$@
 
 .PHONY: $(1).meta_lic
 $(1).meta_lic : $(_dir)/$(1).meta_lic
 
+$(strip $(eval _mifs := $(sort $(ALL_MODULES.$(1).MODULE_INSTALLED_FILENAMES))))
+$(strip $(eval _infs := $(sort $(ALL_MODULES.$(1).INSTALLED_NOTICE_FILE))))
+
+# Emit each installed notice file rule if it references the current module
+$(if $(_infs),$(foreach inf,$(_infs),
+$(if $(strip $(filter $(1),$(INSTALLED_NOTICE_FILES.$(inf).MODULE))),
+$(strip $(eval _mif := $(firstword $(foreach m,$(_mifs),$(if $(filter %/src/$(m).txt,$(inf)),$(m))))))
+
+$(inf) : $(_dir)/$(1).meta_lic
+$(inf): PRIVATE_INSTALLED_MODULE := $(_mif)
+$(inf) : PRIVATE_NOTICES := $(_notices)
+
+$(inf): $(_notices)
+	@echo Notice file: $$< -- $$@
+	mkdir -p $$(dir $$@)
+	awk 'FNR==1 && NR > 1 {print "\n"} {print}' $$(PRIVATE_NOTICES) > $$@
+
+)))
+
 endef
 
 ###########################################################
 ## Declares a license metadata build rule for ALL_MODULES
 ###########################################################
 define build-license-metadata
-$(foreach m,$(ALL_MODULES),$(eval $(call license-metadata-rule,$(m))))
+$(foreach m,$(sort $(ALL_MODULES)),$(eval $(call license-metadata-rule,$(m))))
 endef
 
 ###########################################################
@@ -2396,14 +2413,19 @@
 #
 define uncompress-prebuilt-embedded-jni-libs
   if (zipinfo $@ 'lib/*.so' 2>/dev/null | grep -v ' stor ' >/dev/null) ; then \
-    $(ZIP2ZIP) -i $@ -o $@.tmp -0 'lib/**/*.so' \
-      $(if $(PRIVATE_EMBEDDED_JNI_LIBS), \
-        -x 'lib/**/*.so' \
-        $(addprefix -X ,$(PRIVATE_EMBEDDED_JNI_LIBS))) && \
-    mv -f $@.tmp $@ ; \
+    $(ZIP2ZIP) -i $@ -o $@.tmp -0 'lib/**/*.so' && mv -f $@.tmp $@ ; \
   fi
 endef
 
+# Remove unwanted shared JNI libraries embedded in an apk.
+#
+define remove-unwanted-prebuilt-embedded-jni-libs
+  $(if $(PRIVATE_EMBEDDED_JNI_LIBS), \
+    $(ZIP2ZIP) -i $@ -o $@.tmp \
+      -x 'lib/**/*.so' $(addprefix -X ,$(PRIVATE_EMBEDDED_JNI_LIBS)) && \
+    mv -f $@.tmp $@)
+endef
+
 # TODO(joeo): If we can ever upgrade to post 3.81 make and get the
 # new prebuilt rules to work, we should change this to copy the
 # resources to the out directory and then copy the resources.
diff --git a/core/dex_preopt_config_merger.py b/core/dex_preopt_config_merger.py
index 1bd1519..4efcc17 100755
--- a/core/dex_preopt_config_merger.py
+++ b/core/dex_preopt_config_merger.py
@@ -67,24 +67,25 @@
   # the loop in case this changes in the future.
   for sdk_ver in clc_map:
     clcs = clc_map[sdk_ver]
-    clcs2 = OrderedDict()
-    for lib in clcs:
-      clc = clcs[lib]
+    clcs2 = []
+    for clc in clcs:
+      lib = clc['Name']
       if lib in uses_libs:
         ulib = uses_libs[lib]
+        # The real <uses-library> name (may be different from the module name).
+        clc['Name'] = ulib['ProvidesUsesLibrary']
         # On-device (install) path to the dependency DEX jar file.
         clc['Device'] = ulib['DexLocation']
         # CLC of the dependency becomes a subcontext. We only need sub-CLC for
         # 'any' version because all other versions are for compatibility
         # libraries, which exist only for apps and not for libraries.
         clc['Subcontexts'] = ulib['ClassLoaderContexts'].get('any')
-        # Patch the library name in the CLC as well.
-        clcs2[ulib['ProvidesUsesLibrary']] = clc
       else:
         # dexpreopt.config for this <uses-library> is not among the script
         # arguments, which may be the case with compatibility libraries that
         # don't need patching anyway. Just use the original CLC.
-        clcs2[lib] = clc
+        pass
+      clcs2.append(clc)
     clc_map2[sdk_ver] = clcs2
 
   # Overwrite the original class loader context with the patched one.
diff --git a/core/dex_preopt_odex_install.mk b/core/dex_preopt_odex_install.mk
index 6f6ac28..f9a9ba7 100644
--- a/core/dex_preopt_odex_install.mk
+++ b/core/dex_preopt_odex_install.mk
@@ -196,8 +196,8 @@
   # to load dexpreopt code on device. We should fix this, either by deferring
   # dependency computation until the full list of product packages is known, or
   # by adding product-specific lists of missing libraries.
-  my_filtered_optional_uses_libraries := $(filter $(my_filtered_optional_uses_libraries), \
-    $(PRODUCT_PACKAGES))
+  my_filtered_optional_uses_libraries := $(filter $(PRODUCT_PACKAGES), \
+    $(my_filtered_optional_uses_libraries))
 
   ifeq ($(LOCAL_MODULE_CLASS),APPS)
     # compatibility libraries are added to class loader context of an app only if
@@ -222,10 +222,9 @@
     my_dexpreopt_libs_compat :=
   endif
 
-  my_dexpreopt_libs := $(sort \
+  my_dexpreopt_libs := \
     $(LOCAL_USES_LIBRARIES) \
-    $(my_filtered_optional_uses_libraries) \
-  )
+    $(my_filtered_optional_uses_libraries)
 
   # 1: SDK version
   # 2: list of libraries
@@ -243,14 +242,15 @@
   # which are special and not handled by dex_preopt_config_merger.py.
   #
   add_json_class_loader_context = \
-    $(call add_json_map, $(1)) \
+    $(call add_json_array, $(1)) \
     $(foreach lib, $(2),\
-      $(call add_json_map, $(lib)) \
+      $(call add_json_map_anon) \
+      $(call add_json_str, Name, $(lib)) \
       $(call add_json_str, Host, $(call intermediates-dir-for,JAVA_LIBRARIES,$(lib),,COMMON)/javalib.jar) \
       $(call add_json_str, Device, /system/framework/$(lib).jar) \
-      $(call add_json_map, Subcontexts, ${$}) $(call end_json_map) \
+      $(call add_json_val, Subcontexts, null) \
       $(call end_json_map)) \
-    $(call end_json_map)
+    $(call end_json_array)
 
   # Record dex-preopt config.
   DEXPREOPT.$(LOCAL_MODULE).DEX_PREOPT := $(LOCAL_DEX_PREOPT)
diff --git a/core/dumpconfig.mk b/core/dumpconfig.mk
new file mode 100644
index 0000000..dd3ef43
--- /dev/null
+++ b/core/dumpconfig.mk
@@ -0,0 +1,129 @@
+# Read and dump the product configuration.
+
+# Called from the product-config tool, not from the main build system.
+
+#
+# Ensure we are being called correctly
+#
+ifndef KATI
+    $(warning Kati must be used to call dumpconfig.mk, not make.)
+    $(error stopping)
+endif
+
+ifdef DEFAULT_GOAL
+    $(warning Calling dumpconfig.mk from inside the make build system is not)
+    $(warning supported. It is only meant to be called via kati by product-confing.)
+    $(error stopping)
+endif
+
+ifndef TARGET_PRODUCT
+    $(warning dumpconfig.mk requires TARGET_PRODUCT to be set)
+    $(error stopping)
+endif
+
+ifndef TARGET_BUILD_VARIANT
+    $(warning dumpconfig.mk requires TARGET_BUILD_VARIANT to be set)
+    $(error stopping)
+endif
+
+ifneq (build/make/core/config.mk,$(wildcard build/make/core/config.mk))
+    $(warning dumpconfig must be called from the root of the source tree)
+    $(error stopping)
+endif
+
+ifeq (,$(DUMPCONFIG_FILE))
+    $(warning dumpconfig requires DUMPCONFIG_FILE to be set)
+    $(error stopping)
+endif
+
+# Before we do anything else output the format version.
+$(file > $(DUMPCONFIG_FILE),dumpconfig_version,1)
+$(file >> $(DUMPCONFIG_FILE),dumpconfig_file,$(DUMPCONFIG_FILE))
+
+# Default goal for dumpconfig
+dumpconfig:
+	$(file >> $(DUMPCONFIG_FILE),***DONE***)
+	@echo ***DONE***
+
+# TODO(Remove): These need to be set externally
+OUT_DIR := out
+TMPDIR = /tmp/build-temp
+BUILD_DATETIME_FILE := $(OUT_DIR)/build_date.txt
+
+# Escape quotation marks for CSV, and wraps in quotation marks.
+define escape-for-csv
+"$(subst ","",$1)"
+endef
+
+# Args:
+#   $(1): include stack
+define dump-import-start
+$(eval $(file >> $(DUMPCONFIG_FILE),import,$(strip $(1))))
+endef
+
+# Args:
+#   $(1): include stack
+define dump-import-done
+$(eval $(file >> $(DUMPCONFIG_FILE),imported,$(strip $(1))))
+endef
+
+# Args:
+#   $(1): Current file
+#   $(2): Inherited file
+define dump-inherit
+$(eval $(file >> $(DUMPCONFIG_FILE),inherit,$(strip $(1)),$(strip $(2))))
+endef
+
+# Args:
+#   $(1): Config phase (PRODUCT or DEVICE)
+#   $(2): Root nodes to import
+#   $(3): All variable names
+#   $(4): Single-value variables
+#   $(5): Makefile being processed
+define dump-phase-start
+$(eval $(file >> $(DUMPCONFIG_FILE),phase,$(strip $(1)),$(strip $(2)))) \
+$(foreach var,$(3), \
+    $(eval $(file >> $(DUMPCONFIG_FILE),var,$(if $(filter $(4),$(var)),single,list),$(var))) \
+) \
+$(call dump-config-vals,$(strip $(5)),initial)
+endef
+
+# Args:
+#   $(1): Makefile being processed
+define dump-phase-end
+$(call dump-config-vals,$(strip $(1)),final)
+endef
+
+define dump-debug
+$(eval $(file >> $(DUMPCONFIG_FILE),debug,$(1)))
+endef
+
+# Skip these when dumping. They're not used and they cause a lot of noise in the dump.
+DUMPCONFIG_SKIP_VARS := \
+	.VARIABLES \
+	.KATI_SYMBOLS \
+	1 \
+	2 \
+	LOCAL_PATH \
+	MAKEFILE_LIST \
+	PARENT_PRODUCT_FILES \
+	current_mk \
+	inherit_var \
+	np \
+	_node_import_context \
+	_included \
+	_include_stack \
+	_in \
+	_nic.%
+
+# Args:
+#   $(1): Makefile that was included
+#   $(2): block (before,import,after,initial,final)
+define dump-config-vals
+$(foreach var,$(filter-out $(DUMPCONFIG_SKIP_VARS),$(.KATI_SYMBOLS)),\
+    $(eval $(file >> $(DUMPCONFIG_FILE),val,$(call escape-for-csv,$(1)),$(2),$(call escape-for-csv,$(var)),$(call escape-for-csv,$($(var))),$(call escape-for-csv,$(KATI_variable_location $(var))))) \
+)
+endef
+
+include build/make/core/config.mk
+
diff --git a/core/envsetup.mk b/core/envsetup.mk
index a5571ae..33f4f25 100644
--- a/core/envsetup.mk
+++ b/core/envsetup.mk
@@ -301,6 +301,10 @@
     com.android.art:okhttp \
     com.android.art:bouncycastle \
     com.android.art:apache-xml
+# With EMMA_INSTRUMENT_FRAMEWORK=true the Core libraries depend on jacoco.
+ifeq (true,$(EMMA_INSTRUMENT_FRAMEWORK))
+  ART_APEX_JARS += com.android.art:jacocoagent
+endif
 #################################################################
 
 # Read the product specs so we can get TARGET_DEVICE and other
diff --git a/core/main.mk b/core/main.mk
index 5ea95c8..2c78815 100644
--- a/core/main.mk
+++ b/core/main.mk
@@ -83,6 +83,8 @@
 -include test/vts/tools/vts-core-tradefed/build/config.mk
 # CSUITE-specific config.
 -include test/app_compat/csuite/tools/build/config.mk
+# CTS-Root-specific config.
+-include test/cts-root/tools/build/config.mk
 
 # Clean rules
 .PHONY: clean-dex-files
diff --git a/core/node_fns.mk b/core/node_fns.mk
index b81d60c..8d20160 100644
--- a/core/node_fns.mk
+++ b/core/node_fns.mk
@@ -195,7 +195,11 @@
   $(call clear-var-list, $(3))
   $(eval LOCAL_PATH := $(patsubst %/,%,$(dir $(2))))
   $(eval MAKEFILE_LIST :=)
+  $(call dump-import-start,$(_include_stack))
+  $(call dump-config-vals,$(2),before)
   $(eval include $(2))
+  $(call dump-import-done,$(_include_stack))
+  $(call dump-config-vals,$(2),after)
   $(eval _included := $(filter-out $(2),$(MAKEFILE_LIST)))
   $(eval MAKEFILE_LIST :=)
   $(eval LOCAL_PATH :=)
@@ -250,6 +254,7 @@
 #       of the default list semantics
 #
 define import-nodes
+$(call dump-phase-start,$(1),$(2),$(3),$(4),build/make/core/node_fns.mk) \
 $(if \
   $(foreach _in,$(2), \
     $(eval _node_import_context := _nic.$(1).[[$(_in)]]) \
@@ -263,5 +268,6 @@
     $(if $(_include_stack),$(eval $(error ASSERTION FAILED: _include_stack \
                 should be empty here: $(_include_stack))),) \
    ) \
-,)
+,) \
+$(call dump-phase-end,build/make/core/node_fns.mk)
 endef
diff --git a/core/notice_files.mk b/core/notice_files.mk
index 89f822b..9678380 100644
--- a/core/notice_files.mk
+++ b/core/notice_files.mk
@@ -80,7 +80,7 @@
 ifeq (true,$(is_container))
 # Include shared libraries' notices for "container" types, but not for binaries etc.
 notice_deps := \
-    $(sort \
+    $(strip \
         $(LOCAL_REQUIRED_MODULES) \
         $(LOCAL_STATIC_LIBRARIES) \
         $(LOCAL_WHOLE_STATIC_LIBRARIES) \
@@ -95,7 +95,7 @@
     )
 else
 notice_deps := \
-    $(sort \
+    $(strip \
         $(LOCAL_REQUIRED_MODULES) \
         $(LOCAL_STATIC_LIBRARIES) \
         $(LOCAL_WHOLE_STATIC_LIBRARIES) \
@@ -106,24 +106,24 @@
     )
 endif
 ifeq ($(LOCAL_IS_HOST_MODULE),true)
-notice_deps := $(sort $(notice_deps) $(LOCAL_HOST_REQUIRED_MODULES))
+notice_deps := $(strip $(notice_deps) $(LOCAL_HOST_REQUIRED_MODULES))
 else
-notice_deps := $(sort $(notice_deps) $(LOCAL_TARGET_REQUIRED_MODULES))
+notice_deps := $(strip $(notice_deps) $(LOCAL_TARGET_REQUIRED_MODULES))
 endif
 
 ifdef my_register_name
 ALL_MODULES.$(my_register_name).LICENSE_PACKAGE_NAME := $(strip $(license_package_name))
-ALL_MODULES.$(my_register_name).LICENSE_KINDS := $(sort $(ALL_MODULES.$(my_register_name).LICENSE_KINDS) $(license_kinds))
-ALL_MODULES.$(my_register_name).LICENSE_CONDITIONS := $(sort $(ALL_MODULES.$(my_register_name).LICENSE_CONDITIONS) $(license_conditions))
-ALL_MODULES.$(my_register_name).LICENSE_INSTALL_MAP := $(sort $(ALL_MODULES.$(my_register_name).LICENSE_INSTALL_MAP) $(install_map))
-ALL_MODULES.$(my_register_name).NOTICE_DEPS := $(sort $(ALL_MODULES.$(my_register_name).NOTICE_DEPS) $(notice_deps))
-ALL_MODULES.$(my_register_name).IS_CONTAINER := $(sort $(ALL_MODULES.$(my_register_name).IS_CONTAINER) $(is_container))
+ALL_MODULES.$(my_register_name).LICENSE_KINDS := $(ALL_MODULES.$(my_register_name).LICENSE_KINDS) $(license_kinds)
+ALL_MODULES.$(my_register_name).LICENSE_CONDITIONS := $(ALL_MODULES.$(my_register_name).LICENSE_CONDITIONS) $(license_conditions)
+ALL_MODULES.$(my_register_name).LICENSE_INSTALL_MAP := $(ALL_MODULES.$(my_register_name).LICENSE_INSTALL_MAP) $(install_map)
+ALL_MODULES.$(my_register_name).NOTICE_DEPS := $(ALL_MODULES.$(my_register_name).NOTICE_DEPS) $(notice_deps)
+ALL_MODULES.$(my_register_name).IS_CONTAINER := $(strip $(filter-out false,$(ALL_MODULES.$(my_register_name).IS_CONTAINER) $(is_container)))
 endif
 
 ifdef notice_file
 
 ifdef my_register_name
-ALL_MODULES.$(my_register_name).NOTICES := $(sort $(ALL_MODULES.$(my_register_name).NOTICES) $(notice_file))
+ALL_MODULES.$(my_register_name).NOTICES := $(ALL_MODULES.$(my_register_name).NOTICES) $(notice_file)
 endif
 
 # This relies on the name of the directory in PRODUCT_OUT matching where
@@ -180,9 +180,10 @@
 installed_notice_file := $($(my_prefix)OUT_NOTICE_FILES)/src/$(module_installed_filename).txt
 
 ifdef my_register_name
-ALL_MODULES.$(my_register_name).INSTALLED_NOTICE_FILE := $(installed_notice_file)
-endif
-
+ALL_MODULES.$(my_register_name).INSTALLED_NOTICE_FILE := $(ALL_MODULES.$(my_register_name).INSTALLED_NOTICE_FILE) $(installed_notice_file)
+ALL_MODULES.$(my_register_name).MODULE_INSTALLED_FILENAMES := $(ALL_MODULES.$(my_register_name).MODULE_INSTALLED_FILENAMES) $(module_installed_filename)
+INSTALLED_NOTICE_FILES.$(installed_notice_file).MODULE := $(my_register_name)
+else
 $(installed_notice_file): PRIVATE_INSTALLED_MODULE := $(module_installed_filename)
 $(installed_notice_file) : PRIVATE_NOTICES := $(notice_file)
 
@@ -190,6 +191,7 @@
 	@echo Notice file: $< -- $@
 	$(hide) mkdir -p $(dir $@)
 	$(hide) awk 'FNR==1 && NR > 1 {print "\n"} {print}' $(PRIVATE_NOTICES) > $@
+endif
 
 ifdef LOCAL_INSTALLED_MODULE
 # Make LOCAL_INSTALLED_MODULE depend on NOTICE files if they exist
diff --git a/core/package_internal.mk b/core/package_internal.mk
index 1b40624..346ca24 100644
--- a/core/package_internal.mk
+++ b/core/package_internal.mk
@@ -101,7 +101,6 @@
 enforce_rro_enabled :=
 ifneq (,$(filter *, $(PRODUCT_ENFORCE_RRO_TARGETS)))
   # * means all system and system_ext APKs, so enable conditionally based on module path.
-  # Note that modules in PRODUCT_ENFORCE_RRO_EXEMPTED_TARGETS are excluded even if it is '*'
 
   # Note that base_rules.mk has not yet been included, so it's likely that only
   # one of LOCAL_MODULE_PATH and the LOCAL_X_MODULE flags has been set.
@@ -120,12 +119,6 @@
   enforce_rro_enabled := true
 endif
 
-# TODO(b/150820813) Some modules depend on static overlay, remove this after eliminating the dependency.
-ifneq (,$(filter $(LOCAL_PACKAGE_NAME), $(PRODUCT_ENFORCE_RRO_EXEMPTED_TARGETS)))
-  enforce_rro_enabled :=
-endif
-
-
 product_package_overlays := $(strip \
     $(wildcard $(foreach dir, $(PRODUCT_PACKAGE_OVERLAYS), \
       $(addprefix $(dir)/, $(LOCAL_RESOURCE_DIR)))))
diff --git a/core/product.mk b/core/product.mk
index 2ab4b06..170402a 100644
--- a/core/product.mk
+++ b/core/product.mk
@@ -195,9 +195,6 @@
 # Package list to apply enforcing RRO.
 _product_list_vars += PRODUCT_ENFORCE_RRO_TARGETS
 
-# Packages to skip auto-generating RROs for when PRODUCT_ENFORCE_RRO_TARGETS is set to *.
-_product_list_vars += PRODUCT_ENFORCE_RRO_EXEMPTED_TARGETS
-
 _product_list_vars += PRODUCT_SDK_ATREE_FILES
 _product_list_vars += PRODUCT_SDK_ADDON_NAME
 _product_list_vars += PRODUCT_SDK_ADDON_COPY_FILES
@@ -224,10 +221,10 @@
 # instead of PRODUCT_BOOT_JARS, so that device-specific jars go after common jars.
 _product_list_vars += PRODUCT_BOOT_JARS_EXTRA
 
-_product_list_vars += PRODUCT_SUPPORTS_BOOT_SIGNER
-_product_list_vars += PRODUCT_SUPPORTS_VBOOT
-_product_list_vars += PRODUCT_SUPPORTS_VERITY
-_product_list_vars += PRODUCT_SUPPORTS_VERITY_FEC
+_product_single_value_vars += PRODUCT_SUPPORTS_BOOT_SIGNER
+_product_single_value_vars += PRODUCT_SUPPORTS_VBOOT
+_product_single_value_vars += PRODUCT_SUPPORTS_VERITY
+_product_single_value_vars += PRODUCT_SUPPORTS_VERITY_FEC
 _product_list_vars += PRODUCT_SYSTEM_SERVER_APPS
 _product_list_vars += PRODUCT_SYSTEM_SERVER_JARS
 # List of system_server jars delivered via apex. Format = <apex name>:<jar name>.
@@ -460,7 +457,9 @@
   $(eval current_mk := $(strip $(word 1,$(_include_stack)))) \
   $(eval inherit_var := PRODUCTS.$(current_mk).INHERITS_FROM) \
   $(eval $(inherit_var) := $(sort $($(inherit_var)) $(np))) \
-  $(eval PARENT_PRODUCT_FILES := $(sort $(PARENT_PRODUCT_FILES) $(current_mk)))
+  $(eval PARENT_PRODUCT_FILES := $(sort $(PARENT_PRODUCT_FILES) $(current_mk))) \
+  $(call dump-inherit,$(strip $(word 1,$(_include_stack))),$(1)) \
+  $(call dump-config-vals,$(current_mk),inherit)
 endef
 
 # Specifies a number of path prefixes, relative to PRODUCT_OUT, where the
diff --git a/core/product_config.mk b/core/product_config.mk
index 7b72b5e..6d886ec 100644
--- a/core/product_config.mk
+++ b/core/product_config.mk
@@ -401,6 +401,11 @@
 $(KATI_obsolete_var PRODUCT_USE_PRODUCT_VNDK,Use PRODUCT_PRODUCT_VNDK_VERSION instead)
 $(KATI_obsolete_var PRODUCT_USE_PRODUCT_VNDK_OVERRIDE,Use PRODUCT_PRODUCT_VNDK_VERSION instead)
 
+ifdef PRODUCT_ENFORCE_RRO_EXEMPTED_TARGETS
+    $(error PRODUCT_ENFORCE_RRO_EXEMPTED_TARGETS is deprecated, consider using RRO for \
+      $(PRODUCT_ENFORCE_RRO_EXEMPTED_TARGETS))
+endif
+
 define product-overrides-config
 $$(foreach rule,$$(PRODUCT_$(1)_OVERRIDES),\
     $$(if $$(filter 2,$$(words $$(subst :,$$(space),$$(rule)))),,\
diff --git a/core/soong_config.mk b/core/soong_config.mk
index bdc8ce9..9fdf7b8 100644
--- a/core/soong_config.mk
+++ b/core/soong_config.mk
@@ -80,7 +80,6 @@
 $(call add_json_list, DeviceResourceOverlays,            $(DEVICE_PACKAGE_OVERLAYS))
 $(call add_json_list, ProductResourceOverlays,           $(PRODUCT_PACKAGE_OVERLAYS))
 $(call add_json_list, EnforceRROTargets,                 $(PRODUCT_ENFORCE_RRO_TARGETS))
-$(call add_json_list, EnforceRROExemptedTargets,         $(PRODUCT_ENFORCE_RRO_EXEMPTED_TARGETS))
 $(call add_json_list, EnforceRROExcludedOverlays,        $(PRODUCT_ENFORCE_RRO_EXCLUDED_OVERLAYS))
 
 $(call add_json_str,  AAPTCharacteristics,               $(TARGET_AAPT_CHARACTERISTICS))
@@ -244,6 +243,10 @@
 $(call add_json_bool, BoardMoveRecoveryResourcesToVendorBoot, $(BOARD_MOVE_RECOVERY_RESOURCES_TO_VENDOR_BOOT))
 $(call add_json_str,  PrebuiltHiddenApiDir, $(BOARD_PREBUILT_HIDDENAPI_DIR))
 
+$(call add_json_str,  ShippingApiLevel, $(PRODUCT_SHIPPING_API_LEVEL))
+
+$(call add_json_bool, BuildBrokenVendorPropertyNamespace, $(BUILD_BROKEN_VENDOR_PROPERTY_NAMESPACE))
+
 $(call json_end)
 
 $(file >$(SOONG_VARIABLES).tmp,$(json_contents))
diff --git a/core/tasks/cts_root.mk b/core/tasks/cts_root.mk
new file mode 100644
index 0000000..b618121
--- /dev/null
+++ b/core/tasks/cts_root.mk
@@ -0,0 +1,25 @@
+# Copyright (C) 2021 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.
+
+ifneq ($(wildcard test/cts-root/README.md),)
+test_suite_name := cts_root
+test_suite_tradefed := cts-root-tradefed
+test_suite_readme := test/cts-root/README.md
+
+include $(BUILD_SYSTEM)/tasks/tools/compatibility.mk
+
+.PHONY: cts_root
+cts_root: $(compatibility_zip)
+$(call dist-for-goals, cts_root, $(compatibility_zip))
+endif
diff --git a/envsetup.sh b/envsetup.sh
index c03e2cb..a5f6b6d 100644
--- a/envsetup.sh
+++ b/envsetup.sh
@@ -27,6 +27,7 @@
 - mangrep:    Greps on all local AndroidManifest.xml files.
 - mgrep:      Greps on all local Makefiles and *.bp files.
 - owngrep:    Greps on all local OWNERS files.
+- rgrep:      Greps on all local Rust files.
 - sepgrep:    Greps on all local sepolicy files.
 - sgrep:      Greps on all local source files.
 - godir:      Go to the directory containing a file.
@@ -1037,6 +1038,12 @@
         -exec grep --color -n "$@" {} +
 }
 
+function rgrep()
+{
+    find . -name .repo -prune -o -name .git -prune -o -name out -prune -o -type f -name "*\.rs" \
+        -exec grep --color -n "$@" {} +
+}
+
 function cgrep()
 {
     find . -name .repo -prune -o -name .git -prune -o -name out -prune -o -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) \
diff --git a/target/board/Android.mk b/target/board/Android.mk
index 9edc85c..4dd6b17 100644
--- a/target/board/Android.mk
+++ b/target/board/Android.mk
@@ -35,6 +35,8 @@
 # $(DEVICE_MANIFEST_FILE) can be a list of files
 include $(CLEAR_VARS)
 LOCAL_MODULE        := vendor_manifest.xml
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 legacy_not_a_contribution
+LOCAL_LICENSE_CONDITIONS := by_exception_only not_allowed notice
 LOCAL_MODULE_STEM   := manifest.xml
 LOCAL_MODULE_CLASS  := ETC
 LOCAL_MODULE_PATH   := $(TARGET_OUT_VENDOR)/etc/vintf
@@ -65,6 +67,8 @@
 my_fragment_files := $$($$(my_fragment_files_var))
 include $$(CLEAR_VARS)
 LOCAL_MODULE := vendor_manifest_$(1).xml
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 legacy_not_a_contribution
+LOCAL_LICENSE_CONDITIONS := by_exception_only not_allowed notice
 LOCAL_MODULE_STEM := manifest_$(1).xml
 LOCAL_MODULE_CLASS := ETC
 LOCAL_MODULE_PATH   := $(TARGET_OUT_VENDOR)/etc/vintf
@@ -94,6 +98,8 @@
 # ODM_MANIFEST_FILES is a list of files that is combined and installed as the default ODM manifest.
 include $(CLEAR_VARS)
 LOCAL_MODULE := odm_manifest.xml
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 legacy_not_a_contribution
+LOCAL_LICENSE_CONDITIONS := by_exception_only not_allowed notice
 LOCAL_MODULE_STEM := manifest.xml
 LOCAL_MODULE_CLASS := ETC
 LOCAL_MODULE_RELATIVE_PATH := vintf
@@ -124,6 +130,8 @@
 my_fragment_files := $$($$(my_fragment_files_var))
 include $$(CLEAR_VARS)
 LOCAL_MODULE := odm_manifest_$(1).xml
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 legacy_not_a_contribution
+LOCAL_LICENSE_CONDITIONS := by_exception_only not_allowed notice
 LOCAL_MODULE_STEM := manifest_$(1).xml
 LOCAL_MODULE_CLASS := ETC
 LOCAL_MODULE_RELATIVE_PATH := vintf
diff --git a/target/board/BoardConfigEmuCommon.mk b/target/board/BoardConfigEmuCommon.mk
index 342abd7..fe0293b 100644
--- a/target/board/BoardConfigEmuCommon.mk
+++ b/target/board/BoardConfigEmuCommon.mk
@@ -90,3 +90,6 @@
 DEVICE_MATRIX_FILE   := device/generic/goldfish/compatibility_matrix.xml
 
 BOARD_SEPOLICY_DIRS += device/generic/goldfish/sepolicy/common
+
+# b/176210699: remove this
+BUILD_BROKEN_VENDOR_PROPERTY_NAMESPACE := true
diff --git a/target/product/base_system.mk b/target/product/base_system.mk
index 5238d40..09864bc 100644
--- a/target/product/base_system.mk
+++ b/target/product/base_system.mk
@@ -214,6 +214,7 @@
     ndc \
     netd \
     NetworkStackNext \
+    odsign \
     org.apache.http.legacy \
     otacerts \
     PackageInstaller \
diff --git a/target/product/gsi/Android.bp b/target/product/gsi/Android.bp
index b7ce86e..88472eb 100644
--- a/target/product/gsi/Android.bp
+++ b/target/product/gsi/Android.bp
@@ -12,6 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "build_make_license"
+    // to get the below license kinds:
+    //   legacy_restricted
+    default_applicable_licenses: ["build_make_license"],
+}
+
 filegroup {
     name: "vndk_lib_lists",
     srcs: [
diff --git a/target/product/gsi/Android.mk b/target/product/gsi/Android.mk
index f21fe16..ecce01a 100644
--- a/target/product/gsi/Android.mk
+++ b/target/product/gsi/Android.mk
@@ -63,6 +63,8 @@
 # Script to update the latest VNDK lib list
 include $(CLEAR_VARS)
 LOCAL_MODULE := update-vndk-list.sh
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := EXECUTABLES
 LOCAL_MODULE_STEM := $(LOCAL_MODULE)
 LOCAL_IS_HOST_MODULE := true
@@ -146,6 +148,8 @@
 
 include $(CLEAR_VARS)
 LOCAL_MODULE := vndk_package
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 # Filter LLNDK libs moved to APEX to avoid pulling them into /system/LIB
 LOCAL_REQUIRED_MODULES := \
     $(filter-out $(LLNDK_MOVED_TO_APEX_LIBRARIES),$(LLNDK_LIBRARIES))
@@ -169,6 +173,8 @@
 	_vndk_versions += $(BOARD_VNDK_VERSION)
 endif
 LOCAL_MODULE := vndk_apex_snapshot_package
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_REQUIRED_MODULES := $(foreach vndk_ver,$(_vndk_versions),com.android.vndk.v$(vndk_ver))
 include $(BUILD_PHONY_PACKAGE)
 
@@ -181,6 +187,8 @@
 
 include $(CLEAR_VARS)
 LOCAL_MODULE := gsi_skip_mount.cfg
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_STEM := skip_mount.cfg
 LOCAL_SRC_FILES := $(LOCAL_MODULE)
 LOCAL_MODULE_CLASS := ETC
@@ -204,6 +212,8 @@
 
 include $(CLEAR_VARS)
 LOCAL_MODULE := init.gsi.rc
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_SRC_FILES := $(LOCAL_MODULE)
 LOCAL_MODULE_CLASS := ETC
 LOCAL_SYSTEM_EXT_MODULE := true
@@ -214,6 +224,8 @@
 
 include $(CLEAR_VARS)
 LOCAL_MODULE := init.vndk-nodef.rc
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_SRC_FILES := $(LOCAL_MODULE)
 LOCAL_MODULE_CLASS := ETC
 LOCAL_SYSTEM_EXT_MODULE := true
diff --git a/target/product/gsi/current.txt b/target/product/gsi/current.txt
index 717d990..c753e6c 100644
--- a/target/product/gsi/current.txt
+++ b/target/product/gsi/current.txt
@@ -19,12 +19,8 @@
 LLNDK: libvndksupport.so
 LLNDK: libvulkan.so
 VNDK-SP: android.hardware.common-V2-ndk_platform.so
-VNDK-SP: android.hardware.common-unstable-ndk_platform.so
 VNDK-SP: android.hardware.common.fmq-V1-ndk_platform.so
-VNDK-SP: android.hardware.common.fmq-ndk_platform.so
-VNDK-SP: android.hardware.common.fmq-unstable-ndk_platform.so
 VNDK-SP: android.hardware.graphics.common-V2-ndk_platform.so
-VNDK-SP: android.hardware.graphics.common-unstable-ndk_platform.so
 VNDK-SP: android.hardware.graphics.common@1.0.so
 VNDK-SP: android.hardware.graphics.common@1.1.so
 VNDK-SP: android.hardware.graphics.common@1.2.so
@@ -62,10 +58,7 @@
 VNDK-SP: libz.so
 VNDK-core: android.hardware.audio.common@2.0.so
 VNDK-core: android.hardware.authsecret-V1-ndk_platform.so
-VNDK-core: android.hardware.authsecret-ndk_platform.so
-VNDK-core: android.hardware.authsecret-unstable-ndk_platform.so
 VNDK-core: android.hardware.automotive.occupant_awareness-V1-ndk_platform.so
-VNDK-core: android.hardware.automotive.occupant_awareness-ndk_platform.so
 VNDK-core: android.hardware.configstore-utils.so
 VNDK-core: android.hardware.configstore@1.0.so
 VNDK-core: android.hardware.configstore@1.1.so
@@ -76,52 +69,28 @@
 VNDK-core: android.hardware.graphics.bufferqueue@1.0.so
 VNDK-core: android.hardware.graphics.bufferqueue@2.0.so
 VNDK-core: android.hardware.health.storage-V1-ndk_platform.so
-VNDK-core: android.hardware.health.storage-ndk_platform.so
-VNDK-core: android.hardware.health.storage-unstable-ndk_platform.so
 VNDK-core: android.hardware.identity-V2-ndk_platform.so
-VNDK-core: android.hardware.identity-ndk_platform.so
 VNDK-core: android.hardware.keymaster-V2-ndk_platform.so
-VNDK-core: android.hardware.keymaster-ndk_platform.so
 VNDK-core: android.hardware.light-V1-ndk_platform.so
-VNDK-core: android.hardware.light-ndk_platform.so
 VNDK-core: android.hardware.media.bufferpool@2.0.so
 VNDK-core: android.hardware.media.omx@1.0.so
 VNDK-core: android.hardware.media@1.0.so
 VNDK-core: android.hardware.memtrack-V1-ndk_platform.so
-VNDK-core: android.hardware.memtrack-ndk_platform.so
-VNDK-core: android.hardware.memtrack-unstable-ndk_platform.so
 VNDK-core: android.hardware.memtrack@1.0.so
 VNDK-core: android.hardware.oemlock-V1-ndk_platform.so
-VNDK-core: android.hardware.oemlock-ndk_platform.so
-VNDK-core: android.hardware.oemlock-unstable-ndk_platform.so
 VNDK-core: android.hardware.power-V1-ndk_platform.so
-VNDK-core: android.hardware.power-ndk_platform.so
 VNDK-core: android.hardware.power.stats-V1-ndk_platform.so
-VNDK-core: android.hardware.power.stats-ndk_platform.so
-VNDK-core: android.hardware.power.stats-unstable-ndk_platform.so
 VNDK-core: android.hardware.rebootescrow-V1-ndk_platform.so
-VNDK-core: android.hardware.rebootescrow-ndk_platform.so
 VNDK-core: android.hardware.security.keymint-V1-ndk_platform.so
-VNDK-core: android.hardware.security.keymint-ndk_platform.so
-VNDK-core: android.hardware.security.keymint-unstable-ndk_platform.so
 VNDK-core: android.hardware.security.secureclock-V1-ndk_platform.so
-VNDK-core: android.hardware.security.secureclock-ndk_platform.so
-VNDK-core: android.hardware.security.secureclock-unstable-ndk_platform.so
 VNDK-core: android.hardware.security.sharedsecret-V1-ndk_platform.so
-VNDK-core: android.hardware.security.sharedsecret-ndk_platform.so
-VNDK-core: android.hardware.security.sharedsecret-unstable-ndk_platform.so
 VNDK-core: android.hardware.soundtrigger@2.0-core.so
 VNDK-core: android.hardware.soundtrigger@2.0.so
 VNDK-core: android.hardware.vibrator-V1-ndk_platform.so
-VNDK-core: android.hardware.vibrator-ndk_platform.so
 VNDK-core: android.hardware.weaver-V1-ndk_platform.so
-VNDK-core: android.hardware.weaver-ndk_platform.so
-VNDK-core: android.hardware.weaver-unstable-ndk_platform.so
 VNDK-core: android.hidl.token@1.0-utils.so
 VNDK-core: android.hidl.token@1.0.so
 VNDK-core: android.system.keystore2-V1-ndk_platform.so
-VNDK-core: android.system.keystore2-ndk_platform.so
-VNDK-core: android.system.keystore2-unstable-ndk_platform.so
 VNDK-core: android.system.suspend@1.0.so
 VNDK-core: libaudioroute.so
 VNDK-core: libaudioutils.so
diff --git a/target/product/runtime_libart.mk b/target/product/runtime_libart.mk
index a07e706..b588c78 100644
--- a/target/product/runtime_libart.mk
+++ b/target/product/runtime_libart.mk
@@ -30,10 +30,43 @@
 PRODUCT_PACKAGES += com.android.runtime
 
 # ART APEX module.
-# Note that this package includes the minimal boot classpath JARs (listed in
-# ART_APEX_JARS), which should no longer be added directly to PRODUCT_PACKAGES.
-PRODUCT_PACKAGES += com.android.art-autoselect
-PRODUCT_HOST_PACKAGES += com.android.art-autoselect
+#
+# Select either release (com.android.art) or debug (com.android.art.debug)
+# variant of the ART APEX. By default, "user" build variants contain the release
+# module, while the "eng" build variant contain the debug module. However, if
+# `PRODUCT_ART_TARGET_INCLUDE_DEBUG_BUILD` is defined, it overrides the previous
+# logic:
+# - if `PRODUCT_ART_TARGET_INCLUDE_DEBUG_BUILD` is set to `false`, the
+#   build will include the release module (whatever the build
+#   variant);
+# - if `PRODUCT_ART_TARGET_INCLUDE_DEBUG_BUILD` is set to `true`, the
+#   build will include the debug module (whatever the build variant).
+#
+# Note that the ART APEX package includes the minimal boot classpath JARs
+# (listed in ART_APEX_JARS), which should no longer be added directly to
+# PRODUCT_PACKAGES.
+
+art_target_include_debug_build := $(PRODUCT_ART_TARGET_INCLUDE_DEBUG_BUILD)
+ifneq (false,$(art_target_include_debug_build))
+  ifneq (,$(filter eng,$(TARGET_BUILD_VARIANT)))
+    art_target_include_debug_build := true
+  endif
+endif
+
+ifeq (true,$(art_target_include_debug_build))
+  PRODUCT_PACKAGES += com.android.art.debug
+  apex_test_module := art-check-debug-apex-gen-fakebin
+else
+  PRODUCT_PACKAGES += com.android.art
+  apex_test_module := art-check-release-apex-gen-fakebin
+endif
+
+ifeq (true,$(SOONG_CONFIG_art_module_source_build)
+  PRODUCT_HOST_PACKAGES += $(apex_test_module)
+endif
+
+art_target_include_debug_build :=
+apex_test_module :=
 
 # Certificates.
 PRODUCT_PACKAGES += \
@@ -55,7 +88,7 @@
 # On eng builds, make "boot" reasons only extract for faster turnaround.
 ifeq (eng,$(TARGET_BUILD_VARIANT))
     PRODUCT_SYSTEM_PROPERTIES += \
-        pm.dexopt.first-boot-ota?=extract \
+        pm.dexopt.first-boot?=extract \
         pm.dexopt.boot-after-ota?=extract
 else
     PRODUCT_SYSTEM_PROPERTIES += \
diff --git a/target/product/security/Android.bp b/target/product/security/Android.bp
index 5f4f82b..98698c5 100644
--- a/target/product/security/Android.bp
+++ b/target/product/security/Android.bp
@@ -1,4 +1,13 @@
 // AOSP test certificate
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "build_make_license"
+    // to get the below license kinds:
+    //   legacy_restricted
+    default_applicable_licenses: ["build_make_license"],
+}
+
 android_app_certificate {
     name: "aosp-testkey",
     certificate: "testkey",
diff --git a/target/product/security/Android.mk b/target/product/security/Android.mk
index d6a8b53..83f0a4b 100644
--- a/target/product/security/Android.mk
+++ b/target/product/security/Android.mk
@@ -5,6 +5,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := verity_key
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_SRC_FILES := $(LOCAL_MODULE)
 LOCAL_MODULE_CLASS := ETC
 LOCAL_MODULE_PATH := $(TARGET_ROOT_OUT)
@@ -24,6 +26,8 @@
 ifneq ($(BOARD_BUILD_SYSTEM_ROOT_IMAGE),true)
   include $(CLEAR_VARS)
   LOCAL_MODULE := verity_key_ramdisk
+  LOCAL_LICENSE_KINDS := legacy_restricted
+  LOCAL_LICENSE_CONDITIONS := restricted
   LOCAL_MODULE_CLASS := ETC
   LOCAL_SRC_FILES := verity_key
   LOCAL_MODULE_STEM := verity_key
@@ -37,6 +41,8 @@
   ifneq ($(filter eng userdebug,$(TARGET_BUILD_VARIANT)),)
     include $(CLEAR_VARS)
     LOCAL_MODULE := adb_keys
+    LOCAL_LICENSE_KINDS := legacy_restricted
+    LOCAL_LICENSE_CONDITIONS := restricted
     LOCAL_MODULE_CLASS := ETC
     LOCAL_MODULE_PATH := $(TARGET_ROOT_OUT)
     LOCAL_PREBUILT_MODULE_FILE := $(PRODUCT_ADB_KEYS)
@@ -51,6 +57,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := otacerts
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_MODULE_STEM := otacerts.zip
 LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/security
@@ -65,6 +73,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := otacerts.recovery
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_MODULE_STEM := otacerts.zip
 LOCAL_MODULE_PATH := $(TARGET_RECOVERY_ROOT_OUT)/system/etc/security
diff --git a/target/product/sysconfig/Android.bp b/target/product/sysconfig/Android.bp
index 5632d17..29122e4 100644
--- a/target/product/sysconfig/Android.bp
+++ b/target/product/sysconfig/Android.bp
@@ -12,6 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 prebuilt_etc {
     name: "preinstalled-packages-platform-aosp-product.xml",
     product_specific: true,
@@ -30,4 +34,4 @@
     product_specific: true,
     sub_dir: "sysconfig",
     src: "preinstalled-packages-platform-handheld-product.xml",
-}
\ No newline at end of file
+}
diff --git a/tools/Android.bp b/tools/Android.bp
index 357ee2a..269e610 100644
--- a/tools/Android.bp
+++ b/tools/Android.bp
@@ -12,6 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "build_make_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    //   SPDX-license-identifier-BSD
+    //   SPDX-license-identifier-CC-BY
+    //   SPDX-license-identifier-GPL
+    //   SPDX-license-identifier-MIT
+    default_applicable_licenses: ["build_make_license"],
+}
+
 python_binary_host {
   name: "generate-self-extracting-archive",
   srcs: ["generate-self-extracting-archive.py"],
diff --git a/tools/acp/Android.bp b/tools/acp/Android.bp
index 64f5a10..78738b0 100644
--- a/tools/acp/Android.bp
+++ b/tools/acp/Android.bp
@@ -2,6 +2,15 @@
 //
 // Custom version of cp.
 
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "build_make_license"
+    // to get the below license kinds:
+    //   legacy_restricted
+    default_applicable_licenses: ["build_make_license"],
+}
+
 cc_binary_host {
 
     srcs: ["acp.c"],
diff --git a/tools/apicheck/Android.bp b/tools/apicheck/Android.bp
index 8fe20e9..f58042f 100644
--- a/tools/apicheck/Android.bp
+++ b/tools/apicheck/Android.bp
@@ -12,6 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 java_binary_host {
     name: "apicheck",
     wrapper: "etc/apicheck",
diff --git a/tools/atree/Android.bp b/tools/atree/Android.bp
index 5fbe042..7906d8b 100644
--- a/tools/atree/Android.bp
+++ b/tools/atree/Android.bp
@@ -2,6 +2,15 @@
 //
 // Copies files into the directory structure described by a manifest
 
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "build_make_license"
+    // to get the below license kinds:
+    //   legacy_restricted
+    default_applicable_licenses: ["build_make_license"],
+}
+
 cc_binary_host {
     name: "atree",
     srcs: [
diff --git a/tools/droiddoc/Android.bp b/tools/droiddoc/Android.bp
index 0428068..efd30c1 100644
--- a/tools/droiddoc/Android.bp
+++ b/tools/droiddoc/Android.bp
@@ -12,6 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "build_make_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    //   SPDX-license-identifier-BSD
+    //   SPDX-license-identifier-CC-BY
+    //   SPDX-license-identifier-GPL
+    //   SPDX-license-identifier-MIT
+    default_applicable_licenses: ["build_make_license"],
+}
+
 droiddoc_exported_dir {
     name: "droiddoc-templates-pdk",
     path: "templates-pdk",
diff --git a/tools/fs_config/Android.bp b/tools/fs_config/Android.bp
index 1dd5e4a..4544e07 100644
--- a/tools/fs_config/Android.bp
+++ b/tools/fs_config/Android.bp
@@ -12,6 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "build_make_license"
+    // to get the below license kinds:
+    //   legacy_restricted
+    default_applicable_licenses: ["build_make_license"],
+}
+
 bootstrap_go_package {
     name: "soong-fs_config",
     pkgPath: "android/soong/fs_config",
diff --git a/tools/fs_config/Android.mk b/tools/fs_config/Android.mk
index c338462..10d25e0 100644
--- a/tools/fs_config/Android.mk
+++ b/tools/fs_config/Android.mk
@@ -42,6 +42,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := fs_config_dirs
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_REQUIRED_MODULES := \
   fs_config_dirs_system \
   fs_config_dirs_system_ext \
@@ -55,6 +57,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := fs_config_files
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_REQUIRED_MODULES := \
   fs_config_files_system \
   fs_config_files_system_ext \
@@ -69,6 +73,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := fs_config_dirs_system_ext
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_REQUIRED_MODULES := $(if $(BOARD_USES_SYSTEM_EXTIMAGE)$(BOARD_SYSTEM_EXTIMAGE_FILE_SYSTEM_TYPE),_fs_config_dirs_system_ext)
 include $(BUILD_PHONY_PACKAGE)
 
@@ -79,6 +85,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := fs_config_files_system_ext
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_REQUIRED_MODULES := $(if $(BOARD_USES_SYSTEM_EXTIMAGE)$(BOARD_SYSTEM_EXTIMAGE_FILE_SYSTEM_TYPE),_fs_config_files_system_ext)
 include $(BUILD_PHONY_PACKAGE)
 
@@ -89,6 +97,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := fs_config_dirs_product
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_REQUIRED_MODULES := $(if $(BOARD_USES_PRODUCTIMAGE)$(BOARD_PRODUCTIMAGE_FILE_SYSTEM_TYPE),_fs_config_dirs_product)
 include $(BUILD_PHONY_PACKAGE)
 
@@ -99,6 +109,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := fs_config_files_product
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_REQUIRED_MODULES := $(if $(BOARD_USES_PRODUCTIMAGE)$(BOARD_PRODUCTIMAGE_FILE_SYSTEM_TYPE),_fs_config_files_product)
 include $(BUILD_PHONY_PACKAGE)
 
@@ -109,6 +121,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := fs_config_dirs_nonsystem
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_REQUIRED_MODULES := $(foreach t,$(fs_config_generate_extra_partition_list),_fs_config_dirs_$(t))
 include $(BUILD_PHONY_PACKAGE)
 
@@ -119,6 +133,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := fs_config_files_nonsystem
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_REQUIRED_MODULES := $(foreach t,$(fs_config_generate_extra_partition_list),_fs_config_files_$(t))
 include $(BUILD_PHONY_PACKAGE)
 
@@ -129,6 +145,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := fs_config_dirs_system
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_dirs
 include $(BUILD_SYSTEM)/base_rules.mk
@@ -154,6 +172,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := fs_config_files_system
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_files
 include $(BUILD_SYSTEM)/base_rules.mk
@@ -180,6 +200,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_dirs_vendor
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_dirs
 LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR)/etc
@@ -204,6 +226,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_files_vendor
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_files
 LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR)/etc
@@ -231,6 +255,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_dirs_oem
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_dirs
 LOCAL_MODULE_PATH := $(TARGET_OUT_OEM)/etc
@@ -255,6 +281,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_files_oem
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_files
 LOCAL_MODULE_PATH := $(TARGET_OUT_OEM)/etc
@@ -282,6 +310,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_dirs_odm
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_dirs
 LOCAL_MODULE_PATH := $(TARGET_OUT_ODM)/etc
@@ -306,6 +336,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_files_odm
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_files
 LOCAL_MODULE_PATH := $(TARGET_OUT_ODM)/etc
@@ -333,6 +365,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_dirs_vendor_dlkm
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_dirs
 LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR_DLKM)/etc
@@ -357,6 +391,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_files_vendor_dlkm
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_files
 LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR_DLKM)/etc
@@ -384,6 +420,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_dirs_odm_dlkm
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_dirs
 LOCAL_MODULE_PATH := $(TARGET_OUT_ODM_DLKM)/etc
@@ -408,6 +446,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_files_odm_dlkm
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_files
 LOCAL_MODULE_PATH := $(TARGET_OUT_ODM_DLKM)/etc
@@ -435,6 +475,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_dirs_product
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_dirs
 LOCAL_MODULE_PATH := $(TARGET_OUT_PRODUCT)/etc
@@ -459,6 +501,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_files_product
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_files
 LOCAL_MODULE_PATH := $(TARGET_OUT_PRODUCT)/etc
@@ -485,6 +529,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_dirs_system_ext
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_dirs
 LOCAL_MODULE_PATH := $(TARGET_OUT_SYSTEM_EXT)/etc
@@ -509,6 +555,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := _fs_config_files_system_ext
+LOCAL_LICENSE_KINDS := legacy_restricted
+LOCAL_LICENSE_CONDITIONS := restricted
 LOCAL_MODULE_CLASS := ETC
 LOCAL_INSTALLED_MODULE_STEM := fs_config_files
 LOCAL_MODULE_PATH := $(TARGET_OUT_SYSTEM_EXT)/etc
diff --git a/tools/fs_get_stats/Android.bp b/tools/fs_get_stats/Android.bp
index 67742b8..9457de4 100644
--- a/tools/fs_get_stats/Android.bp
+++ b/tools/fs_get_stats/Android.bp
@@ -1,3 +1,12 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "build_make_license"
+    // to get the below license kinds:
+    //   legacy_restricted
+    default_applicable_licenses: ["build_make_license"],
+}
+
 cc_binary_host {
     name: "fs_get_stats",
     srcs: ["fs_get_stats.c"],
diff --git a/tools/libhost/Android.bp b/tools/libhost/Android.bp
index 4c9100f..a83f2e7 100644
--- a/tools/libhost/Android.bp
+++ b/tools/libhost/Android.bp
@@ -1,3 +1,12 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "build_make_license"
+    // to get the below license kinds:
+    //   legacy_restricted
+    default_applicable_licenses: ["build_make_license"],
+}
+
 cc_library_host_static {
 
     srcs: ["CopyFile.c"],
diff --git a/tools/product_config/Android.bp b/tools/product_config/Android.bp
index 287ed5a..5fdbcf0 100644
--- a/tools/product_config/Android.bp
+++ b/tools/product_config/Android.bp
@@ -1,3 +1,7 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 java_defaults {
     name: "product-config-defaults",
     srcs: ["src/**/*.java"],
@@ -18,6 +22,6 @@
     static_libs: [
         "junit"
     ],
+    manifest: "TEST_MANIFEST.MF",
     test_suites: ["general-tests"]
 }
-
diff --git a/tools/product_config/TEST_MANIFEST.MF b/tools/product_config/TEST_MANIFEST.MF
new file mode 100644
index 0000000..287a77f
--- /dev/null
+++ b/tools/product_config/TEST_MANIFEST.MF
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: com.android.build.config.TestRunner
diff --git a/tools/product_config/src/com/android/build/config/CommandException.java b/tools/product_config/src/com/android/build/config/CommandException.java
new file mode 100644
index 0000000..f1a2c39
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/CommandException.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+/**
+ * Exception to indicate that a fatal error has occurred.  Throwing this
+ * will cause errors to be printed, cleanup to occur, and the command to
+ * exit with a failure code.
+ *
+ * These are user errors. Throwing other exceptions will result in
+ * the stack trace being shown.
+ */
+public class CommandException extends RuntimeException {
+    public CommandException() {
+        super();
+    }
+
+    public CommandException(String message) {
+        super(message);
+    }
+
+    public CommandException(String message, Throwable chain) {
+        super(message, chain);
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/ConfigBase.java b/tools/product_config/src/com/android/build/config/ConfigBase.java
new file mode 100644
index 0000000..9a81011
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/ConfigBase.java
@@ -0,0 +1,112 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Common parts between MakeConfig and the to-be-added GenericConfig, BazelConfig and SoongConfig.
+ */
+public class ConfigBase {
+    protected String mPhase;
+    protected List<String> mRootNodes;
+
+    /**
+     * State of the make varaible environment from before the first config file.
+     */
+    protected Map<String, Str> mInitialVariables = new HashMap();
+
+    /**
+     * State of the make varaible environment from after the first config file.
+     */
+    protected Map<String, Str> mFinalVariables = new HashMap();
+
+
+    /**
+     * The variables that are handled specially.
+     */
+    protected final TreeMap<String, VarType> mProductVars = new TreeMap();
+
+    public void setPhase(String phase) {
+        mPhase = phase;
+    }
+
+    public String getPhase() {
+        return mPhase;
+    }
+
+    public void setRootNodes(List<String> filenames) {
+        mRootNodes = new ArrayList(filenames);
+    }
+
+    public List<String> getRootNodes() {
+        return mRootNodes;
+    }
+
+    public void addProductVar(String name, VarType type) {
+        mProductVars.put(name, type);
+    }
+
+    public TreeMap<String, VarType> getProductVars() {
+        return mProductVars;
+    }
+
+    public VarType getVarType(String name) {
+        final VarType t = mProductVars.get(name);
+        if (t != null) {
+            return t;
+        } else {
+            return VarType.UNKNOWN;
+        }
+    }
+
+    public boolean isProductVar(String name) {
+        return mProductVars.get(name) != null;
+    }
+
+    /**
+     * Return the state the make variable environment from before the first config file.
+     */
+    public Map<String, Str> getInitialVariables() {
+        return mInitialVariables;
+    }
+
+    /**
+     * Return the state the make variable environment from before the first config file.
+     */
+    public Map<String, Str> getFinalVariables() {
+        return mFinalVariables;
+    }
+
+    /**
+     * Copy common base class fields from that to this.
+     */
+    public void copyFrom(ConfigBase that) {
+        setPhase(that.getPhase());
+        setRootNodes(that.getRootNodes());
+        for (Map.Entry<String, VarType> entry: that.getProductVars().entrySet()) {
+            addProductVar(entry.getKey(), entry.getValue());
+        }
+        mInitialVariables = new HashMap(that.getInitialVariables());
+        mFinalVariables = new HashMap(that.getFinalVariables());
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/ConvertMakeToGenericConfig.java b/tools/product_config/src/com/android/build/config/ConvertMakeToGenericConfig.java
new file mode 100644
index 0000000..ca31cd5
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/ConvertMakeToGenericConfig.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+package com.android.build.config;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Converts a MakeConfig into a Generic config by applying heuristics about
+ * the types of variable assignments that we do.
+ */
+public class ConvertMakeToGenericConfig {
+    private final Errors mErrors;
+
+    public ConvertMakeToGenericConfig(Errors errors) {
+        mErrors = errors;
+    }
+
+    public GenericConfig convert(MakeConfig make) {
+        final GenericConfig result = new GenericConfig();
+
+        // Base class fields
+        result.copyFrom(make);
+
+        // Each file
+        for (MakeConfig.ConfigFile f: make.getConfigFiles()) {
+            final GenericConfig.ConfigFile genericFile
+                    = new GenericConfig.ConfigFile(f.getFilename());
+            result.addConfigFile(genericFile);
+
+            final List<MakeConfig.Block> blocks = f.getBlocks();
+
+            // Some assertions:
+            // TODO: Include better context for these errors.
+            // There should always be at least a BEGIN and an AFTER, so assert this.
+            if (blocks.size() < 2) {
+                throw new RuntimeException("expected at least blocks.size() >= 2. Actcual size: "
+                        + blocks.size());
+            }
+            if (blocks.get(0).getBlockType() != MakeConfig.BlockType.BEFORE) {
+                throw new RuntimeException("expected first block to be BEFORE");
+            }
+            if (blocks.get(blocks.size() - 1).getBlockType() != MakeConfig.BlockType.AFTER) {
+                throw new RuntimeException("expected first block to be AFTER");
+            }
+            // Everything in between should be an INHERIT block.
+            for (int index = 1; index < blocks.size() - 1; index++) {
+                if (blocks.get(index).getBlockType() != MakeConfig.BlockType.INHERIT) {
+                    throw new RuntimeException("expected INHERIT at block " + index);
+                }
+            }
+
+            // Each block represents a snapshot of the interpreter variable state (minus a few big
+            // sets of variables which we don't export because they're used in the internals
+            // of node_fns.mk, so we know they're not necessary here). The first (BEFORE) one
+            // is everything that is set before the file is included, so it forms the base
+            // for everything else.
+            MakeConfig.Block prevBlock = blocks.get(0);
+
+            for (int index = 1; index < blocks.size(); index++) {
+                final MakeConfig.Block block = blocks.get(index);
+                for (final Map.Entry<String, Str> entry: block.getVars().entrySet()) {
+                    final String varName = entry.getKey();
+                    final GenericConfig.Assign assign = convertAssignment(block.getBlockType(),
+                            block.getInheritedFile(), make.getVarType(varName), varName,
+                            entry.getValue(), prevBlock.getVar(varName));
+                    if (assign != null) {
+                        genericFile.addStatement(assign);
+                    }
+                }
+                // Handle variables that are in prevBlock but not block -- they were
+                // deleted. Is this even possible, or do they show up as ""?  We will
+                // treat them as positive assigments to empty string
+                for (String prevName: prevBlock.getVars().keySet()) {
+                    if (!block.getVars().containsKey(prevName)) {
+                        genericFile.addStatement(
+                                new GenericConfig.Assign(prevName, new Str("")));
+                    }
+                }
+                if (block.getBlockType() == MakeConfig.BlockType.INHERIT) {
+                    genericFile.addStatement(
+                            new GenericConfig.Inherit(block.getInheritedFile()));
+                }
+                // For next iteration
+                prevBlock = block;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Converts one variable from a MakeConfig Block into a GenericConfig Assignment.
+     */
+    GenericConfig.Assign convertAssignment(MakeConfig.BlockType blockType, Str inheritedFile,
+            VarType varType, String varName, Str varVal, Str prevVal) {
+        if (prevVal == null) {
+            // New variable.
+            return new GenericConfig.Assign(varName, varVal);
+        } else if (!varVal.equals(prevVal)) {
+            // The value changed from the last block.
+            if (varVal.equals("")) {
+                // It was set to empty
+                return new GenericConfig.Assign(varName, varVal);
+            } else {
+                // Product vars have the @inherit processing. Other vars we
+                // will just ignore and put in one section at the end, based
+                // on the difference between the BEFORE and AFTER blocks.
+                if (varType == VarType.UNKNOWN) {
+                    if (blockType == MakeConfig.BlockType.AFTER) {
+                        // For UNKNOWN variables, we don't worry about the
+                        // intermediate steps, just take the final value.
+                        return new GenericConfig.Assign(varName, varVal);
+                    } else {
+                        return null;
+                    }
+                } else {
+                    return convertInheritedVar(blockType, inheritedFile,
+                            varName, varVal, prevVal);
+                }
+            }
+        } else {
+            // Variable not touched
+            return null;
+        }
+    }
+
+    /**
+     * Handle the special inherited values, where the inherit-product puts in the
+     * @inherit:... markers, adding Statements to the ConfigFile.
+     */
+    GenericConfig.Assign convertInheritedVar(MakeConfig.BlockType blockType, Str inheritedFile,
+            String varName, Str varVal, Str prevVal) {
+        String varText = varVal.toString();
+        String prevText = prevVal.toString().trim();
+        if (blockType == MakeConfig.BlockType.INHERIT) {
+            // inherit-product appends @inherit:... so drop that.
+            final String marker = "@inherit:" + inheritedFile;
+            if (varText.endsWith(marker)) {
+                varText = varText.substring(0, varText.length() - marker.length()).trim();
+            } else {
+                mErrors.ERROR_IMPROPER_PRODUCT_VAR_MARKER.add(varVal.getPosition(),
+                        "Variable didn't end with marker \"" + marker + "\": " + varText);
+            }
+        }
+
+        if (!varText.equals(prevText)) {
+            // If the variable value was actually changed.
+            final ArrayList<String> words = split(varText, prevText);
+            if (words.size() == 0) {
+                // Pure Assignment, none of the previous value is present.
+                return new GenericConfig.Assign(varName, new Str(varVal.getPosition(), varText));
+            } else {
+                // Self referential value (prepend, append, both).
+                if (words.size() > 2) {
+                    // This is indicative of a construction that might not be quite
+                    // what we want.  The above code will do something that works if it was
+                    // of the form "VAR := a $(VAR) b $(VAR) c", but if the original code
+                    // something else this won't work. This doesn't happen in AOSP, but
+                    // it's a theoretically possibility, so someone might do it.
+                    mErrors.WARNING_VARIABLE_RECURSION.add(varVal.getPosition(),
+                            "Possible unsupported variable recursion: "
+                                + varName + " = " + varVal + " (prev=" + prevVal + ")");
+                }
+                return new GenericConfig.Assign(varName, Str.toList(varVal.getPosition(), words));
+            }
+        } else {
+            // Variable not touched
+            return null;
+        }
+    }
+
+    /**
+     * Split 'haystack' on occurrences of 'needle'. Trims each string of whitespace
+     * to preserve make list semantics.
+     */
+    private static ArrayList<String> split(String haystack, String needle) {
+        final ArrayList<String> result = new ArrayList();
+        final int needleLen = needle.length();
+        if (needleLen == 0) {
+            return result;
+        }
+        int start = 0;
+        int end;
+        while ((end = haystack.indexOf(needle, start)) >= 0) {
+            result.add(haystack.substring(start, end).trim());
+            start = end + needleLen;
+        }
+        result.add(haystack.substring(start).trim());
+        return result;
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/CsvParser.java b/tools/product_config/src/com/android/build/config/CsvParser.java
new file mode 100644
index 0000000..1c8b9c3
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/CsvParser.java
@@ -0,0 +1,242 @@
+
+/*
+ * 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.
+ */
+
+
+package com.android.build.config;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A CSV parser.
+ */
+public class CsvParser {
+    /**
+     * Internal string buffer grows by this amount.
+     */
+    private static final int CHUNK_SIZE = 64 * 1024;
+
+    /**
+     * Error parsing.
+     */
+    public static class ParseException extends Exception {
+        private int mLine;
+        private int mColumn;
+
+        public ParseException(int line, int column, String message) {
+            super(message);
+            mLine = line;
+            mColumn = column;
+        }
+
+        /**
+         * Line number in source file.
+         */
+        public int getLine() {
+            return mLine;
+        }
+
+        /**
+         * Column in source file.
+         */
+        public int getColumn() {
+            return mColumn;
+        }
+    }
+
+    public static class Line {
+        private final int mLineNumber;
+        private final List<String> mFields;
+
+        Line(int lineno, List<String> fields) {
+            mLineNumber = lineno;
+            mFields = fields;
+        }
+
+        public int getLine() {
+            return mLineNumber;
+        }
+
+        public List<String> getFields() {
+            return mFields;
+        }
+    }
+
+    // Parser States
+    private static final int STATE_START_LINE = 0;
+    private static final int STATE_START_FIELD = 1;
+    private static final int STATE_INSIDE_QUOTED_FIELD = 2;
+    private static final int STATE_FIRST_QUOTATION_MARK = 3;
+    private static final int STATE_INSIDE_UNQUOTED_FIELD = 4;
+    private static final int STATE_DONE = 5;
+
+    // Parser Actions
+    private static final int ACTION_APPEND_CHAR = 1;
+    private static final int ACTION_FIELD_COMPLETE = 2;
+    private static final int ACTION_LINE_COMPLETE = 4;
+
+    /**
+     * Constructor.
+     */
+    private CsvParser() {
+    }
+
+    /**
+     * Reads CSV and returns a list of Line objects.
+     *
+     * Handles newlines inside fields quoted with double quotes (").
+     *
+     * Doesn't report blank lines, but does include empty fields.
+     */
+    public static List<Line> parse(Reader reader)
+            throws ParseException, IOException {
+        ArrayList<Line> result = new ArrayList();
+        int line = 1;
+        int column = 1;
+        int pos = 0;
+        char[] buf = new char[CHUNK_SIZE];
+        HashMap<String,String> stringPool = new HashMap();
+        ArrayList<String> fields = new ArrayList();
+
+        int state = STATE_START_LINE;
+        while (state != STATE_DONE) {
+            int c = reader.read();
+            int action = 0;
+
+            if (state == STATE_START_LINE) {
+                if (c <= 0) {
+                    // No data, skip ACTION_LINE_COMPLETE.
+                    state = STATE_DONE;
+                } else if (c == '"') {
+                    state = STATE_INSIDE_QUOTED_FIELD;
+                } else if (c == ',') {
+                    action = ACTION_FIELD_COMPLETE;
+                    state = STATE_START_FIELD;
+                } else if (c == '\n') {
+                    // Consume the newline, state stays STATE_START_LINE.
+                } else {
+                    action = ACTION_APPEND_CHAR;
+                    state = STATE_INSIDE_UNQUOTED_FIELD;
+                }
+            } else if (state == STATE_START_FIELD) {
+                if (c <= 0) {
+                    // Field will be empty
+                    action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE;
+                    state = STATE_DONE;
+                } else if (c == '"') {
+                    state = STATE_INSIDE_QUOTED_FIELD;
+                } else if (c == ',') {
+                    action = ACTION_FIELD_COMPLETE;
+                    state = STATE_START_FIELD;
+                } else if (c == '\n') {
+                    action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE;
+                    state = STATE_START_LINE;
+                } else {
+                    action = ACTION_APPEND_CHAR;
+                    state = STATE_INSIDE_UNQUOTED_FIELD;
+                }
+            } else if (state == STATE_INSIDE_QUOTED_FIELD) {
+                if (c <= 0) {
+                    throw new ParseException(line, column,
+                            "Bad input: End of input inside quoted field.");
+                } else if (c == '"') {
+                    state = STATE_FIRST_QUOTATION_MARK;
+                } else {
+                    action = ACTION_APPEND_CHAR;
+                }
+            } else if (state == STATE_FIRST_QUOTATION_MARK) {
+                if (c <= 0) {
+                    action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE;
+                    state = STATE_DONE;
+                } else if (c == '"') {
+                    action = ACTION_APPEND_CHAR;
+                    state = STATE_INSIDE_QUOTED_FIELD;
+                } else if (c == ',') {
+                    action = ACTION_FIELD_COMPLETE;
+                    state = STATE_START_FIELD;
+                } else if (c == '\n') {
+                    action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE;
+                    state = STATE_START_LINE;
+                } else {
+                    throw new ParseException(line, column,
+                            "Bad input: Character after field ended or unquoted '\"'.");
+                }
+            } else if (state == STATE_INSIDE_UNQUOTED_FIELD) {
+                if (c <= 0) {
+                    action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE;
+                    state = STATE_DONE;
+                } else if (c == ',') {
+                    action = ACTION_FIELD_COMPLETE;
+                    state = STATE_START_FIELD;
+                } else if (c == '\n') {
+                    action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE;
+                    state = STATE_START_LINE;
+                } else {
+                    action = ACTION_APPEND_CHAR;
+                }
+            }
+
+            if ((action & ACTION_APPEND_CHAR) != 0) {
+                // Reallocate buffer if necessary. Hopefully not often because CHUNK_SIZE is big.
+                if (pos >= buf.length) {
+                    char[] old = buf;
+                    buf = new char[old.length + CHUNK_SIZE];
+                    System.arraycopy(old, 0, buf, 0, old.length);
+                }
+                // Store the character
+                buf[pos] = (char)c;
+                pos++;
+            }
+            if ((action & ACTION_FIELD_COMPLETE) != 0) {
+                // A lot of the strings are duplicated, so pool them to reduce peak memory
+                // usage. This could be made slightly better by having a custom key class
+                // that does the lookup without making a new String that gets immediately
+                // thrown away.
+                String field = new String(buf, 0, pos);
+                final String cached = stringPool.get(field);
+                if (cached == null) {
+                    stringPool.put(field, field);
+                } else {
+                    field = cached;
+                }
+                fields.add(field);
+                pos = 0;
+            }
+            if ((action & ACTION_LINE_COMPLETE) != 0) {
+                // Only report lines with any contents
+                if (fields.size() > 0) {
+                    result.add(new Line(line, fields));
+                    fields = new ArrayList();
+                }
+            }
+
+            if (c == '\n') {
+                line++;
+                column = 1;
+            } else {
+                column++;
+            }
+        }
+
+        return result;
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/DumpConfigParser.java b/tools/product_config/src/com/android/build/config/DumpConfigParser.java
new file mode 100644
index 0000000..6da96c1
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/DumpConfigParser.java
@@ -0,0 +1,313 @@
+/*
+ * 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.
+ */
+
+
+package com.android.build.config;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Parses the output of ckati building build/make/core/dumpconfig.mk.
+ *
+ * The format is as follows:
+ *   - All processed lines are colon (':') separated fields.
+ *   - Lines before the dumpconfig_version line are dropped for forward compatibility
+ *   - Lines where the first field is config_var describe variables declared in makefiles
+ *     (implemented by the dump-config-vals macro)
+ *          Field   Description
+ *          0       "config_var" row type
+ *          1       Product makefile being processed
+ *          2       The variable name
+ *          3       The value of the variable
+ *          4       The location of the variable, as best tracked by kati
+ */
+public class DumpConfigParser {
+    private static final boolean DEBUG = true;
+
+    private final Errors mErrors;
+    private final String mFilename;
+    private final Reader mReader;
+
+    private final ArrayList<MakeConfig> mResults = new ArrayList();
+
+    private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s+");
+
+    /**
+     * Constructor.
+     */
+    private DumpConfigParser(Errors errors, String filename, Reader reader) {
+        mErrors = errors;
+        mFilename = filename;
+        mReader = reader;
+    }
+
+    /**
+     * Parse the text into a list of MakeConfig objects.
+     */
+    public static ArrayList<MakeConfig> parse(Errors errors, String filename, Reader reader)
+            throws CsvParser.ParseException, IOException {
+        DumpConfigParser parser = new DumpConfigParser(errors, filename, reader);
+        parser.parseImpl();
+        return parser.mResults;
+    }
+
+    /**
+     * Parse the input.
+     */
+    private void parseImpl() throws CsvParser.ParseException, IOException {
+        final List<CsvParser.Line> lines = CsvParser.parse(mReader);
+        final int lineCount = lines.size();
+        int index = 0;
+
+        int dumpconfigVersion = 0;
+
+        // Ignore lines until until we get a dumpconfig_version line for forward compatibility.
+        // In a previous life, this loop parsed from all of kati's stdout, not just the file
+        // that dumpconfig.mk writes, but it's harmless to leave this loop in.  It gives us a
+        // little bit of flexibility which we probably won't need anyway, this tool probably
+        // won't diverge from dumpconfig.mk anyway.
+        for (; index < lineCount; index++) {
+            final CsvParser.Line line = lines.get(index);
+            final List<String> fields = line.getFields();
+
+            if (matchLineType(line, "dumpconfig_version", 1)) {
+                try {
+                    dumpconfigVersion = Integer.parseInt(fields.get(1));
+                } catch (NumberFormatException ex) {
+                    mErrors.WARNING_DUMPCONFIG.add(
+                            new Position(mFilename, line.getLine()),
+                            "Couldn't parse dumpconfig_version: " + fields.get(1));
+                }
+                break;
+            }
+        }
+
+        // If we never saw dumpconfig_version, there's a problem with the command, so stop.
+        if (dumpconfigVersion == 0) {
+            mErrors.ERROR_DUMPCONFIG.fatal(
+                    new Position(mFilename),
+                    "Never saw a valid dumpconfig_version line.");
+        }
+
+        // Any lines before the start signal will be dropped. We create garbage objects
+        // here to avoid having to check for null everywhere.
+        MakeConfig makeConfig = new MakeConfig();
+        MakeConfig.ConfigFile configFile = new MakeConfig.ConfigFile("<ignored>");
+        MakeConfig.Block block = new MakeConfig.Block(MakeConfig.BlockType.UNSET);
+        Map<String, Str> initialVariables = new HashMap();
+        Map<String, Str> finalVariables = new HashMap();
+
+        // Number of "phases" we've seen so far.
+        for (; index < lineCount; index++) {
+            final CsvParser.Line line = lines.get(index);
+            final List<String> fields = line.getFields();
+            final String lineType = fields.get(0);
+
+            if (matchLineType(line, "phase", 2)) {
+                // Start the new one
+                makeConfig = new MakeConfig();
+                makeConfig.setPhase(fields.get(1));
+                makeConfig.setRootNodes(splitList(fields.get(2)));
+                mResults.add(makeConfig);
+                initialVariables = makeConfig.getInitialVariables();
+                finalVariables = makeConfig.getFinalVariables();
+
+                if (DEBUG) {
+                    System.out.println("PHASE:");
+                    System.out.println("  " + makeConfig.getPhase());
+                    System.out.println("  " + makeConfig.getRootNodes());
+                }
+            } else if (matchLineType(line, "var", 2)) {
+                final VarType type = "list".equals(fields.get(1)) ? VarType.LIST : VarType.SINGLE;
+                makeConfig.addProductVar(fields.get(2), type);
+
+                if (DEBUG) {
+                    System.out.println("  VAR: " + type + " " + fields.get(2));
+                }
+            } else if (matchLineType(line, "import", 1)) {
+                final List<String> importStack = splitList(fields.get(1));
+                if (importStack.size() == 0) {
+                    mErrors.WARNING_DUMPCONFIG.add(
+                            new Position(mFilename, line.getLine()),
+                            "'import' line with empty include stack.");
+                    continue;
+                }
+
+                // The beginning of importing a new file.
+                configFile = new MakeConfig.ConfigFile(importStack.get(0));
+                if (makeConfig.addConfigFile(configFile) != null) {
+                    mErrors.WARNING_DUMPCONFIG.add(
+                            new Position(mFilename, line.getLine()),
+                            "Duplicate file imported in section: " + configFile.getFilename());
+                }
+                // We expect a Variable block next.
+                block = new MakeConfig.Block(MakeConfig.BlockType.BEFORE);
+                configFile.addBlock(block);
+
+                if (DEBUG) {
+                    System.out.println("  IMPORT: " + configFile.getFilename());
+                }
+            } else if (matchLineType(line, "inherit", 2)) {
+                final String currentFile = fields.get(1);
+                final String inheritedFile = fields.get(2);
+                if (!configFile.getFilename().equals(currentFile)) {
+                    mErrors.WARNING_DUMPCONFIG.add(
+                            new Position(mFilename, line.getLine()),
+                            "Unexpected current file in 'inherit' line '" + currentFile
+                                + "' while processing '" + configFile.getFilename() + "'");
+                    continue;
+                }
+
+                // There is already a file in progress, so add another var block to that.
+                block = new MakeConfig.Block(MakeConfig.BlockType.INHERIT);
+                // TODO: Make dumpconfig.mk also output a Position for inherit-product
+                block.setInheritedFile(new Str(inheritedFile));
+                configFile.addBlock(block);
+
+                if (DEBUG) {
+                    System.out.println("  INHERIT: " + inheritedFile);
+                }
+            } else if (matchLineType(line, "imported", 1)) {
+                final List<String> importStack = splitList(fields.get(1));
+                if (importStack.size() == 0) {
+                    mErrors.WARNING_DUMPCONFIG.add(
+                            new Position(mFilename, line.getLine()),
+                            "'imported' line with empty include stack.");
+                    continue;
+                }
+                final String currentFile = importStack.get(0);
+                if (!configFile.getFilename().equals(currentFile)) {
+                    mErrors.WARNING_DUMPCONFIG.add(
+                            new Position(mFilename, line.getLine()),
+                            "Unexpected current file in 'imported' line '" + currentFile
+                                + "' while processing '" + configFile.getFilename() + "'");
+                    continue;
+                }
+
+                // There is already a file in progress, so add another var block to that.
+                // This will be the last one, but will check that after parsing.
+                block = new MakeConfig.Block(MakeConfig.BlockType.AFTER);
+                configFile.addBlock(block);
+
+                if (DEBUG) {
+                    System.out.println("  AFTER: " + currentFile);
+                }
+            } else if (matchLineType(line, "val", 5)) {
+                final String productMakefile = fields.get(1);
+                final String blockTypeString = fields.get(2);
+                final String varName = fields.get(3);
+                final String varValue = fields.get(4);
+                final Position pos = Position.parse(fields.get(5));
+                final Str str = new Str(pos, varValue);
+
+                if (blockTypeString.equals("initial")) {
+                    initialVariables.put(varName, str);
+                } else if (blockTypeString.equals("final")) {
+                    finalVariables.put(varName, str);
+                } else {
+                    if (!productMakefile.equals(configFile.getFilename())) {
+                        mErrors.WARNING_DUMPCONFIG.add(
+                                new Position(mFilename, line.getLine()),
+                                "Mismatched 'val' product makefile."
+                                    + " Expected: " + configFile.getFilename()
+                                    + " Saw: " + productMakefile);
+                        continue;
+                    }
+
+                    final MakeConfig.BlockType blockType = parseBlockType(line, blockTypeString);
+                    if (blockType == null) {
+                        continue;
+                    }
+                    if (blockType != block.getBlockType()) {
+                        mErrors.WARNING_DUMPCONFIG.add(
+                                new Position(mFilename, line.getLine()),
+                                "Mismatched 'val' block type."
+                                    + " Expected: " + block.getBlockType()
+                                    + " Saw: " + blockType);
+                    }
+
+                    // Add the variable to the block in progress
+                    block.addVar(varName, str);
+                }
+            } else {
+                if (DEBUG) {
+                    System.out.print("# ");
+                    for (int d = 0; d < fields.size(); d++) {
+                        System.out.print(fields.get(d));
+                        if (d != fields.size() - 1) {
+                            System.out.print(",");
+                        }
+                    }
+                    System.out.println();
+                }
+            }
+        }
+    }
+
+    /**
+     * Return true if the line type matches 'lineType' and there are at least 'fieldCount'
+     * fields (not including the first field which is the line type).
+     */
+    private boolean matchLineType(CsvParser.Line line, String lineType, int fieldCount) {
+        final List<String> fields = line.getFields();
+        if (!lineType.equals(fields.get(0))) {
+            return false;
+        }
+        if (fields.size() < (fieldCount + 1)) {
+            mErrors.WARNING_DUMPCONFIG.add(new Position(mFilename, line.getLine()),
+                    fields.get(0) + " line has " + fields.size() + " fields. Expected at least "
+                    + (fieldCount + 1) + " fields.");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Split a string with space separated items (i.e. the make list format) into a List<String>.
+     */
+    private static List<String> splitList(String text) {
+        // Arrays.asList returns a fixed-length List, so we copy it into an ArrayList to not
+        // propagate that surprise detail downstream.
+        return new ArrayList(Arrays.asList(LIST_SEPARATOR.split(text.trim())));
+    }
+
+    /**
+     * Parse a BockType or issue a warning if it can't be parsed.
+     */
+    private MakeConfig.BlockType parseBlockType(CsvParser.Line line, String text) {
+        if ("before".equals(text)) {
+            return MakeConfig.BlockType.BEFORE;
+        } else if ("inherit".equals(text)) {
+            return MakeConfig.BlockType.INHERIT;
+        } else if ("after".equals(text)) {
+            return MakeConfig.BlockType.AFTER;
+        } else {
+            mErrors.WARNING_DUMPCONFIG.add(
+                    new Position(mFilename, line.getLine()),
+                    "Invalid block type: " + text);
+            return null;
+        }
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/ErrorReporter.java b/tools/product_config/src/com/android/build/config/ErrorReporter.java
index f382b4e..5d87636 100644
--- a/tools/product_config/src/com/android/build/config/ErrorReporter.java
+++ b/tools/product_config/src/com/android/build/config/ErrorReporter.java
@@ -49,6 +49,16 @@
      */
     private boolean mHadError;
 
+    public static class FatalException extends RuntimeException {
+        FatalException(String message) {
+            super(message);
+        }
+
+        FatalException(String message, Throwable chain) {
+            super(message, chain);
+        }
+    }
+
     /**
      * Whether errors are errors, warnings or hidden.
      */
@@ -127,6 +137,35 @@
         public String getHelp() {
             return mHelp;
         }
+
+        /**
+         * Add an error with no source position.
+         */
+        public void add(String message) {
+            ErrorReporter.this.add(this, false, new Position(), message);
+        }
+
+        /**
+         * Add an error.
+         */
+        public void add(Position pos, String message) {
+            ErrorReporter.this.add(this, false, pos, message);
+        }
+
+        /**
+         * Add an error with no source position, and throw a FatalException, stopping processing
+         * immediately.
+         */
+        public void fatal(String message) {
+            ErrorReporter.this.add(this, true, new Position(), message);
+        }
+
+        /**
+         * Add an error, and throw a FatalException, stopping processing immediately.
+         */
+        public void fatal(Position pos, String message) {
+            ErrorReporter.this.add(this, true, pos, message);
+        }
     }
 
     /**
@@ -154,6 +193,13 @@
         public String getMessage() {
             return mMessage;
         }
+
+        @Override
+        public String toString() {
+            return mPosition
+                    + "[" + mCategory.getLevel().getLabel() + " " + mCategory.getCode() + "] "
+                    + mMessage;
+        }
     }
 
     private void initLocked() {
@@ -191,22 +237,16 @@
     }
 
     /**
-     * Add an error with no source position.
-     */
-    public void add(Category category, String message) {
-        add(category, new Position(), message);
-    }
-
-    /**
      * Add an error.
      */
-    public void add(Category category, Position pos, String message) {
+    private void add(Category category, boolean fatal, Position pos, String message) {
         synchronized (mEntries) {
             initLocked();
             if (mCategories.get(category.getCode()) != category) {
                 throw new RuntimeException("Errors.Category used from the wrong Errors object.");
             }
-            mEntries.add(new Entry(category, pos, message));
+            final Entry entry = new Entry(category, pos, message);
+            mEntries.add(entry);
             final Level level = category.getLevel();
             if (level == Level.WARNING || level == Level.ERROR) {
                 mHadWarningOrError = true;
@@ -214,6 +254,9 @@
             if (level == Level.ERROR) {
                 mHadError = true;
             }
+            if (fatal) {
+                throw new FatalException(entry.toString());
+            }
         }
     }
 
@@ -250,13 +293,10 @@
     public void printErrors(PrintStream out) {
         synchronized (mEntries) {
             for (Entry entry: mEntries) {
-                final Category category = entry.getCategory();
-                final Level level = category.getLevel();
-                if (level == Level.HIDDEN) {
+                if (entry.getCategory().getLevel() == Level.HIDDEN) {
                     continue;
                 }
-                out.println(entry.getPosition() + "[" + level.getLabel() + " "
-                        + category.getCode() + "] " + entry.getMessage());
+                out.println(entry.toString());
             }
         }
     }
diff --git a/tools/product_config/src/com/android/build/config/Errors.java b/tools/product_config/src/com/android/build/config/Errors.java
index 63792c8..92a4b30 100644
--- a/tools/product_config/src/com/android/build/config/Errors.java
+++ b/tools/product_config/src/com/android/build/config/Errors.java
@@ -30,7 +30,7 @@
  * <b>Naming Convention:</b>
  * <ul>
  *  <li>ERROR_ for Categories with isLevelSettable false and Level.ERROR
- *  <li>WARNING_ for Categories with isLevelSettable false and default WARNING or HIDDEN
+ *  <li>WARNING_ for Categories with isLevelSettable true and default WARNING or HIDDEN
  *  <li>Don't have isLevelSettable true and not ERROR. (The constructor asserts this).
  * </ul>
  */
@@ -42,4 +42,21 @@
     public final Category WARNING_UNKNOWN_COMMAND_LINE_ERROR = new Category(2, true, Level.HIDDEN,
             "Passing unknown errors on the command line.  Hidden by default for\n"
             + "forward compatibility.");
+
+    public final Category ERROR_KATI = new Category(3, false, Level.ERROR,
+            "Error executing or reading from Kati.");
+
+    public final Category WARNING_DUMPCONFIG = new Category(4, true, Level.WARNING,
+            "Anomaly parsing the output of kati and dumpconfig.mk.");
+
+    public final Category ERROR_DUMPCONFIG = new Category(5, false, Level.ERROR,
+            "Error parsing the output of kati and dumpconfig.mk.");
+
+    public final Category WARNING_VARIABLE_RECURSION = new Category(6, true, Level.WARNING,
+            "Possible unsupported variable recursion.");
+
+    // This could be a warning, but it's very likely that the data is corrupted somehow
+    // if we're seeing this.
+    public final Category ERROR_IMPROPER_PRODUCT_VAR_MARKER = new Category(7, true, Level.ERROR,
+            "Bad input from dumpvars causing corrupted product variables.");
 }
diff --git a/tools/product_config/src/com/android/build/config/GenericConfig.java b/tools/product_config/src/com/android/build/config/GenericConfig.java
new file mode 100644
index 0000000..2ee2735
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/GenericConfig.java
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Language-agnostic representation of a configuration statement.
+ */
+public class GenericConfig extends ConfigBase {
+    /**
+     * The config files that were imported in this config pass.
+     */
+    protected final TreeMap<String, ConfigFile> mConfigFiles = new TreeMap();
+
+    /**
+     * A configuration file.
+     */
+    public static class ConfigFile {
+        /**
+         * The name of the file, relative to the tree root.
+         */
+        private final String mFilename;
+
+        /**
+         * Sections of variable definitions and import statements. Product config
+         * files will always have at least one block.
+         */
+        private final ArrayList<Statement> mStatements = new ArrayList();
+
+        public ConfigFile(String filename) {
+            mFilename = filename;
+        }
+
+        public String getFilename() {
+            return mFilename;
+        }
+
+        public void addStatement(Statement statement) {
+            mStatements.add(statement);
+        }
+
+        public ArrayList<Statement> getStatements() {
+            return mStatements;
+        }
+    }
+
+    /**
+     * Base class for statements that appear in config files.
+     */
+    public static class Statement {
+    }
+
+    /**
+     * A variable assignment.
+     */
+    public static class Assign extends Statement {
+        private final String mVarName;
+        private final List<Str> mValue;
+
+        /**
+         * Assignment of a single value
+         */
+        public Assign(String varName, Str value) {
+            mVarName = varName;
+            mValue = new ArrayList();
+            mValue.add(value);
+        }
+
+        /**
+         * Assignment referencing a previous value.
+         *   VAR := $(1) $(VAR) $(2) $(VAR) $(3)
+         */
+        public Assign(String varName, List<Str> value) {
+            mVarName = varName;
+            mValue = value;
+        }
+
+        public String getName() {
+            return mVarName;
+        }
+
+        public List<Str> getValue() {
+            return mValue;
+        }
+    }
+
+    /**
+     * An $(inherit-product FILENAME) statement
+     */
+    public static class Inherit extends Statement {
+        private final Str mFilename;
+
+        public Inherit(Str filename) {
+            mFilename = filename;
+        }
+
+        public Str getFilename() {
+            return mFilename;
+        }
+    }
+
+    /**
+     * Adds the given config file. Returns any one previously added, or null.
+     */
+    public ConfigFile addConfigFile(ConfigFile file) {
+        return mConfigFiles.put(file.getFilename(), file);
+    }
+
+    public TreeMap<String, ConfigFile> getFiles() {
+        return mConfigFiles;
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/Kati.java b/tools/product_config/src/com/android/build/config/Kati.java
new file mode 100644
index 0000000..026ddb5
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/Kati.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+import java.util.List;
+
+/**
+ * Wrapper for invoking kati.
+ */
+public interface Kati {
+    public MakeConfig loadProductConfig();
+}
diff --git a/tools/product_config/src/com/android/build/config/KatiCommand.java b/tools/product_config/src/com/android/build/config/KatiCommand.java
new file mode 100644
index 0000000..f3c71d2
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/KatiCommand.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+import java.util.Arrays;
+import java.util.List;
+
+public interface KatiCommand {
+    public static class KatiException extends Exception {
+        private String mStderr;
+
+        public KatiException(List<String> cmd, String stderr) {
+            super("Error running kati: " + Arrays.toString(cmd.toArray()));
+            mStderr = stderr;
+        }
+
+        public String getStderr() {
+            return mStderr;
+        }
+    }
+
+    /**
+     * Run kati directly. Returns stdout data.
+     *
+     * @throws KatiException if there is an error. KatiException will contain
+     * the stderr from the kati invocation.
+     */
+    public String run(String[] args) throws KatiException;
+}
diff --git a/tools/product_config/src/com/android/build/config/KatiCommandImpl.java b/tools/product_config/src/com/android/build/config/KatiCommandImpl.java
new file mode 100644
index 0000000..53480d4
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/KatiCommandImpl.java
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.nio.charset.StandardCharsets;
+
+public class KatiCommandImpl implements KatiCommand {
+    final Errors mErrors;
+    final Options mOptions;
+
+    /**
+     * Runnable that consumes all of an InputStream until EOF, writes the contents
+     * into a StringBuilder, and then closes the stream.
+     */
+    class OutputReader implements Runnable {
+        private final InputStream mStream;
+        private final StringBuilder mOutput;
+
+        OutputReader(InputStream stream, StringBuilder output) {
+            mStream = stream;
+            mOutput = output;
+        }
+
+        @Override
+        public void run() {
+            final char[] buf = new char[16*1024];
+            final InputStreamReader reader = new InputStreamReader(mStream, StandardCharsets.UTF_8);
+            try {
+                int amt;
+                while ((amt = reader.read(buf, 0, buf.length)) >= 0) {
+                    mOutput.append(buf, 0, amt);
+                }
+            } catch (IOException ex) {
+                mErrors.ERROR_KATI.add("Error reading from kati: " + ex.getMessage());
+            } finally {
+                try {
+                    reader.close();
+                } catch (IOException ex) {
+                    // Close doesn't throw
+                }
+            }
+        }
+    }
+
+    public KatiCommandImpl(Errors errors, Options options) {
+        mErrors = errors;
+        mOptions = options;
+    }
+
+    /**
+     * Run kati directly. Returns stdout data.
+     *
+     * @throws KatiException if there is an error. KatiException will contain
+     * the stderr from the kati invocation.
+     */
+    public String run(String[] args) throws KatiException {
+        final ArrayList<String> cmd = new ArrayList();
+        cmd.add(mOptions.getCKatiBin());
+        for (String arg: args) {
+            cmd.add(arg);
+        }
+
+        final ProcessBuilder builder = new ProcessBuilder(cmd);
+        builder.redirectOutput(ProcessBuilder.Redirect.PIPE);
+        builder.redirectError(ProcessBuilder.Redirect.PIPE);
+
+        Process process = null;
+
+        try {
+            process = builder.start();
+        } catch (IOException ex) {
+            throw new KatiException(cmd, "IOException running process: " + ex.getMessage());
+        }
+
+        final StringBuilder stdout = new StringBuilder();
+        final Thread stdoutThread = new Thread(new OutputReader(process.getInputStream(), stdout),
+                "kati_stdout_reader");
+        stdoutThread.start();
+
+        final StringBuilder stderr = new StringBuilder();
+        final Thread stderrThread = new Thread(new OutputReader(process.getErrorStream(), stderr),
+                "kati_stderr_reader");
+        stderrThread.start();
+
+        int returnCode = waitForProcess(process);
+        joinThread(stdoutThread);
+        joinThread(stderrThread);
+
+        if (returnCode != 0) {
+            throw new KatiException(cmd, stderr.toString());
+        }
+
+        return stdout.toString();
+    }
+
+    /**
+     * Wrap Process.waitFor() because it throws InterruptedException.
+     */
+    private static int waitForProcess(Process proc) {
+        while (true) {
+            try {
+                return proc.waitFor();
+            } catch (InterruptedException ex) {
+            }
+        }
+    }
+
+    /**
+     * Wrap Thread.join() because it throws InterruptedException.
+     */
+    private static void joinThread(Thread thread) {
+        while (true) {
+            try {
+                thread.join();
+                return;
+            } catch (InterruptedException ex) {
+            }
+        }
+    }
+}
+
diff --git a/tools/product_config/src/com/android/build/config/KatiImpl.java b/tools/product_config/src/com/android/build/config/KatiImpl.java
new file mode 100644
index 0000000..feb374c
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/KatiImpl.java
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class KatiImpl implements Kati {
+    // Subdirectory inside out for config stuff.
+    private static final String CONFIG_SUBDIR = "config";
+
+    private final Errors mErrors;
+    private final Options mOptions;
+    private final KatiCommand mCommand;
+
+    // TODO: Do we need to consider the whole or a greater subset of the
+    // environment (or a hash of it?). In theory product-variant is enough, but we know
+    // people use stuff from the environment, even though we're trying to get rid of that.
+    private String getWorkDirPath() {
+        return Paths.get(mOptions.getOutDir(), CONFIG_SUBDIR,
+                mOptions.getProduct() + '-' + mOptions.getVariant()).toString();
+    }
+
+    private String getDumpConfigCsvPath() {
+        return Paths.get(getWorkDirPath(), "dumpconfig.csv").toString();
+    }
+
+    public KatiImpl(Errors errors, Options options) {
+        this(errors, options, new KatiCommandImpl(errors, options));
+    }
+
+    // VisibleForTesting
+    public KatiImpl(Errors errors, Options options, KatiCommand command) {
+        mErrors = errors;
+        mOptions = options;
+        mCommand = command;
+    }
+
+    @Override
+    public MakeConfig loadProductConfig() {
+        final String csvPath = getDumpConfigCsvPath();
+        try {
+            File workDir = new File(getWorkDirPath());
+
+            if (!workDir.mkdirs()) {
+                mErrors.ERROR_KATI.add("Unable to create directory: " + workDir);
+                return null; // TODO: throw exception?
+            }
+
+            System.out.println("running kati");
+            String out = mCommand.run(new String[] {
+                    "-f", "build/make/core/dumpconfig.mk",
+                    "DUMPCONFIG_FILE=" + csvPath
+                });
+
+            if (!out.contains("***DONE***")) {
+                mErrors.ERROR_KATI.add(
+                        "Unknown error with kati, but it didn't print ***DONE*** message");
+                return null; // TODO: throw exception?
+            }
+            // TODO: Check that output was good.
+        } catch (KatiCommand.KatiException ex) {
+            mErrors.ERROR_KATI.add("Error running kati:\n" + ex.getStderr());
+            return null;
+        }
+
+        if (!(new File(csvPath)).canRead()) {
+            mErrors.ERROR_KATI.add("Kati ran but did not create " + csvPath);
+            return null;
+        }
+
+        try (FileReader reader = new FileReader(csvPath)) {
+            System.out.println("csvPath=" + csvPath);
+            List<MakeConfig> makeConfigs = DumpConfigParser.parse(mErrors, csvPath, reader);
+
+            if (makeConfigs.size() == 0) {
+                // TODO: Issue error?
+                return null;
+            }
+
+            // TODO: There are multiple passes. That should be cleaned up in the make
+            // build system, but for now, the first one is the one we want.
+            return makeConfigs.get(0);
+        } catch (CsvParser.ParseException ex) {
+            mErrors.ERROR_KATI.add(new Position(csvPath, ex.getLine()),
+                    "Unable to parse output of dumpconfig.mk: " + ex.getMessage());
+            return null; // TODO: throw exception?
+        } catch (IOException ex) {
+            System.out.println(ex);
+            mErrors.ERROR_KATI.add("Unable to read " + csvPath + ": " + ex.getMessage());
+            return null; // TODO: throw exception?
+        }
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/Main.java b/tools/product_config/src/com/android/build/config/Main.java
index 7669742..7417fc7 100644
--- a/tools/product_config/src/com/android/build/config/Main.java
+++ b/tools/product_config/src/com/android/build/config/Main.java
@@ -16,6 +16,10 @@
 
 package com.android.build.config;
 
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+
 public class Main {
     private final Errors mErrors;
     private final Options mOptions;
@@ -31,6 +35,25 @@
         // TODO: Check the build environment to make sure we're running in a real
         // build environment, e.g. actually inside a source tree, with TARGET_PRODUCT
         // and TARGET_BUILD_VARIANT defined, etc.
+        Kati kati = new KatiImpl(mErrors, mOptions);
+        MakeConfig makeConfig = kati.loadProductConfig();
+        if (makeConfig == null || mErrors.hadError()) {
+            return;
+        }
+
+        System.out.println();
+        System.out.println("====================");
+        System.out.println("PRODUCT CONFIG FILES");
+        System.out.println("====================");
+        makeConfig.printToStream(System.out);
+
+        ConvertMakeToGenericConfig m2g = new ConvertMakeToGenericConfig(mErrors);
+        GenericConfig generic = m2g.convert(makeConfig);
+
+        System.out.println("======================");
+        System.out.println("REGENERATED MAKE FILES");
+        System.out.println("======================");
+        MakeWriter.write(System.out, generic, 0);
 
         // TODO: Run kati and extract the variables and convert all that into starlark files.
 
@@ -42,23 +65,39 @@
 
     public static void main(String[] args) {
         Errors errors = new Errors();
+        int exitCode = 0;
 
-        Options options = Options.parse(errors, args);
-        if (errors.hadError()) {
-            Options.printHelp(System.err);
+        try {
+            Options options = Options.parse(errors, args, System.getenv());
+            if (errors.hadError()) {
+                Options.printHelp(System.err);
+                System.err.println();
+                throw new CommandException();
+            }
+
+            switch (options.getAction()) {
+                case DEFAULT:
+                    (new Main(errors, options)).run();
+                    return;
+                case HELP:
+                    Options.printHelp(System.out);
+                    return;
+            }
+        } catch (CommandException | Errors.FatalException ex) {
+            // These are user errors, so don't show a stack trace
+            exitCode = 1;
+        } catch (Throwable ex) {
+            // These are programming errors in the code of this tool, so print the exception.
+            // We'll try to print this.  If it's something unrecoverable, then we'll hope
+            // for the best. We will still print the errors below, because they can be useful
+            // for debugging.
+            ex.printStackTrace(System.err);
             System.err.println();
+            exitCode = 1;
+        } finally {
+            // Print errors and warnings
             errors.printErrors(System.err);
-            System.exit(1);
         }
-
-        switch (options.getAction()) {
-            case DEFAULT:
-                (new Main(errors, options)).run();
-                errors.printErrors(System.err);
-                return;
-            case HELP:
-                Options.printHelp(System.out);
-                return;
-        }
+        System.exit(exitCode);
     }
 }
diff --git a/tools/product_config/src/com/android/build/config/MakeConfig.java b/tools/product_config/src/com/android/build/config/MakeConfig.java
new file mode 100644
index 0000000..dda0db9
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/MakeConfig.java
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class MakeConfig extends ConfigBase {
+    /**
+     * The config files that were imported in this config pass.
+     */
+    protected final ArrayList<ConfigFile> mConfigFiles = new ArrayList();
+
+    public enum BlockType {
+        UNSET,
+        BEFORE,
+        INHERIT,
+        AFTER
+    }
+
+    public static class ConfigFile {
+        /**
+         * The name of the file, relative to the tree root.
+         */
+        private final String mFilename;
+
+        /**
+         * Sections of variable definitions and import statements. Product config
+         * files will always have at least one block.
+         */
+        private final ArrayList<Block> mBlocks = new ArrayList();
+
+        public ConfigFile(String filename) {
+            mFilename = filename;
+        }
+
+        public String getFilename() {
+            return mFilename;
+        }
+
+        public void addBlock(Block block) {
+            mBlocks.add(block);
+        }
+
+        public ArrayList<Block> getBlocks() {
+            return mBlocks;
+        }
+    }
+
+    /**
+     * A set of variables that were defined.
+     */
+    public static class Block {
+        private final BlockType mBlockType;
+        private final TreeMap<String, Str> mValues = new TreeMap();
+        private Str mInheritedFile;
+
+        public Block(BlockType blockType) {
+            mBlockType = blockType;
+        }
+
+        public BlockType getBlockType() {
+            return mBlockType;
+        }
+
+        public void addVar(String varName, Str varValue) {
+            mValues.put(varName, varValue);
+        }
+
+        public Str getVar(String varName) {
+            return mValues.get(varName);
+        }
+
+        public TreeMap<String, Str> getVars() {
+            return mValues;
+        }
+
+        public void setInheritedFile(Str filename) {
+            mInheritedFile = filename;
+        }
+
+        public Str getInheritedFile() {
+            return mInheritedFile;
+        }
+    }
+
+    /**
+     * Adds the given config file. Returns any one previously added, or null.
+     */
+    public ConfigFile addConfigFile(ConfigFile file) {
+        ConfigFile prev = null;
+        for (ConfigFile f: mConfigFiles) {
+            if (f.getFilename().equals(file.getFilename())) {
+                prev = f;
+                break;
+            }
+        }
+        mConfigFiles.add(file);
+        return prev;
+    }
+
+    public List<ConfigFile> getConfigFiles() {
+        return mConfigFiles;
+    }
+
+    public void printToStream(PrintStream out) {
+        out.println("MakeConfig {");
+        out.println("  phase: " + mPhase);
+        out.println("  rootNodes: " + mRootNodes);
+        out.print("  singleVars: [ ");
+        for (Map.Entry<String,VarType> entry: mProductVars.entrySet()) {
+            if (entry.getValue() == VarType.SINGLE) {
+                out.print(entry.getKey());
+                out.print(" ");
+            }
+        }
+        out.println("]");
+        out.print("  listVars: [ ");
+        for (Map.Entry<String,VarType> entry: mProductVars.entrySet()) {
+            if (entry.getValue() == VarType.LIST) {
+                out.print(entry.getKey());
+                out.print(" ");
+            }
+        }
+        out.println("]");
+        out.println("  configFiles: [");
+        for (final ConfigFile configFile: mConfigFiles) {
+            out.println("    ConfigFile {");
+            out.println("      filename: " + configFile.getFilename());
+            out.println("      blocks: [");
+            for (Block block: configFile.getBlocks()) {
+                out.println("        Block {");
+                out.println("          type: " + block.getBlockType());
+                if (block.getBlockType() == BlockType.INHERIT) {
+                    out.println("          inherited: " + block.getInheritedFile());
+                }
+                out.println("          values: {");
+                for (Map.Entry<String,Str> var: block.getVars().entrySet()) {
+                    if (!var.getKey().equals("PRODUCT_PACKAGES")) {
+                        continue;
+                    }
+                    out.println("            " + var.getKey() + ": " + var.getValue());
+                }
+                out.println("          }");
+                out.println("        }");
+            }
+            out.println("      ]");
+            out.println("    }");
+        }
+        out.println("  ] // configFiles");
+        out.println("} // MakeConfig");
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/MakeWriter.java b/tools/product_config/src/com/android/build/config/MakeWriter.java
new file mode 100644
index 0000000..58dfcc0
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/MakeWriter.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+package com.android.build.config;
+
+import java.io.PrintStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class MakeWriter {
+    public static final int FLAG_WRITE_HEADER = 1;
+    public static final int FLAG_WRITE_ANNOTATIONS = 1 << 1;
+
+    private final boolean mWriteHeader;
+    private final boolean mWriteAnnotations;
+
+    public static void write(PrintStream out, GenericConfig config, int flags) {
+        (new MakeWriter(flags)).write(out, config);
+    }
+
+    private MakeWriter(int flags) {
+        mWriteHeader = (flags & FLAG_WRITE_HEADER) != 0;
+        mWriteAnnotations = (flags & FLAG_WRITE_ANNOTATIONS) != 0;
+    }
+
+    private void write(PrintStream out, GenericConfig config) {
+        for (GenericConfig.ConfigFile file: config.getFiles().values()) {
+            out.println("---------------------------------------------------------");
+            out.println("FILE: " + file.getFilename());
+            out.println("---------------------------------------------------------");
+            writeFile(out, config, file);
+            out.println();
+        }
+        out.println("---------------------------------------------------------");
+        out.println("VARIABLES TOUCHED BY MAKE BASED CONFIG:");
+        out.println("---------------------------------------------------------");
+        writeStrVars(out, getModifiedVars(config.getInitialVariables(),
+                                          config.getFinalVariables()), config);
+    }
+
+    private void writeFile(PrintStream out, GenericConfig config, GenericConfig.ConfigFile file) {
+        if (mWriteHeader) {
+            out.println("# This file is generated by the product_config tool");
+        }
+        for (GenericConfig.Statement statement: file.getStatements()) {
+            if (statement instanceof GenericConfig.Assign) {
+                writeAssign(out, config, (GenericConfig.Assign)statement);
+            } else if (statement instanceof GenericConfig.Inherit) {
+                writeInherit(out, (GenericConfig.Inherit)statement);
+            } else {
+                throw new RuntimeException("Unexpected Statement: " + statement);
+            }
+        }
+    }
+
+    private void writeAssign(PrintStream out, GenericConfig config,
+            GenericConfig.Assign statement) {
+        final List<Str> values = statement.getValue();
+        final int size = values.size();
+        final String varName = statement.getName();
+        Position pos = null;
+        if (size == 0) {
+            return;
+        } else if (size == 1) {
+            // Plain :=
+            final Str value = values.get(0);
+            out.print(varName + " := " + value);
+            pos = value.getPosition();
+        } else if (size == 2 && values.get(0).toString().length() == 0) {
+            // Plain +=
+            final Str value = values.get(1);
+            out.print(varName + " += " + value);
+            pos = value.getPosition();
+        } else {
+            // Write it out the long way
+            out.print(varName + " := " + values.get(0));
+            for (int i = 1; i < size; i++) {
+                out.print("$(" + varName + ") " + values.get(i));
+                pos = values.get(i).getPosition();
+            }
+        }
+        if (mWriteAnnotations) {
+            out.print("  # " + config.getVarType(varName) + " " + pos);
+        }
+        out.println();
+    }
+
+    private void writeInherit(PrintStream out, GenericConfig.Inherit statement) {
+        final Str filename = statement.getFilename();
+        out.print("$(call inherit-product " + filename + ")");
+        if (mWriteAnnotations) {
+            out.print("  # " + filename.getPosition());
+        }
+        out.println();
+    }
+
+    private static Map<String, Str> getModifiedVars(Map<String, Str> before,
+            Map<String, Str> after) {
+        final HashMap<String, Str> result = new HashMap();
+        // Entries that were added or changed.
+        for (Map.Entry<String, Str> afterEntry: after.entrySet()) {
+            final String varName = afterEntry.getKey();
+            final Str afterValue = afterEntry.getValue();
+            final Str beforeValue = before.get(varName);
+            if (beforeValue == null || !beforeValue.equals(afterValue)) {
+                result.put(varName, afterValue);
+            }
+        }
+        // removed Entries that were removed, we just treat them as  
+        for (Map.Entry<String, Str> beforeEntry: before.entrySet()) {
+            final String varName = beforeEntry.getKey();
+            if (!after.containsKey(varName)) {
+                result.put(varName, new Str(""));
+            }
+        }
+        return result;
+    }
+
+    private static class Var {
+        Var(String name, Str val) {
+            this.name = name;
+            this.val = val;
+        }
+        final String name;
+        final Str val;
+    }
+
+    private static void writeStrVars(PrintStream out, Map<String, Str> vars, ConfigBase config) {
+        // Sort by file name and var name
+        TreeMap<String, Var> sorted = new TreeMap();
+        for (Map.Entry<String, Str> entry: vars.entrySet()) {
+            sorted.put(entry.getValue().getPosition().toString() + " " + entry.getKey(),
+                    new Var(entry.getKey(), entry.getValue()));
+        }
+        // Print it
+        for (Var var: sorted.values()) {
+            out.println(var.val.getPosition() + var.name + " := " + var.val);
+        }
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/Options.java b/tools/product_config/src/com/android/build/config/Options.java
index 494b947..4e60484 100644
--- a/tools/product_config/src/com/android/build/config/Options.java
+++ b/tools/product_config/src/com/android/build/config/Options.java
@@ -17,6 +17,7 @@
 package com.android.build.config;
 
 import java.io.PrintStream;
+import java.util.Map;
 import java.util.TreeMap;
 
 public class Options {
@@ -27,19 +28,50 @@
 
     private Action mAction = Action.DEFAULT;
 
+    private String mProduct;
+    private String mVariant;
+    private String mOutDir;
+    private String mCKatiBin;
+
     public Action getAction() {
         return mAction;
     }
 
+    public String getProduct() {
+        return mProduct;
+    }
+
+    public String getVariant() {
+        return mVariant;
+    }
+
+    public String getOutDir() {
+        return mOutDir != null ? mOutDir : "out";
+    }
+
+    public String getCKatiBin() {
+        return mCKatiBin;
+    }
+
     public static void printHelp(PrintStream out) {
         out.println("usage: product_config");
         out.println();
-        out.println("OPTIONS");
+        out.println("REQUIRED FLAGS");
+        out.println("  --ckati_bin CKATI        Kati binary to use.");
+        out.println();
+        out.println("OPTIONAL FLAGS");
         out.println("  --hide ERROR_ID          Suppress this error.");
         out.println("  --error ERROR_ID         Make this ERROR_ID a fatal error.");
         out.println("  --help -h                This message.");
         out.println("  --warning ERROR_ID       Make this ERROR_ID a warning.");
         out.println();
+        out.println("REQUIRED ENVIRONMENT");
+        out.println("  TARGET_PRODUCT           Product to build from lunch command.");
+        out.println("  TARGET_BUILD_VARIANT     Build variant from lunch command.");
+        out.println();
+        out.println("OPTIONAL ENVIRONMENT");
+        out.println("  OUT_DIR                  Build output directory. Defaults to \"out\".");
+        out.println();
         out.println("ERRORS");
         out.println("  The following are the errors that can be controlled on the");
         out.println("  commandline with the --hide --warning --error flags.");
@@ -63,20 +95,26 @@
 
         private Errors mErrors;
         private String[] mArgs;
+        private Map<String,String> mEnv;
         private Options mResult = new Options();
         private int mIndex;
+        private boolean mSkipRequiredArgValidation;
 
-        public Parser(Errors errors, String[] args) {
+        public Parser(Errors errors, String[] args, Map<String,String> env) {
             mErrors = errors;
             mArgs = args;
+            mEnv = env;
         }
 
         public Options parse() {
+            // Args
             try {
                 while (mIndex < mArgs.length) {
                     final String arg = mArgs[mIndex];
 
-                    if ("--hide".equals(arg)) {
+                    if ("--ckati_bin".equals(arg)) {
+                        mResult.mCKatiBin = requireNextStringArg(arg);
+                    } else if ("--hide".equals(arg)) {
                         handleErrorCode(arg, Errors.Level.HIDDEN);
                     } else if ("--error".equals(arg)) {
                         handleErrorCode(arg, Errors.Level.ERROR);
@@ -96,14 +134,48 @@
                     mIndex++;
                 }
             } catch (ParseException ex) {
-                mErrors.add(mErrors.ERROR_COMMAND_LINE, ex.getMessage());
+                mErrors.ERROR_COMMAND_LINE.add(ex.getMessage());
             }
 
+            // Environment
+            mResult.mProduct = mEnv.get("TARGET_PRODUCT");
+            mResult.mVariant = mEnv.get("TARGET_BUILD_VARIANT");
+            mResult.mOutDir = mEnv.get("OUT_DIR");
+
+            validateArgs();
+
             return mResult;
         }
 
-        private void addWarning(Errors.Category category, String message) {
-            mErrors.add(category, message);
+        /**
+         * For testing; don't generate errors about missing arguments
+         */
+        public void setSkipRequiredArgValidation() {
+            mSkipRequiredArgValidation = true;
+        }
+
+        private void validateArgs() {
+            if (!mSkipRequiredArgValidation) {
+                if (mResult.mCKatiBin == null || "".equals(mResult.mCKatiBin)) {
+                    addMissingArgError("--ckati_bin");
+                }
+                if (mResult.mProduct == null) {
+                    addMissingEnvError("TARGET_PRODUCT");
+                }
+                if (mResult.mVariant == null) {
+                    addMissingEnvError("TARGET_BUILD_VARIANT");
+                }
+            }
+        }
+
+        private void addMissingArgError(String argName) {
+            mErrors.ERROR_COMMAND_LINE.add("Required command line argument missing: "
+                    + argName);
+        }
+
+        private void addMissingEnvError(String envName) {
+            mErrors.ERROR_COMMAND_LINE.add("Required environment variable missing: "
+                    + envName);
         }
 
         private String getNextNonFlagArg() {
@@ -117,6 +189,14 @@
             return mArgs[mIndex];
         }
 
+        private String requireNextStringArg(String arg) throws ParseException {
+            final String val = getNextNonFlagArg();
+            if (val == null) {
+                throw new ParseException(arg + " requires a string argument.");
+            }
+            return val;
+        }
+
         private int requireNextNumberArg(String arg) throws ParseException {
             final String val = getNextNonFlagArg();
             if (val == null) {
@@ -133,12 +213,11 @@
             final int code = requireNextNumberArg(arg);
             final Errors.Category category = mErrors.getCategories().get(code);
             if (category == null) {
-                mErrors.add(mErrors.WARNING_UNKNOWN_COMMAND_LINE_ERROR,
-                        "Unknown error code: " + code);
+                mErrors.WARNING_UNKNOWN_COMMAND_LINE_ERROR.add("Unknown error code: " + code);
                 return;
             }
             if (!category.isLevelSettable()) {
-                mErrors.add(mErrors.ERROR_COMMAND_LINE, "Can't set level for error " + code);
+                mErrors.ERROR_COMMAND_LINE.add("Can't set level for error " + code);
                 return;
             }
             category.setLevel(level);
@@ -152,7 +231,7 @@
      * <p>
      * Adds errors encountered to Errors object.
      */
-    public static Options parse(Errors errors, String[] args) {
-        return (new Parser(errors, args)).parse();
+    public static Options parse(Errors errors, String[] args, Map<String, String> env) {
+        return (new Parser(errors, args, env)).parse();
     }
 }
diff --git a/tools/product_config/src/com/android/build/config/Position.java b/tools/product_config/src/com/android/build/config/Position.java
index 7953942..266021d 100644
--- a/tools/product_config/src/com/android/build/config/Position.java
+++ b/tools/product_config/src/com/android/build/config/Position.java
@@ -16,6 +16,9 @@
 
 package com.android.build.config;
 
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 /**
  * Position in a source file.
  */
@@ -25,6 +28,9 @@
      */
     public static final int NO_LINE = -1;
 
+    private static final Pattern REGEX = Pattern.compile("([^:]*)(?::(\\d)*)?:?\\s*");
+    public static final String UNKNOWN = "<unknown>";
+
     private final String mFile;
     private final int mLine;
 
@@ -63,12 +69,39 @@
         return mLine;
     }
 
+    /**
+     * Return a Position object from a string containing <filename>:<line>, or the default
+     * Position(null, NO_LINE) if the string can't be parsed.
+     */
+    public static Position parse(String str) {
+        final Matcher m = REGEX.matcher(str);
+        if (!m.matches()) {
+            return new Position();
+        }
+        String filename = m.group(1);
+        if (filename.length() == 0 || UNKNOWN.equals(filename)) {
+            filename = null;
+        }
+        String lineString = m.group(2);
+        int line;
+        if (lineString == null || lineString.length() == 0) {
+            line = NO_LINE;
+        } else {
+            try {
+                line = Integer.parseInt(lineString);
+            } catch (NumberFormatException ex) {
+                line = NO_LINE;
+            }
+        }
+        return new Position(filename, line);
+    }
+
     @Override
     public String toString() {
       if (mFile == null && mLine == NO_LINE) {
         return "";
       } else if (mFile == null && mLine != NO_LINE) {
-        return "<unknown>:" + mLine + ": ";
+        return UNKNOWN + ":" + mLine + ": ";
       } else if (mFile != null && mLine == NO_LINE) {
         return mFile + ": ";
       } else { // if (mFile != null && mLine != NO_LINE)
diff --git a/tools/product_config/src/com/android/build/config/Str.java b/tools/product_config/src/com/android/build/config/Str.java
new file mode 100644
index 0000000..9c345a6
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/Str.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A String and a Position, where it came from in source code.
+ */
+public class Str {
+    private String mValue;
+    private Position mPosition;
+
+    public Str(String s) {
+        mValue = s;
+        mPosition = new Position();
+    }
+
+    public Str(Position pos, String s) {
+        mValue = s;
+        mPosition = pos;
+    }
+
+    @Override
+    public String toString() {
+        return mValue;
+    }
+
+    public Position getPosition() {
+        return mPosition;
+    }
+
+    /**
+     * Str is equal if the string value is equal, regardless of whether the position
+     * is the same.
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (o == null) {
+            return false;
+        } else if (o instanceof String) {
+            return mValue.equals(o);
+        } else if (o instanceof Str) {
+            final Str that = (Str)o;
+            return mValue.equals(that.mValue);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return mValue.hashCode();
+    }
+
+    public static ArrayList<Str> toList(Position pos, List<String> list) {
+        final ArrayList<Str> result = new ArrayList(list.size());
+        for (String s: list) {
+            result.add(new Str(pos, s));
+        }
+        return result;
+    }
+}
diff --git a/tools/product_config/src/com/android/build/config/VarType.java b/tools/product_config/src/com/android/build/config/VarType.java
new file mode 100644
index 0000000..43e9366
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/VarType.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+/**
+ * Whether a product config variable is a list or single-value variable.
+ */
+public enum VarType {
+    /**
+     * A product config variable that is a list of space separated strings.
+     * These are defined by _product_single_value_vars in product.mk.
+     */
+    LIST,
+
+    /**
+     * A product config varaible that is a single string.
+     * These are defined by _product_list_vars in product.mk.
+     */
+    SINGLE,
+
+    /**
+     * A variable that is given the special product config handling but is
+     * nonetheless defined by product config makefiles.
+     */
+    UNKNOWN
+}
+
diff --git a/tools/product_config/test/com/android/build/config/CsvParserTest.java b/tools/product_config/test/com/android/build/config/CsvParserTest.java
new file mode 100644
index 0000000..6f38d68
--- /dev/null
+++ b/tools/product_config/test/com/android/build/config/CsvParserTest.java
@@ -0,0 +1,148 @@
+
+/*
+ * 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.
+ */
+
+
+package com.android.build.config;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Test for CSV parser class.
+ */
+public class CsvParserTest {
+    public String listsToStrings(String[] expected, List<String> actual) {
+        return "expected=" + Arrays.toString(expected)
+                + " actual=" + Arrays.toString(actual.toArray());
+    }
+
+    public void assertLineEquals(CsvParser.Line actual, int lineno, String... fields) {
+        if (actual.getLine() != lineno) {
+            throw new RuntimeException("lineno mismatch: expected=" + lineno
+                    + " actual=" + actual.getLine());
+        }
+        if (fields.length != actual.getFields().size()) {
+            throw new RuntimeException("getFields().size() mismatch: expected=" + fields.length
+                    + " actual=" + actual.getFields().size()
+                    + " values: " + listsToStrings(fields, actual.getFields()));
+        }
+        for (int i = 0; i < fields.length; i++) {
+            if (!fields[i].equals(actual.getFields().get(i))) {
+                throw new RuntimeException("getFields().get(" + i + ") mismatch: expected="
+                        + fields[i] + " actual=" + actual.getFields().get(i)
+                        + " values: " + listsToStrings(fields, actual.getFields()));
+
+            }
+        }
+    }
+
+    @Test
+    public void testEmptyString() throws Exception {
+        List<CsvParser.Line> lines = CsvParser.parse(new StringReader(
+                    ""));
+
+        Assert.assertEquals(0, lines.size());
+    }
+
+    @Test
+    public void testLexerOneCharacter() throws Exception {
+        List<CsvParser.Line> lines = CsvParser.parse(new StringReader(
+                    "a"));
+
+        Assert.assertEquals(1, lines.size());
+        assertLineEquals(lines.get(0), 1, "a");
+    }
+
+    @Test
+    public void testLexerTwoFieldsNoNewline() throws Exception {
+        List<CsvParser.Line> lines = CsvParser.parse(new StringReader(
+                    "a,b"));
+
+        Assert.assertEquals(1, lines.size());
+        assertLineEquals(lines.get(0), 1, "a", "b");
+    }
+
+    @Test
+    public void testLexerTwoFieldsNewline() throws Exception {
+        List<CsvParser.Line> lines = CsvParser.parse(new StringReader(
+                    "a,b\n"));
+
+        Assert.assertEquals(1, lines.size());
+        assertLineEquals(lines.get(0), 1, "a", "b");
+    }
+
+    @Test
+    public void testEndsWithTwoNewlines() throws Exception {
+        List<CsvParser.Line> lines = CsvParser.parse(new StringReader(
+                    "a,b\n\n"));
+
+        Assert.assertEquals(1, lines.size());
+        assertLineEquals(lines.get(0), 1, "a", "b");
+    }
+
+    @Test
+    public void testOnlyNewlines() throws Exception {
+        List<CsvParser.Line> lines = CsvParser.parse(new StringReader(
+                    "\n\n\n\n"));
+
+        Assert.assertEquals(0, lines.size());
+    }
+
+
+    @Test
+    public void testLexerComplex() throws Exception {
+        List<CsvParser.Line> lines = CsvParser.parse(new StringReader(
+                    ",\"ab\"\"\nc\",,de\n"
+                    + "fg,\n"
+                    + "\n"
+                    + ",\n"
+                    + "hijk"));
+
+        Assert.assertEquals(4, lines.size());
+        assertLineEquals(lines.get(0), 2, "", "ab\"\nc", "", "de");
+        assertLineEquals(lines.get(1), 3, "fg", "");
+        assertLineEquals(lines.get(2), 5, "", "");
+        assertLineEquals(lines.get(3), 6, "hijk");
+    }
+
+    @Test
+    public void testEndInsideQuoted() throws Exception {
+        try {
+            List<CsvParser.Line> lines = CsvParser.parse(new StringReader(
+                        "\"asd"));
+            throw new RuntimeException("Didn't throw ParseException");
+        } catch (CsvParser.ParseException ex) {
+            System.out.println("Caught: " + ex);
+        }
+    }
+
+    @Test
+    public void testCharacterAfterQuotedField() throws Exception {
+        try {
+            List<CsvParser.Line> lines = CsvParser.parse(new StringReader(
+                        "\"\"a"));
+            throw new RuntimeException("Didn't throw ParseException");
+        } catch (CsvParser.ParseException ex) {
+            System.out.println("Caught: " + ex);
+        }
+    }
+}
+
diff --git a/tools/product_config/test/com/android/build/config/ErrorReporterTest.java b/tools/product_config/test/com/android/build/config/ErrorReporterTest.java
index 2cde476..b9b25b4 100644
--- a/tools/product_config/test/com/android/build/config/ErrorReporterTest.java
+++ b/tools/product_config/test/com/android/build/config/ErrorReporterTest.java
@@ -30,7 +30,7 @@
     public void testAdding() {
         TestErrors errors = new TestErrors();
 
-        errors.add(errors.ERROR, new Position("a", 12), "Errrororrrr");
+        errors.ERROR.add(new Position("a", 12), "Errrororrrr");
 
         Assert.assertTrue(errors.hadWarningOrError());
         Assert.assertTrue(errors.hadError());
@@ -66,7 +66,7 @@
     public void testWarning() {
         TestErrors errors = new TestErrors();
 
-        errors.add(errors.WARNING, "Waaaaarninggggg");
+        errors.WARNING.add("Waaaaarninggggg");
 
         Assert.assertTrue(errors.hadWarningOrError());
         Assert.assertFalse(errors.hadError());
@@ -80,7 +80,7 @@
     public void testHidden() {
         TestErrors errors = new TestErrors();
 
-        errors.add(errors.HIDDEN, "Hidddeennn");
+        errors.HIDDEN.add("Hidddeennn");
 
         Assert.assertFalse(errors.hadWarningOrError());
         Assert.assertFalse(errors.hadError());
diff --git a/tools/product_config/test/com/android/build/config/OptionsTest.java b/tools/product_config/test/com/android/build/config/OptionsTest.java
index 2c36322..459efa5 100644
--- a/tools/product_config/test/com/android/build/config/OptionsTest.java
+++ b/tools/product_config/test/com/android/build/config/OptionsTest.java
@@ -19,12 +19,24 @@
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.util.HashMap;
+
 public class OptionsTest {
+
+    private Options parse(Errors errors, String[] args) {
+        final HashMap<String, String> env = new HashMap();
+        env.put("TARGET_PRODUCT", "test_product");
+        env.put("TARGET_BUILD_VARIANT", "user");
+        final Options.Parser parser = new Options.Parser(errors, args, env);
+        parser.setSkipRequiredArgValidation();
+        return parser.parse();
+    }
+
     @Test
     public void testErrorMissingLast() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--error"
                 });
 
@@ -37,7 +49,7 @@
     public void testErrorMissingNotLast() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--error", "--warning", "2"
                 });
 
@@ -50,7 +62,7 @@
     public void testErrorNotNumeric() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--error", "notgood"
                 });
 
@@ -63,7 +75,7 @@
     public void testErrorInvalidError() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--error", "50000"
                 });
 
@@ -76,7 +88,7 @@
     public void testErrorOne() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--error", "2"
                 });
 
@@ -89,7 +101,7 @@
     public void testWarningOne() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--warning", "2"
                 });
 
@@ -102,7 +114,7 @@
     public void testHideOne() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--hide", "2"
                 });
 
@@ -110,5 +122,16 @@
         Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
         Assert.assertFalse(errors.hadWarningOrError());
     }
+
+    @Test
+    public void testEnv() {
+        final Errors errors = new Errors();
+
+        final Options options = parse(errors, new String[0]);
+
+        Assert.assertEquals("test_product", options.getProduct());
+        Assert.assertEquals("user", options.getVariant());
+        Assert.assertFalse(errors.hadWarningOrError());
+    }
 }
 
diff --git a/tools/product_config/test/com/android/build/config/PositionTest.java b/tools/product_config/test/com/android/build/config/PositionTest.java
new file mode 100644
index 0000000..82b5dd4
--- /dev/null
+++ b/tools/product_config/test/com/android/build/config/PositionTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashMap;
+
+public class PositionTest {
+
+    @Test
+    public void testParseEmpty() {
+        final Position pos = Position.parse("");
+
+        Assert.assertEquals(null, pos.getFile());
+        Assert.assertEquals(Position.NO_LINE, pos.getLine());
+    }
+
+    @Test
+    public void testParseOnlyFile() {
+        final Position pos = Position.parse("asdf");
+
+        Assert.assertEquals("asdf", pos.getFile());
+        Assert.assertEquals(Position.NO_LINE, pos.getLine());
+    }
+
+    @Test
+    public void testParseBoth() {
+        final Position pos = Position.parse("asdf:1");
+
+        Assert.assertEquals("asdf", pos.getFile());
+        Assert.assertEquals(1, pos.getLine());
+    }
+
+    @Test
+    public void testParseEndsWithColon() {
+        final Position pos = Position.parse("asdf:");
+
+        Assert.assertEquals("asdf", pos.getFile());
+        Assert.assertEquals(Position.NO_LINE, pos.getLine());
+    }
+
+    @Test
+    public void testParseEndsWithSpace() {
+        final Position pos = Position.parse("asdf: ");
+
+        Assert.assertEquals("asdf", pos.getFile());
+        Assert.assertEquals(Position.NO_LINE, pos.getLine());
+    }
+
+
+}
+
diff --git a/tools/product_config/test/com/android/build/config/TestRunner.java b/tools/product_config/test/com/android/build/config/TestRunner.java
new file mode 100644
index 0000000..546518f
--- /dev/null
+++ b/tools/product_config/test/com/android/build/config/TestRunner.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+package com.android.build.config;
+
+import org.junit.runner.Description;
+import org.junit.runner.JUnitCore;
+import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+
+public class TestRunner {
+    public static void main(String[] args) {
+        JUnitCore junit = new JUnitCore();
+
+        junit.addListener(new RunListener() {
+                    @Override
+                    public void testStarted(Description description) {
+                        System.out.println("\nSTARTING: " + description.getDisplayName());
+                    }
+
+                    @Override
+                    public void testFailure(Failure failure) {
+                        System.out.println("FAILED: "
+                                + failure.getDescription().getDisplayName());
+                        System.out.println(failure.getTrace());
+                    }
+                });
+        Result result = junit.run(CsvParserTest.class,
+                                  ErrorReporterTest.class,
+                                  OptionsTest.class,
+                                  PositionTest.class);
+        if (!result.wasSuccessful()) {
+            System.out.println("\n*** FAILED ***");
+        }
+    }
+}
+
diff --git a/tools/releasetools/Android.bp b/tools/releasetools/Android.bp
index 2429f17..81528ae 100644
--- a/tools/releasetools/Android.bp
+++ b/tools/releasetools/Android.bp
@@ -22,6 +22,10 @@
 //    `releasetools_X_defaults` in their defaults.
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 python_defaults {
     name: "releasetools_add_img_to_target_files_defaults",
     srcs: [
diff --git a/tools/releasetools/add_img_to_target_files.py b/tools/releasetools/add_img_to_target_files.py
index 7839b47..5f9f19a 100644
--- a/tools/releasetools/add_img_to_target_files.py
+++ b/tools/releasetools/add_img_to_target_files.py
@@ -64,7 +64,7 @@
 import verity_utils
 import ota_metadata_pb2
 
-from apex_utils import GetApexInfoFromTargetFiles
+from apex_utils import GetSystemApexInfoFromTargetFiles
 
 if sys.hexversion < 0x02070000:
   print("Python 2.7 or newer is required.", file=sys.stderr)
@@ -757,7 +757,7 @@
                            "{}.img".format(partition_name))))
 
 def AddApexInfo(output_zip):
-  apex_infos = GetApexInfoFromTargetFiles(OPTIONS.input_tmp)
+  apex_infos = GetSystemApexInfoFromTargetFiles(OPTIONS.input_tmp)
   apex_metadata_proto = ota_metadata_pb2.ApexMetadata()
   apex_metadata_proto.apex_info.extend(apex_infos)
   apex_info_bytes = apex_metadata_proto.SerializeToString()
diff --git a/tools/releasetools/apex_utils.py b/tools/releasetools/apex_utils.py
index 644b92a..1c88053 100644
--- a/tools/releasetools/apex_utils.py
+++ b/tools/releasetools/apex_utils.py
@@ -516,7 +516,7 @@
     raise ApexInfoError(
         'Failed to get type for {}:\n{}'.format(apex_file, e))
 
-def GetApexInfoFromTargetFiles(input_file):
+def GetSystemApexInfoFromTargetFiles(input_file):
   """
   Get information about system APEX stored in the input_file zip
 
@@ -538,6 +538,11 @@
     tmp_dir = UnzipTemp(input_file, ["SYSTEM/apex/*"])
   target_dir = os.path.join(tmp_dir, "SYSTEM/apex/")
 
+  # Partial target-files packages for vendor-only builds may not contain
+  # a system apex directory.
+  if not os.path.exists(target_dir):
+    return []
+
   apex_infos = []
 
   debugfs_path = "debugfs"
diff --git a/tools/releasetools/ota_from_target_files.py b/tools/releasetools/ota_from_target_files.py
index 4c69e69..2cbaf37 100755
--- a/tools/releasetools/ota_from_target_files.py
+++ b/tools/releasetools/ota_from_target_files.py
@@ -273,6 +273,7 @@
 OPTIONS.partial = None
 OPTIONS.custom_images = {}
 OPTIONS.disable_vabc = False
+OPTIONS.spl_downgrade = False
 
 POSTINSTALL_CONFIG = 'META/postinstall_config.txt'
 DYNAMIC_PARTITION_INFO = 'META/dynamic_partitions_info.txt'
@@ -291,6 +292,8 @@
     'system_ext', 'vbmeta', 'vbmeta_system', 'vbmeta_vendor', 'vendor',
     'vendor_boot']
 
+SECURITY_PATCH_LEVEL_PROP_NAME = "ro.build.version.security_patch"
+
 
 class PayloadSigner(object):
   """A class that wraps the payload signing works.
@@ -1262,6 +1265,8 @@
       OPTIONS.custom_images[custom_partition] = custom_image
     elif o == "--disable_vabc":
       OPTIONS.disable_vabc = True
+    elif o == "--spl_downgrade":
+      OPTIONS.spl_downgrade = True
     else:
       return False
     return True
@@ -1304,6 +1309,7 @@
                                  "partial=",
                                  "custom_image=",
                                  "disable_vabc",
+                                 "spl_downgrade"
                              ], extra_option_handler=option_handler)
 
   if len(args) != 2:
@@ -1399,7 +1405,27 @@
           "build/make/target/product/security/testkey")
     # Get signing keys
     OPTIONS.key_passwords = common.GetKeyPasswords([OPTIONS.package_key])
+    private_key_path = OPTIONS.package_key + OPTIONS.private_key_suffix
+    if not os.path.exists(private_key_path):
+      raise common.ExternalError(
+                        "Private key {} doesn't exist. Make sure you passed the"
+                        " correct key path through -k option".format(
+                          private_key_path)
+                          )
 
+  if OPTIONS.source_info_dict:
+    source_build_prop = OPTIONS.source_info_dict["build.prop"]
+    target_build_prop = OPTIONS.target_info_dict["build.prop"]
+    source_spl = source_build_prop.GetProp(SECURITY_PATCH_LEVEL_PROP_NAME)
+    target_spl = target_build_prop.GetProp(SECURITY_PATCH_LEVEL_PROP_NAME)
+    if target_spl < source_spl and not OPTIONS.spl_downgrade:
+      raise common.ExternalError(
+        "Target security patch level {} is older than source SPL {} applying "
+        "such OTA will likely cause device fail to boot. Pass --spl-downgrade "
+        "to override this check. This script expects security patch level to "
+        "be in format yyyy-mm-dd (e.x. 2021-02-05). It's possible to use "
+        "separators other than -, so as long as it's used consistenly across "
+        "all SPL dates".format(target_spl, source_spl))
   if generate_ab:
     GenerateAbOtaPackage(
         target_file=args[0],
diff --git a/tools/releasetools/sign_target_files_apks.py b/tools/releasetools/sign_target_files_apks.py
index 176e258..05a085b 100755
--- a/tools/releasetools/sign_target_files_apks.py
+++ b/tools/releasetools/sign_target_files_apks.py
@@ -329,9 +329,8 @@
   """
   unknown_files = []
   for info in input_tf_zip.infolist():
-    # Handle APEXes first, e.g. SYSTEM/apex/com.android.tzdata.apex.
-    if (info.filename.startswith('SYSTEM/apex') and
-        info.filename.endswith('.apex')):
+    # Handle APEXes on all partitions
+    if info.filename.endswith('.apex'):
       name = os.path.basename(info.filename)
       if name not in known_keys:
         unknown_files.append(name)
@@ -363,8 +362,7 @@
 
   invalid_apexes = []
   for info in input_tf_zip.infolist():
-    if (not info.filename.startswith('SYSTEM/apex') or
-        not info.filename.endswith('.apex')):
+    if not info.filename.endswith('.apex'):
       continue
 
     name = os.path.basename(info.filename)
@@ -445,6 +443,25 @@
   return data
 
 
+def IsBuildPropFile(filename):
+  return filename in (
+        "SYSTEM/etc/prop.default",
+        "BOOT/RAMDISK/prop.default",
+        "RECOVERY/RAMDISK/prop.default",
+
+        "VENDOR_BOOT/RAMDISK/default.prop",
+        "VENDOR_BOOT/RAMDISK/prop.default",
+
+        # ROOT/default.prop is a legacy path, but may still exist for upgrading
+        # devices that don't support `property_overrides_split_enabled`.
+        "ROOT/default.prop",
+
+        # RECOVERY/RAMDISK/default.prop is a legacy path, but will always exist
+        # as a symlink in the current code. So it's a no-op here. Keeping the
+        # path here for clarity.
+        "RECOVERY/RAMDISK/default.prop") or filename.endswith("build.prop")
+
+
 def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info,
                        apk_keys, apex_keys, key_passwords,
                        platform_api_level, codename_to_api_level_map,
@@ -497,8 +514,8 @@
             "        (skipped due to special cert string)" % (name,))
         common.ZipWriteStr(output_tf_zip, out_info, data)
 
-    # Sign bundled APEX files.
-    elif filename.startswith("SYSTEM/apex") and filename.endswith(".apex"):
+    # Sign bundled APEX files on all partitions
+    elif filename.endswith(".apex"):
       name = os.path.basename(filename)
       payload_key, container_key = apex_keys[name]
 
@@ -528,39 +545,8 @@
             "        (skipped due to special cert string)" % (name,))
         common.ZipWriteStr(output_tf_zip, out_info, data)
 
-    # AVB public keys for the installed APEXes, which will be updated later.
-    elif (os.path.dirname(filename) == 'SYSTEM/etc/security/apex' and
-          filename != 'SYSTEM/etc/security/apex/'):
-      continue
-
     # System properties.
-    elif filename in (
-        "SYSTEM/build.prop",
-
-        "VENDOR/build.prop",
-        "SYSTEM/vendor/build.prop",
-
-        "ODM/etc/build.prop",
-        "VENDOR/odm/etc/build.prop",
-
-        "PRODUCT/build.prop",
-        "SYSTEM/product/build.prop",
-
-        "SYSTEM_EXT/build.prop",
-        "SYSTEM/system_ext/build.prop",
-
-        "SYSTEM/etc/prop.default",
-        "BOOT/RAMDISK/prop.default",
-        "RECOVERY/RAMDISK/prop.default",
-
-        # ROOT/default.prop is a legacy path, but may still exist for upgrading
-        # devices that don't support `property_overrides_split_enabled`.
-        "ROOT/default.prop",
-
-        # RECOVERY/RAMDISK/default.prop is a legacy path, but will always exist
-        # as a symlink in the current code. So it's a no-op here. Keeping the
-        # path here for clarity.
-        "RECOVERY/RAMDISK/default.prop"):
+    elif IsBuildPropFile(filename):
       print("Rewriting %s:" % (filename,))
       if stat.S_ISLNK(info.external_attr >> 16):
         new_data = data
@@ -588,12 +574,7 @@
 
     # Don't copy OTA certs if we're replacing them.
     # Replacement of update-payload-key.pub.pem was removed in b/116660991.
-    elif (
-        OPTIONS.replace_ota_keys and
-        filename in (
-            "BOOT/RAMDISK/system/etc/security/otacerts.zip",
-            "RECOVERY/RAMDISK/system/etc/security/otacerts.zip",
-            "SYSTEM/etc/security/otacerts.zip")):
+    elif OPTIONS.replace_ota_keys and filename.endswith("/otacerts.zip"):
       pass
 
     # Skip META/misc_info.txt since we will write back the new values later.
@@ -861,21 +842,12 @@
     print("META/otakeys.txt has no keys; using %s for OTA package"
           " verification." % (mapped_keys[0],))
 
-  # recovery now uses the same x509.pem version of the keys.
-  # extra_recovery_keys are used only in recovery.
-  if misc_info.get("recovery_as_boot") == "true":
-    recovery_keys_location = "BOOT/RAMDISK/system/etc/security/otacerts.zip"
-  else:
-    recovery_keys_location = "RECOVERY/RAMDISK/system/etc/security/otacerts.zip"
-
-  WriteOtacerts(output_tf_zip, recovery_keys_location,
-                mapped_keys + extra_recovery_keys)
-
-  # SystemUpdateActivity uses the x509.pem version of the keys, but
-  # put into a zipfile system/etc/security/otacerts.zip.
-  # We DO NOT include the extra_recovery_keys (if any) here.
-  WriteOtacerts(output_tf_zip, "SYSTEM/etc/security/otacerts.zip", mapped_keys)
-
+  otacerts = [info
+              for info in input_tf_zip.infolist()
+              if info.filename.endswith("/otacerts.zip")]
+  for info in otacerts:
+    print("Rewriting OTA key:", info.filename, mapped_keys)
+    WriteOtacerts(output_tf_zip, info.filename, mapped_keys)
 
 
 def ReplaceVerityPublicKey(output_zip, filename, key_path):
diff --git a/tools/releasetools/test_ota_from_target_files.py b/tools/releasetools/test_ota_from_target_files.py
index b556b3a..8266908 100644
--- a/tools/releasetools/test_ota_from_target_files.py
+++ b/tools/releasetools/test_ota_from_target_files.py
@@ -33,7 +33,7 @@
     GetTargetFilesZipWithoutPostinstallConfig,
     Payload, PayloadSigner, POSTINSTALL_CONFIG,
     StreamingPropertyFiles, AB_PARTITIONS)
-from apex_utils import GetApexInfoFromTargetFiles
+from apex_utils import GetSystemApexInfoFromTargetFiles
 from test_utils import PropertyFilesTestCase
 
 
@@ -281,9 +281,9 @@
         metadata)
 
   @test_utils.SkipIfExternalToolsUnavailable()
-  def test_GetApexInfoFromTargetFiles(self):
+  def test_GetSystemApexInfoFromTargetFiles(self):
     target_files = construct_target_files(compressedApex=True)
-    apex_infos = GetApexInfoFromTargetFiles(target_files)
+    apex_infos = GetSystemApexInfoFromTargetFiles(target_files)
     self.assertEqual(len(apex_infos), 1)
     self.assertEqual(apex_infos[0].package_name, "com.android.apex.compressed")
     self.assertEqual(apex_infos[0].version, 1)
diff --git a/tools/signapk/Android.bp b/tools/signapk/Android.bp
index b90f010..bee6a6f 100644
--- a/tools/signapk/Android.bp
+++ b/tools/signapk/Android.bp
@@ -16,6 +16,10 @@
 
 // the signapk tool (a .jar application used to sign packages)
 // ============================================================
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 java_binary_host {
     name: "signapk",
     srcs: ["src/**/*.java"],
diff --git a/tools/signtos/Android.bp b/tools/signtos/Android.bp
index 688e7b8..cd41acc 100644
--- a/tools/signtos/Android.bp
+++ b/tools/signtos/Android.bp
@@ -16,6 +16,10 @@
 
 // the signtos tool - signs Trusty images
 // ============================================================
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 java_library_host {
     name: "signtos",
     srcs: ["SignTos.java"],
diff --git a/tools/zipalign/Android.bp b/tools/zipalign/Android.bp
index 1ebf4eb..8cab04c 100644
--- a/tools/zipalign/Android.bp
+++ b/tools/zipalign/Android.bp
@@ -4,6 +4,10 @@
 // Zip alignment tool
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 cc_defaults {
     name: "zipalign_defaults",
     target: {
diff --git a/tools/ziptime/Android.bp b/tools/ziptime/Android.bp
index 5ef45ed..fa46b30 100644
--- a/tools/ziptime/Android.bp
+++ b/tools/ziptime/Android.bp
@@ -18,6 +18,10 @@
 // Zip timestamp removal tool
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 cc_binary_host {
 
     srcs: [