Merge "Revert "Do not compress zip file for unit tests.""
diff --git a/OWNERS b/OWNERS
index 4cac0f5..6e7c0ea 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1 +1,3 @@
 include platform/build/soong:/OWNERS
+
+per-file finalize_branch_for_release.sh = smoreland@google.com
diff --git a/core/Makefile b/core/Makefile
index 5bbf70f..3246f58 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -842,10 +842,6 @@
 $(call declare-0p-target,$(INSTALLED_FILES_FILE_ROOT))
 $(call declare-0p-target,$(INSTALLED_FILES_JSON_ROOT))
 
-ifeq ($(HOST_OS),linux)
-$(call dist-for-goals, sdk sdk_addon, $(INSTALLED_FILES_FILE_ROOT))
-endif
-
 #------------------------------------------------------------------
 # dtb
 ifdef BOARD_INCLUDE_DTB_IN_BOOTIMG
@@ -877,9 +873,6 @@
 $(eval $(call declare-0p-target,$(INSTALLED_FILES_FILE_RAMDISK)))
 $(eval $(call declare-0p-target,$(INSTALLED_FILES_JSON_RAMDISK)))
 
-ifeq ($(HOST_OS),linux)
-$(call dist-for-goals, sdk sdk_addon, $(INSTALLED_FILES_FILE_RAMDISK))
-endif
 BUILT_RAMDISK_TARGET := $(PRODUCT_OUT)/ramdisk.img
 
 ifeq ($(BOARD_RAMDISK_USE_LZ4),true)
@@ -976,11 +969,14 @@
 
 INTERNAL_INIT_BOOT_IMAGE_ARGS :=
 
-ifneq ($(BOARD_BUILD_SYSTEM_ROOT_IMAGE),true)
-  ifneq ($(BUILDING_INIT_BOOT_IMAGE),true)
-    INTERNAL_BOOTIMAGE_ARGS += --ramdisk $(INSTALLED_RAMDISK_TARGET)
-  else
-    INTERNAL_INIT_BOOT_IMAGE_ARGS += --ramdisk $(INSTALLED_RAMDISK_TARGET)
+# TODO(b/229701033): clean up BOARD_BUILD_GKI_BOOT_IMAGE_WITHOUT_RAMDISK.
+ifneq ($(BOARD_BUILD_GKI_BOOT_IMAGE_WITHOUT_RAMDISK),true)
+  ifneq ($(BOARD_BUILD_SYSTEM_ROOT_IMAGE),true)
+    ifneq ($(BUILDING_INIT_BOOT_IMAGE),true)
+      INTERNAL_BOOTIMAGE_ARGS += --ramdisk $(INSTALLED_RAMDISK_TARGET)
+    else
+      INTERNAL_INIT_BOOT_IMAGE_ARGS += --ramdisk $(INSTALLED_RAMDISK_TARGET)
+    endif
   endif
 endif
 
@@ -1568,7 +1564,6 @@
 # TODO These intermediate NOTICE.txt/NOTICE.html files should go into
 # TARGET_OUT_NOTICE_FILES now that the notice files are gathered from
 # the src subdirectory.
-target_notice_file_txt := $(TARGET_OUT_INTERMEDIATES)/NOTICE.txt
 kernel_notice_file := $(TARGET_OUT_NOTICE_FILES)/src/kernel.txt
 winpthreads_notice_file := $(TARGET_OUT_NOTICE_FILES)/src/winpthreads.txt
 
@@ -1765,8 +1760,12 @@
     $(BOARD_SYSTEM_DLKMIMAGE_FILE_SYSTEM_TYPE) \
   ,erofs),)
 INTERNAL_USERIMAGES_DEPS += $(MKEROFS)
+ifeq ($(BOARD_EROFS_USE_LEGACY_COMPRESSION),true)
+BOARD_EROFS_COMPRESSOR ?= "lz4"
+else
 BOARD_EROFS_COMPRESSOR ?= "lz4hc,9"
 endif
+endif
 
 ifneq ($(filter \
     $(BOARD_PRODUCTIMAGE_FILE_SYSTEM_TYPE) \
@@ -1912,6 +1911,7 @@
 $(if $(BOARD_EROFS_COMPRESSOR),$(hide) echo "erofs_default_compressor=$(BOARD_EROFS_COMPRESSOR)" >> $(1))
 $(if $(BOARD_EROFS_PCLUSTER_SIZE),$(hide) echo "erofs_pcluster_size=$(BOARD_EROFS_PCLUSTER_SIZE)" >> $(1))
 $(if $(BOARD_EROFS_SHARE_DUP_BLOCKS),$(hide) echo "erofs_share_dup_blocks=$(BOARD_EROFS_SHARE_DUP_BLOCKS)" >> $(1))
+$(if $(BOARD_EROFS_USE_LEGACY_COMPRESSION),$(hide) echo "erofs_use_legacy_compression=$(BOARD_EROFS_USE_LEGACY_COMPRESSION)" >> $(1))
 $(if $(BOARD_EXT4_SHARE_DUP_BLOCKS),$(hide) echo "ext4_share_dup_blocks=$(BOARD_EXT4_SHARE_DUP_BLOCKS)" >> $(1))
 $(if $(BOARD_FLASH_LOGICAL_BLOCK_SIZE), $(hide) echo "flash_logical_block_size=$(BOARD_FLASH_LOGICAL_BLOCK_SIZE)" >> $(1))
 $(if $(BOARD_FLASH_ERASE_BLOCK_SIZE), $(hide) echo "flash_erase_block_size=$(BOARD_FLASH_ERASE_BLOCK_SIZE)" >> $(1))
@@ -2004,6 +2004,8 @@
     $(hide) echo "recovery_as_boot=true" >> $(1))
 $(if $(filter true,$(BOARD_BUILD_SYSTEM_ROOT_IMAGE)),\
     $(hide) echo "system_root_image=true" >> $(1))
+$(if $(filter true,$(BOARD_BUILD_GKI_BOOT_IMAGE_WITHOUT_RAMDISK)),\
+    $(hide) echo "gki_boot_image_without_ramdisk=true" >> $(1))
 $(hide) echo "root_dir=$(TARGET_ROOT_OUT)" >> $(1)
 $(if $(filter true,$(PRODUCT_USE_DYNAMIC_PARTITION_SIZE)),\
     $(hide) echo "use_dynamic_partition_size=true" >> $(1))
@@ -3099,10 +3101,6 @@
 .PHONY: installed-file-list
 installed-file-list: $(INSTALLED_FILES_FILE)
 
-ifeq ($(HOST_OS),linux)
-$(call dist-for-goals, sdk sdk_addon, $(INSTALLED_FILES_FILE))
-endif
-
 systemimage_intermediates := \
     $(call intermediates-dir-for,PACKAGING,systemimage)
 BUILT_SYSTEMIMAGE := $(systemimage_intermediates)/system.img
@@ -5365,7 +5363,7 @@
 tool_extension := $(wildcard $(tool_extensions)/releasetools.py)
 $(BUILT_TARGET_FILES_PACKAGE): PRIVATE_TOOL_EXTENSION := $(tool_extension)
 
-updaer_dep :=
+updater_dep :=
 ifeq ($(AB_OTA_UPDATER),true)
 updater_dep += system/update_engine/update_engine.conf
 $(call declare-1p-target,system/update_engine/update_engine.conf,system/update_engine)
@@ -6789,8 +6787,6 @@
 # if we don't have a real list, then use "everything"
 ifeq ($(strip $(ATREE_FILES)),)
 ATREE_FILES := \
-	$(ALL_DEFAULT_INSTALLED_MODULES) \
-	$(INSTALLED_RAMDISK_TARGET) \
 	$(ALL_DOCS) \
 	$(ALL_SDK_FILES)
 endif
@@ -6817,21 +6813,9 @@
 include $(BUILD_SYSTEM)/sdk_font.mk
 
 deps := \
-	$(target_notice_file_txt) \
 	$(OUT_DOCS)/offline-sdk-timestamp \
 	$(SDK_METADATA_FILES) \
-	$(SYMBOLS_ZIP) \
-	$(COVERAGE_ZIP) \
-	$(APPCOMPAT_ZIP) \
-	$(INSTALLED_SYSTEMIMAGE_TARGET) \
-	$(INSTALLED_QEMU_SYSTEMIMAGE) \
-	$(INSTALLED_QEMU_RAMDISKIMAGE) \
-	$(INSTALLED_QEMU_VENDORIMAGE) \
-	$(QEMU_VERIFIED_BOOT_PARAMS) \
-	$(INSTALLED_USERDATAIMAGE_TARGET) \
-	$(INSTALLED_RAMDISK_TARGET) \
-	$(INSTALLED_SDK_BUILD_PROP_TARGET) \
-	$(INSTALLED_BUILD_PROP_TARGET) \
+  $(INSTALLED_SDK_BUILD_PROP_TARGET) \
 	$(ATREE_FILES) \
 	$(sdk_atree_files) \
 	$(HOST_OUT_EXECUTABLES)/atree \
@@ -6877,8 +6861,6 @@
 	        -v "DLL_EXTENSION=$(HOST_SHLIB_SUFFIX)" \
 	        -v "FONT_OUT=$(SDK_FONT_TEMP)" \
 	        -o $(PRIVATE_DIR) && \
-	    cp -f $(target_notice_file_txt) \
-	            $(PRIVATE_DIR)/system-images/android-$(PLATFORM_VERSION)/$(TARGET_CPU_ABI)/NOTICE.txt && \
 	    HOST_OUT_EXECUTABLES=$(HOST_OUT_EXECUTABLES) HOST_OS=$(HOST_OS) \
 	        development/build/tools/sdk_clean.sh $(PRIVATE_DIR) && \
 	    chmod -R ug+rwX $(PRIVATE_DIR) && \
diff --git a/core/OWNERS b/core/OWNERS
index 8794434..dae34ff 100644
--- a/core/OWNERS
+++ b/core/OWNERS
@@ -1,6 +1,6 @@
-per-file dex_preopt*.mk = ngeoffray@google.com,calin@google.com,mathewi@google.com,skvadrik@google.com
-per-file verify_uses_libraries.sh = ngeoffray@google.com,calin@google.com,skvadrik@google.com
+per-file dex_preopt*.* = ngeoffray@google.com,skvadrik@google.com
+per-file verify_uses_libraries.sh = ngeoffray@google.com,skvadrik@google.com
 
 # For version updates
-per-file version_defaults.mk = aseaton@google.com,elisapascual@google.com,lubomir@google.com,pscovanner@google.com
+per-file version_defaults.mk = aseaton@google.com,lubomir@google.com,pscovanner@google.com,bkhalife@google.com,jainne@google.com
 
diff --git a/core/android_manifest.mk b/core/android_manifest.mk
index 254e09b..ff49262 100644
--- a/core/android_manifest.mk
+++ b/core/android_manifest.mk
@@ -87,13 +87,23 @@
   endif
 endif
 
+# TODO: Replace this hardcoded list of optional uses-libraries with build logic
+# that propagates optionality via the generated exported-sdk-libs files.
+# Hardcodng doesn't scale and enforces a single choice on each library, while in
+# reality this is a choice of the library users (which may differ).
+my_optional_sdk_lib_names := \
+    android.test.base \
+    android.test.mock \
+    androidx.window.extensions \
+    androidx.window.sidecar
+
 $(fixed_android_manifest): PRIVATE_MANIFEST_FIXER_FLAGS := $(my_manifest_fixer_flags)
 # These two libs are added as optional dependencies (<uses-library> with
 # android:required set to false). This is because they haven't existed in pre-P
 # devices, but classes in them were in bootclasspath jars, etc. So making them
 # hard dependencies (andriod:required=true) would prevent apps from being
 # installed to such legacy devices.
-$(fixed_android_manifest): PRIVATE_OPTIONAL_SDK_LIB_NAMES := android.test.base android.test.mock
+$(fixed_android_manifest): PRIVATE_OPTIONAL_SDK_LIB_NAMES := $(my_optional_sdk_lib_names)
 $(fixed_android_manifest): $(MANIFEST_FIXER)
 $(fixed_android_manifest): $(main_android_manifest)
 	echo $(PRIVATE_OPTIONAL_SDK_LIB_NAMES) | tr ' ' '\n' > $(PRIVATE_EXPORTED_SDK_LIBS_FILE).optional
@@ -109,3 +119,5 @@
 	   ) \
 	  $< $@
 	rm $(PRIVATE_EXPORTED_SDK_LIBS_FILE).optional
+
+my_optional_sdk_lib_names :=
diff --git a/core/base_rules.mk b/core/base_rules.mk
index e6b8f20..355a22e 100644
--- a/core/base_rules.mk
+++ b/core/base_rules.mk
@@ -20,7 +20,11 @@
 # Users can define base-rules-hook in their buildspec.mk to perform
 # arbitrary operations as each module is included.
 ifdef base-rules-hook
-$(if $(base-rules-hook),)
+  ifndef _has_warned_about_base_rules_hook
+    $(warning base-rules-hook is deprecated, please remove usages of it and/or convert to Soong.)
+    _has_warned_about_base_rules_hook := true
+  endif
+  $(if $(base-rules-hook),)
 endif
 
 ###########################################################
@@ -590,10 +594,18 @@
       my_init_rc := $(foreach rc,$(LOCAL_INIT_RC_$(my_32_64_bit_suffix)) $(LOCAL_INIT_RC),$(LOCAL_PATH)/$(rc))
     endif
     ifneq ($(strip $(my_init_rc)),)
-      # Make doesn't support recovery as an output partition, but some Soong modules installed in recovery
-      # have init.rc files that need to be installed alongside them. Manually handle the case where the
-      # output file is in the recovery partition.
-      my_init_rc_path := $(if $(filter $(TARGET_RECOVERY_ROOT_OUT)/%,$(my_module_path)),$(TARGET_RECOVERY_ROOT_OUT)/system/etc,$(TARGET_OUT$(partition_tag)_ETC))
+      # Make doesn't support recovery or ramdisk as an output partition,
+      # but some Soong modules installed in recovery or ramdisk
+      # have init.rc files that need to be installed alongside them.
+      # Manually handle the case where the
+      # output file is in the recovery or ramdisk partition.
+      ifneq (,$(filter $(TARGET_RECOVERY_ROOT_OUT)/%,$(my_module_path)))
+        my_init_rc_path := $(TARGET_RECOVERY_ROOT_OUT)/system/etc
+      else ifneq (,$(filter $(TARGET_RAMDISK_OUT)/%,$(my_module_path)))
+        my_init_rc_path := $(TARGET_RAMDISK_OUT)/system/etc
+      else
+        my_init_rc_path := $(TARGET_OUT$(partition_tag)_ETC)
+      endif
       my_init_rc_pairs := $(foreach rc,$(my_init_rc),$(rc):$(my_init_rc_path)/init/$(notdir $(rc)))
       my_init_rc_installed := $(foreach rc,$(my_init_rc_pairs),$(call word-colon,2,$(rc)))
 
@@ -713,6 +725,7 @@
 endif
 ifdef LOCAL_MULTILIB
   multi_arch := true
+# These conditionals allow this functionality to be mimicked in Soong
 else ifeq ($(LOCAL_MODULE_MAKEFILE),$(SOONG_ANDROID_MK))
   ifeq ($(LOCAL_MODULE_CLASS),SHARED_LIBRARIES)
     multi_arch := true
diff --git a/core/board_config.mk b/core/board_config.mk
index dc50a68..8074225 100644
--- a/core/board_config.mk
+++ b/core/board_config.mk
@@ -234,10 +234,7 @@
   .KATI_READONLY := TARGET_DEVICE_DIR
 endif
 
-# TODO(colefaust) change this if to RBC_PRODUCT_CONFIG when
-# the board configuration is known to work on everything
-# the product config works on.
-ifndef RBC_BOARD_CONFIG
+ifndef RBC_PRODUCT_CONFIG
 include $(board_config_mk)
 else
   $(shell mkdir -p $(OUT_DIR)/rbc)
diff --git a/core/config.mk b/core/config.mk
index 7af600f..247103d 100644
--- a/core/config.mk
+++ b/core/config.mk
@@ -226,8 +226,6 @@
 BUILD_FUZZ_TEST :=$= $(BUILD_SYSTEM)/fuzz_test.mk
 
 BUILD_NOTICE_FILE :=$= $(BUILD_SYSTEM)/notice_files.mk
-BUILD_HOST_DALVIK_JAVA_LIBRARY :=$= $(BUILD_SYSTEM)/host_dalvik_java_library.mk
-BUILD_HOST_DALVIK_STATIC_JAVA_LIBRARY :=$= $(BUILD_SYSTEM)/host_dalvik_static_java_library.mk
 
 include $(BUILD_SYSTEM)/deprecation.mk
 
@@ -427,21 +425,6 @@
 $(hide) $(HOST_NM) -gP $(1) | cut -f1-2 -d" " | (grep -v U$$ >> $(2) || true)
 endef
 
-GOMA_POOL :=
-RBE_POOL :=
-GOMA_OR_RBE_POOL :=
-# When goma or RBE are enabled, kati will be passed --default_pool=local_pool to put
-# most rules into the local pool.  Explicitly set the pool to "none" for rules that
-# should be run outside the local pool, i.e. with -j500.
-ifneq (,$(filter-out false,$(USE_GOMA)))
-  GOMA_POOL := none
-  GOMA_OR_RBE_POOL := none
-else ifneq (,$(filter-out false,$(USE_RBE)))
-  RBE_POOL := none
-  GOMA_OR_RBE_POOL := none
-endif
-.KATI_READONLY := GOMA_POOL RBE_POOL GOMA_OR_RBE_POOL
-
 ifeq ($(CALLED_FROM_SETUP),true)
 include $(BUILD_SYSTEM)/ccache.mk
 include $(BUILD_SYSTEM)/goma.mk
@@ -876,6 +859,7 @@
     30.0 \
     31.0 \
     32.0 \
+    33.0 \
 
 .KATI_READONLY := \
     PLATFORM_SEPOLICY_COMPAT_VERSIONS \
@@ -1233,6 +1217,39 @@
     $(filter $(ANDROID_WARNING_ALLOWED_PROJECTS),$(1)/)
 endef
 
+GOMA_POOL :=
+RBE_POOL :=
+GOMA_OR_RBE_POOL :=
+# When goma or RBE are enabled, kati will be passed --default_pool=local_pool to put
+# most rules into the local pool.  Explicitly set the pool to "none" for rules that
+# should be run outside the local pool, i.e. with -j500.
+ifneq (,$(filter-out false,$(USE_GOMA)))
+  GOMA_POOL := none
+  GOMA_OR_RBE_POOL := none
+else ifneq (,$(filter-out false,$(USE_RBE)))
+  RBE_POOL := none
+  GOMA_OR_RBE_POOL := none
+endif
+.KATI_READONLY := GOMA_POOL RBE_POOL GOMA_OR_RBE_POOL
+
+JAVAC_NINJA_POOL :=
+R8_NINJA_POOL :=
+D8_NINJA_POOL :=
+
+ifneq ($(filter-out false,$(USE_RBE)),)
+  ifdef RBE_JAVAC
+    JAVAC_NINJA_POOL := $(RBE_POOL)
+  endif
+  ifdef RBE_R8
+    R8_NINJA_POOL := $(RBE_POOL)
+  endif
+  ifdef RBE_D8
+    D8_NINJA_POOL := $(RBE_POOL)
+  endif
+endif
+
+.KATI_READONLY := JAVAC_NINJA_POOL R8_NINJA_POOL D8_NINJA_POOL
+
 # These goals don't need to collect and include Android.mks/CleanSpec.mks
 # in the source tree.
 dont_bother_goals := out product-graph
diff --git a/core/definitions.mk b/core/definitions.mk
index 0d72473..e424bc2 100644
--- a/core/definitions.mk
+++ b/core/definitions.mk
@@ -2609,7 +2609,7 @@
 @mkdir -p $(dir $@)tmp
 $(hide) rm -f $(dir $@)classes*.dex $(dir $@)d8_input.jar
 $(hide) $(ZIP2ZIP) -j -i $< -o $(dir $@)d8_input.jar "**/*.class"
-$(hide) $(D8_WRAPPER) $(DX_COMMAND) $(DEX_FLAGS) \
+$(hide) $(D8_WRAPPER) $(D8_COMMAND) \
     --output $(dir $@)tmp \
     $(addprefix --lib ,$(PRIVATE_D8_LIBS)) \
     --min-api $(PRIVATE_MIN_SDK_VERSION) \
@@ -3213,7 +3213,7 @@
 define transform-jar-to-dex-r8
 @echo R8: $@
 $(hide) rm -f $(PRIVATE_PROGUARD_DICTIONARY)
-$(hide) $(R8_WRAPPER) $(R8_COMPAT_PROGUARD) $(DEX_FLAGS) \
+$(hide) $(R8_WRAPPER) $(R8_COMMAND) \
     -injars '$<' \
     --min-api $(PRIVATE_MIN_SDK_VERSION) \
     --no-data-resources \
@@ -3360,8 +3360,6 @@
   STATIC_TEST_LIBRARY \
   HOST_STATIC_TEST_LIBRARY \
   NOTICE_FILE \
-  HOST_DALVIK_JAVA_LIBRARY \
-  HOST_DALVIK_STATIC_JAVA_LIBRARY \
   base_rules \
   HEADER_LIBRARY \
   HOST_TEST_CONFIG \
diff --git a/core/deprecation.mk b/core/deprecation.mk
index 2b7a869..ed4215e 100644
--- a/core/deprecation.mk
+++ b/core/deprecation.mk
@@ -3,8 +3,6 @@
   BUILD_EXECUTABLE \
   BUILD_FUZZ_TEST \
   BUILD_HEADER_LIBRARY \
-  BUILD_HOST_DALVIK_JAVA_LIBRARY \
-  BUILD_HOST_DALVIK_STATIC_JAVA_LIBRARY \
   BUILD_HOST_JAVA_LIBRARY \
   BUILD_HOST_PREBUILT \
   BUILD_JAVA_LIBRARY \
@@ -39,6 +37,8 @@
 OBSOLETE_BUILD_MODULE_TYPES :=$= \
   BUILD_AUX_EXECUTABLE \
   BUILD_AUX_STATIC_LIBRARY \
+  BUILD_HOST_DALVIK_JAVA_LIBRARY \
+  BUILD_HOST_DALVIK_STATIC_JAVA_LIBRARY \
   BUILD_HOST_FUZZ_TEST \
   BUILD_HOST_NATIVE_TEST \
   BUILD_HOST_SHARED_TEST_LIBRARY \
diff --git a/core/dex_preopt_config.mk b/core/dex_preopt_config.mk
index d5293cf..c11b7f4 100644
--- a/core/dex_preopt_config.mk
+++ b/core/dex_preopt_config.mk
@@ -96,7 +96,6 @@
   $(call add_json_list, DisablePreoptModules,                    $(DEXPREOPT_DISABLED_MODULES))
   $(call add_json_bool, OnlyPreoptBootImageAndSystemServer,      $(filter true,$(WITH_DEXPREOPT_BOOT_IMG_AND_SYSTEM_SERVER_ONLY)))
   $(call add_json_bool, PreoptWithUpdatableBcp,                  $(filter true,$(DEX_PREOPT_WITH_UPDATABLE_BCP)))
-  $(call add_json_bool, UseArtImage,                             $(filter true,$(DEXPREOPT_USE_ART_IMAGE)))
   $(call add_json_bool, DontUncompressPrivAppsDex,               $(filter true,$(DONT_UNCOMPRESS_PRIV_APPS_DEXS)))
   $(call add_json_list, ModulesLoadedByPrivilegedModules,        $(PRODUCT_LOADED_BY_PRIVILEGED_MODULES))
   $(call add_json_bool, HasSystemOther,                          $(BOARD_USES_SYSTEM_OTHER_ODEX))
diff --git a/core/dex_preopt_odex_install.mk b/core/dex_preopt_odex_install.mk
index ea50313..216168b 100644
--- a/core/dex_preopt_odex_install.mk
+++ b/core/dex_preopt_odex_install.mk
@@ -272,11 +272,13 @@
 my_dexpreopt_images_deps :=
 my_dexpreopt_image_locations_on_host :=
 my_dexpreopt_image_locations_on_device :=
+# Infix can be 'boot' or 'art'. Soong creates a set of variables for Make, one
+# for each boot image (primary and the framework extension). The only reason why
+# the primary image is exposed to Make is testing (art gtests) and benchmarking
+# (art golem benchmarks). Install rules that use those variables are in
+# dex_preopt_libart.mk. Here for dexpreopt purposes the infix is always 'boot'.
 my_dexpreopt_infix := boot
 my_create_dexpreopt_config :=
-ifeq (true, $(DEXPREOPT_USE_ART_IMAGE))
-  my_dexpreopt_infix := art
-endif
 
 ifdef LOCAL_DEX_PREOPT
   ifeq (,$(filter PRESIGNED,$(LOCAL_CERTIFICATE)))
diff --git a/core/host_dalvik_java_library.mk b/core/host_dalvik_java_library.mk
deleted file mode 100644
index 5eeb8ac..0000000
--- a/core/host_dalvik_java_library.mk
+++ /dev/null
@@ -1,191 +0,0 @@
-#
-# Copyright (C) 2013 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.
-#
-$(call record-module-type,HOST_DALVIK_JAVA_LIBRARY)
-
-#
-# Rules for building a host dalvik java library. These libraries
-# are meant to be used by a dalvik VM instance running on the host.
-# They will be compiled against libcore and not the host JRE.
-#
-
-ifeq ($(HOST_OS),linux)
-USE_CORE_LIB_BOOTCLASSPATH := true
-
-#######################################
-include $(BUILD_SYSTEM)/host_java_library_common.mk
-#######################################
-
-full_classes_turbine_jar := $(intermediates.COMMON)/classes-turbine.jar
-full_classes_header_jarjar := $(intermediates.COMMON)/classes-header-jarjar.jar
-full_classes_header_jar := $(intermediates.COMMON)/classes-header.jar
-full_classes_compiled_jar := $(intermediates.COMMON)/classes-full-debug.jar
-full_classes_combined_jar := $(intermediates.COMMON)/classes-combined.jar
-full_classes_jarjar_jar := $(intermediates.COMMON)/classes-jarjar.jar
-full_classes_jar := $(intermediates.COMMON)/classes.jar
-built_dex := $(intermediates.COMMON)/classes.dex
-java_source_list_file := $(intermediates.COMMON)/java-source-list
-
-LOCAL_INTERMEDIATE_TARGETS += \
-    $(full_classes_turbine_jar) \
-    $(full_classes_compiled_jar) \
-    $(full_classes_combined_jar) \
-    $(full_classes_jarjar_jar) \
-    $(full_classes_jar) \
-    $(built_dex) \
-    $(java_source_list_file)
-
-# See comment in java.mk
-ifndef LOCAL_CHECKED_MODULE
-ifeq ($(LOCAL_IS_STATIC_JAVA_LIBRARY),true)
-LOCAL_CHECKED_MODULE := $(full_classes_compiled_jar)
-else
-LOCAL_CHECKED_MODULE := $(built_dex)
-endif
-endif
-
-#######################################
-include $(BUILD_SYSTEM)/base_rules.mk
-#######################################
-java_sources := $(addprefix $(LOCAL_PATH)/, $(filter %.java,$(LOCAL_SRC_FILES))) \
-                $(filter %.java,$(LOCAL_GENERATED_SOURCES))
-all_java_sources := $(java_sources)
-
-include $(BUILD_SYSTEM)/java_common.mk
-
-include $(BUILD_SYSTEM)/sdk_check.mk
-
-$(cleantarget): PRIVATE_CLEAN_FILES += $(intermediates.COMMON)
-
-# List of dependencies for anything that needs all java sources in place
-java_sources_deps := \
-    $(java_sources) \
-    $(java_resource_sources) \
-    $(LOCAL_SRCJARS) \
-    $(LOCAL_ADDITIONAL_DEPENDENCIES)
-
-$(java_source_list_file): $(java_sources_deps)
-	$(write-java-source-list)
-
-# TODO(b/143658984): goma can't handle the --system argument to javac.
-#$(full_classes_compiled_jar): .KATI_NINJA_POOL := $(GOMA_POOL)
-$(full_classes_compiled_jar): PRIVATE_JAVA_LAYERS_FILE := $(layers_file)
-$(full_classes_compiled_jar): PRIVATE_JAVACFLAGS := $(LOCAL_JAVACFLAGS) $(annotation_processor_flags)
-$(full_classes_compiled_jar): PRIVATE_JAR_EXCLUDE_FILES :=
-$(full_classes_compiled_jar): PRIVATE_JAR_PACKAGES :=
-$(full_classes_compiled_jar): PRIVATE_JAR_EXCLUDE_PACKAGES :=
-$(full_classes_compiled_jar): PRIVATE_SRCJARS := $(LOCAL_SRCJARS)
-$(full_classes_compiled_jar): PRIVATE_SRCJAR_LIST_FILE := $(intermediates.COMMON)/srcjar-list
-$(full_classes_compiled_jar): PRIVATE_SRCJAR_INTERMEDIATES_DIR := $(intermediates.COMMON)/srcjars
-$(full_classes_compiled_jar): \
-    $(java_source_list_file) \
-    $(java_sources_deps) \
-    $(full_java_header_libs) \
-    $(full_java_bootclasspath_libs) \
-    $(full_java_system_modules_deps) \
-    $(annotation_processor_deps) \
-    $(NORMALIZE_PATH) \
-    $(JAR_ARGS) \
-    $(ZIPSYNC) \
-    $(SOONG_ZIP) \
-    | $(SOONG_JAVAC_WRAPPER)
-	$(transform-host-java-to-dalvik-package)
-
-ifneq ($(TURBINE_ENABLED),false)
-
-$(full_classes_turbine_jar): PRIVATE_JAVACFLAGS := $(LOCAL_JAVACFLAGS) $(annotation_processor_flags)
-$(full_classes_turbine_jar): PRIVATE_SRCJARS := $(LOCAL_SRCJARS)
-$(full_classes_turbine_jar): \
-    $(java_source_list_file) \
-    $(java_sources_deps) \
-    $(full_java_header_libs) \
-    $(full_java_bootclasspath_libs) \
-    $(NORMALIZE_PATH) \
-    $(JAR_ARGS) \
-    $(ZIPTIME) \
-    | $(TURBINE) \
-    $(MERGE_ZIPS)
-	$(transform-java-to-header.jar)
-
-.KATI_RESTAT: $(full_classes_turbine_jar)
-
-# Run jarjar before generate classes-header.jar if necessary.
-ifneq ($(strip $(LOCAL_JARJAR_RULES)),)
-$(full_classes_header_jarjar): PRIVATE_JARJAR_RULES := $(LOCAL_JARJAR_RULES)
-$(full_classes_header_jarjar): $(full_classes_turbine_jar) $(LOCAL_JARJAR_RULES) | $(JARJAR)
-	$(call transform-jarjar)
-else
-full_classes_header_jarjar := $(full_classes_turbine_jar)
-endif
-
-$(eval $(call copy-one-file,$(full_classes_header_jarjar),$(full_classes_header_jar)))
-
-endif # TURBINE_ENABLED != false
-
-$(full_classes_combined_jar): PRIVATE_DONT_DELETE_JAR_META_INF := $(LOCAL_DONT_DELETE_JAR_META_INF)
-$(full_classes_combined_jar): $(full_classes_compiled_jar) \
-                              $(jar_manifest_file) \
-                              $(full_static_java_libs)  | $(MERGE_ZIPS)
-	$(if $(PRIVATE_JAR_MANIFEST), $(hide) sed -e "s/%BUILD_NUMBER%/$(BUILD_NUMBER_FROM_FILE)/" \
-            $(PRIVATE_JAR_MANIFEST) > $(dir $@)/manifest.mf)
-	$(MERGE_ZIPS) -j --ignore-duplicates $(if $(PRIVATE_JAR_MANIFEST),-m $(dir $@)/manifest.mf) \
-            $(if $(PRIVATE_DONT_DELETE_JAR_META_INF),,-stripDir META-INF -zipToNotStrip $<) \
-            $@ $< $(PRIVATE_STATIC_JAVA_LIBRARIES)
-
-# Run jarjar if necessary, otherwise just copy the file.
-ifneq ($(strip $(LOCAL_JARJAR_RULES)),)
-$(full_classes_jarjar_jar): PRIVATE_JARJAR_RULES := $(LOCAL_JARJAR_RULES)
-$(full_classes_jarjar_jar): $(full_classes_combined_jar) $(LOCAL_JARJAR_RULES) | $(JARJAR)
-	$(call transform-jarjar)
-else
-full_classes_jarjar_jar := $(full_classes_combined_jar)
-endif
-
-$(eval $(call copy-one-file,$(full_classes_jarjar_jar),$(full_classes_jar)))
-
-ifeq ($(LOCAL_IS_STATIC_JAVA_LIBRARY),true)
-# No dex; all we want are the .class files with resources.
-$(LOCAL_BUILT_MODULE) : $(java_resource_sources)
-$(LOCAL_BUILT_MODULE) : $(full_classes_jar)
-	@echo "host Static Jar: $(PRIVATE_MODULE) ($@)"
-	$(copy-file-to-target)
-
-else # !LOCAL_IS_STATIC_JAVA_LIBRARY
-$(built_dex): PRIVATE_INTERMEDIATES_DIR := $(intermediates.COMMON)
-$(built_dex): PRIVATE_DX_FLAGS := $(LOCAL_DX_FLAGS)
-$(built_dex): $(full_classes_jar) $(DX) $(ZIP2ZIP)
-	$(transform-classes.jar-to-dex)
-
-$(LOCAL_BUILT_MODULE): PRIVATE_DEX_FILE := $(built_dex)
-$(LOCAL_BUILT_MODULE): PRIVATE_SOURCE_ARCHIVE := $(full_classes_jarjar_jar)
-$(LOCAL_BUILT_MODULE): $(MERGE_ZIPS) $(SOONG_ZIP) $(ZIP2ZIP)
-$(LOCAL_BUILT_MODULE): $(built_dex) $(java_resource_sources)
-	@echo "Host Jar: $(PRIVATE_MODULE) ($@)"
-	rm -rf $@.parts
-	mkdir -p $@.parts
-	$(call create-dex-jar,$@.parts/dex.zip,$(PRIVATE_DEX_FILE))
-	$(call extract-resources-jar,$@.parts/res.zip,$(PRIVATE_SOURCE_ARCHIVE))
-	$(MERGE_ZIPS) -j $@ $@.parts/dex.zip $@.parts/res.zip
-	rm -rf $@.parts
-
-endif # !LOCAL_IS_STATIC_JAVA_LIBRARY
-
-$(LOCAL_INTERMEDIATE_TARGETS): PRIVATE_DEFAULT_APP_TARGET_SDK := $(call module-target-sdk-version)
-$(LOCAL_INTERMEDIATE_TARGETS): PRIVATE_SDK_VERSION := $(call module-sdk-version)
-$(LOCAL_INTERMEDIATE_TARGETS): PRIVATE_MIN_SDK_VERSION := $(call codename-or-sdk-to-sdk,$(call module-min-sdk-version))
-
-USE_CORE_LIB_BOOTCLASSPATH :=
-
-endif
diff --git a/core/host_dalvik_static_java_library.mk b/core/host_dalvik_static_java_library.mk
deleted file mode 100644
index 78faf73..0000000
--- a/core/host_dalvik_static_java_library.mk
+++ /dev/null
@@ -1,28 +0,0 @@
-#
-# Copyright (C) 2013 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.
-#
-$(call record-module-type,HOST_DALVIK_STATIC_JAVA_LIBRARY)
-
-#
-# Rules for building a host dalvik static java library.
-# These libraries will be compiled against libcore and not the host
-# JRE.
-#
-LOCAL_UNINSTALLABLE_MODULE := true
-LOCAL_IS_STATIC_JAVA_LIBRARY := true
-
-include $(BUILD_SYSTEM)/host_dalvik_java_library.mk
-
-LOCAL_IS_STATIC_JAVA_LIBRARY :=
diff --git a/core/java.mk b/core/java.mk
index a29f820..01951c0 100644
--- a/core/java.mk
+++ b/core/java.mk
@@ -494,13 +494,13 @@
   $(built_dex_intermediate): PRIVATE_EXTRA_INPUT_JAR := $(extra_input_jar)
   $(built_dex_intermediate): PRIVATE_PROGUARD_FLAGS := $(legacy_proguard_flags) $(common_proguard_flags) $(LOCAL_PROGUARD_FLAGS)
   $(built_dex_intermediate): PRIVATE_PROGUARD_DICTIONARY := $(proguard_dictionary)
-  $(built_dex_intermediate) : $(full_classes_pre_proguard_jar) $(extra_input_jar) $(my_proguard_sdk_raise) $(common_proguard_flag_files) $(legacy_proguard_lib_deps) $(R8_COMPAT_PROGUARD) $(LOCAL_PROGUARD_FLAGS_DEPS)
+  $(built_dex_intermediate) : $(full_classes_pre_proguard_jar) $(extra_input_jar) $(my_proguard_sdk_raise) $(common_proguard_flag_files) $(legacy_proguard_lib_deps) $(R8) $(LOCAL_PROGUARD_FLAGS_DEPS)
 	$(transform-jar-to-dex-r8)
 else # !LOCAL_PROGUARD_ENABLED
   $(built_dex_intermediate): .KATI_NINJA_POOL := $(D8_NINJA_POOL)
   $(built_dex_intermediate): PRIVATE_D8_LIBS := $(full_java_bootclasspath_libs) $(full_shared_java_header_libs)
   $(built_dex_intermediate): $(full_java_bootclasspath_libs) $(full_shared_java_header_libs)
-  $(built_dex_intermediate): $(full_classes_pre_proguard_jar) $(DX) $(ZIP2ZIP)
+  $(built_dex_intermediate): $(full_classes_pre_proguard_jar) $(D8) $(ZIP2ZIP)
 	$(transform-classes.jar-to-dex)
 endif
 
diff --git a/core/main.mk b/core/main.mk
index e0efdad..78f38f3 100644
--- a/core/main.mk
+++ b/core/main.mk
@@ -931,7 +931,7 @@
     $(eval my_deps := $(call get-all-shared-libs-deps,$(m)))\
     $(foreach dep,$(my_deps),\
       $(foreach f,$(ALL_MODULES.$(dep).HOST_SHARED_LIBRARY_FILES),\
-        $(if $(filter $(suite),device-tests general-tests),\
+        $(if $(filter $(suite),device-tests general-tests art-host-tests host-unit-tests),\
           $(eval my_testcases := $(HOST_OUT_TESTCASES)),\
           $(eval my_testcases := $$(COMPATIBILITY_TESTCASES_OUT_$(suite))))\
         $(eval target := $(my_testcases)/$(lastword $(subst /, ,$(dir $(f))))/$(notdir $(f)))\
@@ -1936,10 +1936,6 @@
 sdk: $(ALL_SDK_TARGETS)
 $(call dist-for-goals,sdk, \
     $(ALL_SDK_TARGETS) \
-    $(SYMBOLS_ZIP) \
-    $(SYMBOLS_MAPPING) \
-    $(COVERAGE_ZIP) \
-    $(APPCOMPAT_ZIP) \
     $(INSTALLED_BUILD_PROP_TARGET) \
 )
 endif
diff --git a/core/product-graph.mk b/core/product-graph.mk
index 379110e..4a44837 100644
--- a/core/product-graph.mk
+++ b/core/product-graph.mk
@@ -25,7 +25,7 @@
 		$(if $(filter $(p),$(_all_products_visited)),, \
 			$(p) \
 			$(eval _all_products_visited += $(p)) \
-			$(call all-products-inner, $(PRODUCTS.$(strip $(p)).INHERITS_FROM))
+			$(call gather-all-makefiles-for-current-product-inner, $(PRODUCTS.$(strip $(p)).INHERITS_FROM))
 		) \
 	)
 endef
diff --git a/core/product_config.mk b/core/product_config.mk
index 1e74fa9..540289a 100644
--- a/core/product_config.mk
+++ b/core/product_config.mk
@@ -210,7 +210,6 @@
 # Dedup, extract product names, etc.
 product_paths := $(sort $(product_paths))
 all_named_products := $(sort $(call _first,$(product_paths),:))
-all_product_makefiles := $(sort $(call _second,$(product_paths),:))
 current_product_makefile := $(call _second,$(filter $(TARGET_PRODUCT):%,$(product_paths)),:)
 COMMON_LUNCH_CHOICES := $(sort $(common_lunch_choices))
 
@@ -230,7 +229,6 @@
 
 ifneq (,$(filter $(TARGET_PRODUCT),$(products_using_starlark_config)))
   RBC_PRODUCT_CONFIG := true
-  RBC_BOARD_CONFIG := true
 endif
 
 ifndef RBC_PRODUCT_CONFIG
@@ -274,8 +272,6 @@
 ############################################################################
 
 current_product_makefile :=
-all_product_makefiles :=
-all_product_configs :=
 
 #############################################################################
 # Quick check and assign default values
diff --git a/core/product_config.rbc b/core/product_config.rbc
index 917475a..7a5e501 100644
--- a/core/product_config.rbc
+++ b/core/product_config.rbc
@@ -104,6 +104,11 @@
     seen = {item: 0 for item in value_list}
     return sorted(seen.keys()) if _options.rearrange == "sort" else seen.keys()
 
+def __sort_pcm_names(pcm_names):
+    # We have to add an extension back onto the pcm names when sorting,
+    # or else the sort order could be wrong when one is a prefix of another.
+    return [x[:-3] for x in sorted([y + ".mk" for y in pcm_names], reverse=True)]
+
 def _product_configuration(top_pcm_name, top_pcm, input_variables_init):
     """Creates configuration."""
 
@@ -142,7 +147,7 @@
 
         # Run this one, obtaining its configuration and child PCMs.
         if _options.trace_modules:
-            print("#%d: %s" % (n, name))
+            rblf_log("%d: %s" % (n, name))
 
         # Run PCM.
         handle = __h_new()
@@ -162,12 +167,12 @@
         # Now we know everything about this PCM, record it in 'configs'.
         children = handle.inherited_modules
         if _options.trace_modules:
-            print("#   ", "    ".join(children.keys()))
+            rblf_log("   ", "    ".join(children.keys()))
         # Starlark dictionaries are guaranteed to iterate through in insertion order,
         # so children.keys() will be ordered by the inherit() calls
         configs[name] = (pcm, handle.cfg, children.keys(), False)
 
-        for child_name in sorted(children, reverse = True):
+        for child_name in __sort_pcm_names(children.keys()):
             if child_name not in configs:
                 configs[child_name] = (children[child_name], None, [], False)
             pcm_stack.append(child_name)
@@ -202,7 +207,7 @@
         pcm_name, before = pcm_stack.pop()
         if before:
             pcm_stack.append((pcm_name, False))
-            for child in sorted(configs[pcm_name][2], reverse = True):
+            for child in __sort_pcm_names(configs[pcm_name][2]):
                 pcm_stack.append((child, True))
         else:
             configs_postfix.append(pcm_name)
@@ -229,9 +234,9 @@
     configs = cloned_configs
 
     if trace:
-        print("\n#---Postfix---")
+        rblf_log("\n---Postfix---")
         for x in configs_postfix:
-            print("#   ", x)
+            rblf_log("   ", x)
 
     # Traverse the tree from the bottom, evaluating inherited values
     for pcm_name in configs_postfix:
@@ -304,7 +309,7 @@
             old_val = val
             new_val = _value_expand(configs, attr, val)
             if new_val != old_val:
-                print("%s(i): %s=%s (was %s)" % (pcm_name, attr, new_val, old_val))
+                rblf_log("%s(i): %s=%s (was %s)" % (pcm_name, attr, new_val, old_val))
             cfg[attr] = new_val
 
 def _value_expand(configs, attr, values_list):
@@ -358,7 +363,7 @@
 
     for attr in _options.trace_variables:
         if attr in percolated_attrs:
-            print("%s: %s^=%s" % (cfg_name, attr, cfg[attr]))
+            rblf_log("%s: %s^=%s" % (cfg_name, attr, cfg[attr]))
 
 def __move_items(to_list, from_cfg, attr):
     value = from_cfg.get(attr, [])
@@ -400,10 +405,26 @@
     """Gets to the value of the variable in the namespace."""
     return g.get(_soong_config_namespaces_key, {}).get(nsname, {}).get(var, None)
 
-
-def _abspath(path):
+def _abspath(paths):
     """Provided for compatibility, to be removed later."""
-    return path
+    cwd = rblf_shell('pwd')
+    results = []
+    for path in __words(paths):
+        if path[0] != "/":
+            path = cwd + "/" + path
+
+        resultparts = []
+        for part in path.split('/'):
+            if part == "." or part == "":
+                continue
+            elif part == "..":
+                if resultparts:
+                    resultparts.pop()
+            else:
+                resultparts.append(part)
+        results.append("/" + "/".join(resultparts))
+
+    return " ".join(results)
 
 
 def _addprefix(prefix, string_or_list):
@@ -515,8 +536,11 @@
     """If from file exists, returns [from:to] pair."""
     value = path_pair.split(":", 2)
 
+    if value[0].find('*') != -1:
+        fail("copy_if_exists: input file cannot contain *")
+
     # Check that l[0] exists
-    return [":".join(value)] if rblf_file_exists(value[0]) else []
+    return [":".join(value)] if rblf_wildcard(value[0]) else []
 
 def _enforce_product_packages_exist(handle, pkg_string_or_list=[]):
     """Makes including non-existent modules in PRODUCT_PACKAGES an error."""
@@ -531,10 +555,6 @@
     _setdefault(handle, "PRODUCT_DEX_PREOPT_MODULE_CONFIGS")
     handle.cfg["PRODUCT_DEX_PREOPT_MODULE_CONFIGS"] += [m + "=" + config for m in modules]
 
-def _file_wildcard_exists(file_pattern):
-    """Return True if there are files matching given bash pattern."""
-    return len(rblf_wildcard(file_pattern)) > 0
-
 def _find_and_copy(pattern, from_dir, to_dir):
     """Return a copy list for the files matching the pattern."""
     return sorted([("%s/%s:%s/%s" % (from_dir, f, to_dir, f))
@@ -584,6 +604,27 @@
                 break
     return res
 
+def _first_word(input):
+    """Equivalent to the GNU make function $(firstword)."""
+    input = __words(input)
+    if len(input) == 0:
+        return ""
+    return input[0]
+
+def _last_word(input):
+    """Equivalent to the GNU make function $(lastword)."""
+    input = __words(input)
+    l = len(input)
+    if l == 0:
+        return ""
+    return input[l-1]
+
+def _flatten_2d_list(list):
+    result = []
+    for x in list:
+        result += x
+    return result
+
 def _dir(paths):
     """Equivalent to the GNU make function $(dir).
 
@@ -746,8 +787,11 @@
     That is, removes string's leading and trailing whitespace characters and
     replaces any sequence of whitespace characters with with a single space.
     """
-    if type(s) != "string":
-        return s
+    t = type(s)
+    if t == "list":
+        s = " ".join(s)
+    elif t != "string":
+        fail("Argument to mkstrip must be a string or list, got: "+t)
     result = ""
     was_space = False
     for ch in s.strip().elems():
@@ -835,12 +879,13 @@
     dir = _dir,
     enforce_product_packages_exist = _enforce_product_packages_exist,
     expand_wildcard = _expand_wildcard,
-    file_exists = rblf_file_exists,
-    file_wildcard_exists = _file_wildcard_exists,
     filter = _filter,
     filter_out = _filter_out,
     find_and_copy = _find_and_copy,
     findstring = _findstring,
+    first_word = _first_word,
+    last_word = _last_word,
+    flatten_2d_list = _flatten_2d_list,
     inherit = _inherit,
     indirect = _indirect,
     mk2rbc_error = _mk2rbc_error,
diff --git a/core/rbe.mk b/core/rbe.mk
index e399ec1..370d4bd 100644
--- a/core/rbe.mk
+++ b/core/rbe.mk
@@ -14,10 +14,6 @@
 # limitations under the License.
 #
 
-JAVAC_NINJA_POOL :=
-R8_NINJA_POOL :=
-D8_NINJA_POOL :=
-
 # Notice: this works only with Google's RBE service.
 ifneq ($(filter-out false,$(USE_RBE)),)
   ifdef RBE_DIR
@@ -88,20 +84,16 @@
 
   ifdef RBE_JAVAC
     JAVAC_WRAPPER := $(strip $(JAVAC_WRAPPER) $(RBE_WRAPPER) --labels=type=compile,lang=java,compiler=javac --exec_strategy=$(javac_exec_strategy) --platform=$(java_r8_d8_platform))
-    JAVAC_NINJA_POOL := $(RBE_POOL)
   endif
 
   ifdef RBE_R8
     R8_WRAPPER := $(strip $(RBE_WRAPPER) --labels=type=compile,compiler=r8 --exec_strategy=$(r8_exec_strategy) --platform=$(java_r8_d8_platform) --inputs=out/soong/host/linux-x86/framework/r8-compat-proguard.jar,build/make/core/proguard_basic_keeps.flags --toolchain_inputs=prebuilts/jdk/jdk11/linux-x86/bin/java)
-    R8_NINJA_POOL := $(RBE_POOL)
   endif
 
   ifdef RBE_D8
     D8_WRAPPER := $(strip $(RBE_WRAPPER) --labels=type=compile,compiler=d8 --exec_strategy=$(d8_exec_strategy) --platform=$(java_r8_d8_platform) --inputs=out/soong/host/linux-x86/framework/d8.jar --toolchain_inputs=prebuilts/jdk/jdk11/linux-x86/bin/java)
-    D8_NINJA_POOL := $(RBE_POOL)
   endif
 
   rbe_dir :=
 endif
 
-.KATI_READONLY := JAVAC_NINJA_POOL R8_NINJA_POOL D8_NINJA_POOL
diff --git a/core/soong_cc_rust_prebuilt.mk b/core/soong_cc_rust_prebuilt.mk
index 7a177ff..07e577a 100644
--- a/core/soong_cc_rust_prebuilt.mk
+++ b/core/soong_cc_rust_prebuilt.mk
@@ -260,6 +260,9 @@
 installed_static_library_notice_file_targets += \
     $(foreach lib,$(LOCAL_RLIB_LIBRARIES), \
       NOTICE-$(if $(LOCAL_IS_HOST_MODULE),HOST$(if $(my_host_cross),_CROSS,),TARGET)-RLIB_LIBRARIES-$(lib))
+installed_static_library_notice_file_targets += \
+    $(foreach lib,$(LOCAL_PROC_MACRO_LIBRARIES), \
+      NOTICE-$(if $(LOCAL_IS_HOST_MODULE),HOST$(if $(my_host_cross),_CROSS,),TARGET)-PROC_MACRO_LIBRARIES-$(lib))
 
 $(notice_target): | $(installed_static_library_notice_file_targets)
 $(LOCAL_INSTALLED_MODULE): | $(notice_target)
diff --git a/core/soong_config.mk b/core/soong_config.mk
index 32675f2..c84676b 100644
--- a/core/soong_config.mk
+++ b/core/soong_config.mk
@@ -33,6 +33,7 @@
 $(call add_json_str,  Platform_security_patch,           $(PLATFORM_SECURITY_PATCH))
 $(call add_json_str,  Platform_preview_sdk_version,      $(PLATFORM_PREVIEW_SDK_VERSION))
 $(call add_json_str,  Platform_base_os,                  $(PLATFORM_BASE_OS))
+$(call add_json_str,  Platform_version_last_stable,      $(PLATFORM_VERSION_LAST_STABLE))
 
 $(call add_json_str,  Platform_min_supported_target_sdk_version, $(PLATFORM_MIN_SUPPORTED_TARGET_SDK_VERSION))
 
@@ -169,6 +170,8 @@
 $(call add_json_list, RecoverySnapshotDirsExcluded,      $(RECOVERY_SNAPSHOT_DIRS_EXCLUDED))
 $(call add_json_bool, HostFakeSnapshotEnabled,           $(HOST_FAKE_SNAPSHOT_ENABLE))
 
+$(call add_json_bool, MultitreeUpdateMeta,               $(filter true,$(TARGET_MULTITREE_UPDATE_META)))
+
 $(call add_json_bool, Treble_linker_namespaces,          $(filter true,$(PRODUCT_TREBLE_LINKER_NAMESPACES)))
 $(call add_json_bool, Enforce_vintf_manifest,            $(filter true,$(PRODUCT_ENFORCE_VINTF_MANIFEST)))
 
diff --git a/core/tasks/multitree.mk b/core/tasks/multitree.mk
new file mode 100644
index 0000000..225477e
--- /dev/null
+++ b/core/tasks/multitree.mk
@@ -0,0 +1,16 @@
+# Copyright (C) 2022 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.
+
+.PHONY: update-meta
+update-meta: $(SOONG_MULTITREE_METADATA)
diff --git a/core/tasks/tools/compatibility.mk b/core/tasks/tools/compatibility.mk
index add580d..4b8bd16 100644
--- a/core/tasks/tools/compatibility.mk
+++ b/core/tasks/tools/compatibility.mk
@@ -132,6 +132,9 @@
 $(call declare-0p-target,$(test_suite_notice_html))
 $(call declare-0p-target,$(test_suite_notice_txt))
 
+$(call declare-1p-copy-files,$(test_suite_dynamic_config),)
+$(call declare-1p-copy-files,$(test_suite_prebuilt_tools),)
+
 # Reset all input variables
 test_suite_name :=
 test_suite_tradefed :=
diff --git a/core/version_defaults.mk b/core/version_defaults.mk
index c8b381d..af7d1c0 100644
--- a/core/version_defaults.mk
+++ b/core/version_defaults.mk
@@ -40,10 +40,10 @@
   include $(INTERNAL_BUILD_ID_MAKEFILE)
 endif
 
-DEFAULT_PLATFORM_VERSION := TP1A
+DEFAULT_PLATFORM_VERSION := UP1A
 .KATI_READONLY := DEFAULT_PLATFORM_VERSION
 MIN_PLATFORM_VERSION := TP1A
-MAX_PLATFORM_VERSION := TP1A
+MAX_PLATFORM_VERSION := UP1A
 
 # The last stable version name of the platform that was released.  During
 # development, this stays at that previous version, while the codename indicates
@@ -54,6 +54,7 @@
 # These are the current development codenames, if the build is not a final
 # release build.  If this is a final release build, it is simply "REL".
 PLATFORM_VERSION_CODENAME.TP1A := Tiramisu
+PLATFORM_VERSION_CODENAME.UP1A := UpsideDownCake
 
 # This is the user-visible version.  In a final release build it should
 # be empty to use PLATFORM_VERSION as the user-visible version.  For
@@ -85,8 +86,13 @@
 PLATFORM_BASE_SDK_EXTENSION_VERSION := 1
 .KATI_READONLY := PLATFORM_BASE_SDK_EXTENSION_VERSION
 
-# This is are all known codenames starting from Q.
-PLATFORM_VERSION_KNOWN_CODENAMES := Q R S Sv2 Tiramisu
+# This are all known codenames.
+PLATFORM_VERSION_KNOWN_CODENAMES := \
+Base Base11 Cupcake Donut Eclair Eclair01 EclairMr1 Froyo Gingerbread GingerbreadMr1 \
+Honeycomb HoneycombMr1 HoneycombMr2 IceCreamSandwich IceCreamSandwichMr1 \
+JellyBean JellyBeanMr1 JellyBeanMr2 Kitkat KitkatWatch Lollipop LollipopMr1 M N NMr1 O OMr1 P \
+Q R S Sv2 Tiramisu UpsideDownCake
+
 # Convert from space separated list to comma separated
 PLATFORM_VERSION_KNOWN_CODENAMES := \
   $(call normalize-comma-list,$(PLATFORM_VERSION_KNOWN_CODENAMES))
@@ -98,7 +104,7 @@
     #  It must be of the form "YYYY-MM-DD" on production devices.
     #  It must match one of the Android Security Patch Level strings of the Public Security Bulletins.
     #  If there is no $PLATFORM_SECURITY_PATCH set, keep it empty.
-    PLATFORM_SECURITY_PATCH := 2022-04-05
+    PLATFORM_SECURITY_PATCH := 2022-05-05
 endif
 .KATI_READONLY := PLATFORM_SECURITY_PATCH
 
diff --git a/core/version_util.mk b/core/version_util.mk
index 3a0d4b5..cbfef96 100644
--- a/core/version_util.mk
+++ b/core/version_util.mk
@@ -56,36 +56,34 @@
 # unreleased API level targetable by this branch, not just those that are valid
 # lunch targets for this branch.
 
+PLATFORM_VERSION_CODENAME := $(PLATFORM_VERSION_CODENAME.$(TARGET_PLATFORM_VERSION))
 ifndef PLATFORM_VERSION_CODENAME
-  PLATFORM_VERSION_CODENAME := $(PLATFORM_VERSION_CODENAME.$(TARGET_PLATFORM_VERSION))
-  ifndef PLATFORM_VERSION_CODENAME
-    # PLATFORM_VERSION_CODENAME falls back to TARGET_PLATFORM_VERSION
-    PLATFORM_VERSION_CODENAME := $(TARGET_PLATFORM_VERSION)
-  endif
-
-  # This is all of the *active* development codenames.
-  # This confusing name is needed because
-  # all_codenames has been baked into build.prop for ages.
-  #
-  # Should be either the same as PLATFORM_VERSION_CODENAME or a comma-separated
-  # list of additional codenames after PLATFORM_VERSION_CODENAME.
-  PLATFORM_VERSION_ALL_CODENAMES :=
-
-  # Build a list of all active code names. Avoid duplicates, and stop when we
-  # reach a codename that matches PLATFORM_VERSION_CODENAME (anything beyond
-  # that is not included in our build).
-  _versions_in_target := \
-    $(call find_and_earlier,$(ALL_VERSIONS),$(TARGET_PLATFORM_VERSION))
-  $(foreach version,$(_versions_in_target),\
-    $(eval _codename := $(PLATFORM_VERSION_CODENAME.$(version)))\
-    $(if $(filter $(_codename),$(PLATFORM_VERSION_ALL_CODENAMES)),,\
-      $(eval PLATFORM_VERSION_ALL_CODENAMES += $(_codename))))
-
-  # And convert from space separated to comma separated.
-  PLATFORM_VERSION_ALL_CODENAMES := \
-    $(subst $(space),$(comma),$(strip $(PLATFORM_VERSION_ALL_CODENAMES)))
-
+  # PLATFORM_VERSION_CODENAME falls back to TARGET_PLATFORM_VERSION
+  PLATFORM_VERSION_CODENAME := $(TARGET_PLATFORM_VERSION)
 endif
+
+# This is all of the *active* development codenames.
+# This confusing name is needed because
+# all_codenames has been baked into build.prop for ages.
+#
+# Should be either the same as PLATFORM_VERSION_CODENAME or a comma-separated
+# list of additional codenames after PLATFORM_VERSION_CODENAME.
+PLATFORM_VERSION_ALL_CODENAMES :=
+
+# Build a list of all active code names. Avoid duplicates, and stop when we
+# reach a codename that matches PLATFORM_VERSION_CODENAME (anything beyond
+# that is not included in our build).
+_versions_in_target := \
+  $(call find_and_earlier,$(ALL_VERSIONS),$(TARGET_PLATFORM_VERSION))
+$(foreach version,$(_versions_in_target),\
+  $(eval _codename := $(PLATFORM_VERSION_CODENAME.$(version)))\
+  $(if $(filter $(_codename),$(PLATFORM_VERSION_ALL_CODENAMES)),,\
+    $(eval PLATFORM_VERSION_ALL_CODENAMES += $(_codename))))
+
+# And convert from space separated to comma separated.
+PLATFORM_VERSION_ALL_CODENAMES := \
+  $(subst $(space),$(comma),$(strip $(PLATFORM_VERSION_ALL_CODENAMES)))
+
 .KATI_READONLY := \
   PLATFORM_VERSION_CODENAME \
   PLATFORM_VERSION_ALL_CODENAMES
diff --git a/envsetup.sh b/envsetup.sh
index e7b8538..b079d41 100644
--- a/envsetup.sh
+++ b/envsetup.sh
@@ -395,7 +395,7 @@
     fi
 
     local completion_files=(
-      system/core/adb/adb.bash
+      packages/modules/adb/adb.bash
       system/core/fastboot/fastboot.bash
       tools/asuite/asuite.sh
     )
@@ -404,7 +404,9 @@
     # ENVSETUP_NO_COMPLETION=adb # -> disable adb completion
     # ENVSETUP_NO_COMPLETION=adb:bit # -> disable adb and bit completion
     for f in ${completion_files[*]}; do
-        if [ -f "$f" ] && should_add_completion "$f"; then
+        if [ ! -f "$f" ]; then
+          echo "Warning: completion file $f not found"
+        elif should_add_completion "$f"; then
             . $f
         fi
     done
@@ -454,7 +456,7 @@
     if $(echo "$1" | grep -q '^-') ; then
         # Calls starting with a -- argument are passed directly and the function
         # returns with the lunch.py exit code.
-        build/make/orchestrator/core/lunch.py "$@"
+        build/build/make/orchestrator/core/lunch.py "$@"
         code=$?
         if [[ $code -eq 2 ]] ; then
           echo 1>&2
@@ -465,7 +467,7 @@
         fi
     else
         # All other calls go through the --lunch variant of lunch.py
-        results=($(build/make/orchestrator/core/lunch.py --lunch "$@"))
+        results=($(build/build/make/orchestrator/core/lunch.py --lunch "$@"))
         code=$?
         if [[ $code -eq 2 ]] ; then
           echo 1>&2
@@ -942,6 +944,34 @@
     fi
 }
 
+# TODO: Merge into gettop as part of launching multitree
+function multitree_gettop
+{
+    local TOPFILE=build/build/make/core/envsetup.mk
+    if [ -n "$TOP" -a -f "$TOP/$TOPFILE" ] ; then
+        # The following circumlocution ensures we remove symlinks from TOP.
+        (cd "$TOP"; PWD= /bin/pwd)
+    else
+        if [ -f $TOPFILE ] ; then
+            # The following circumlocution (repeated below as well) ensures
+            # that we record the true directory name and not one that is
+            # faked up with symlink names.
+            PWD= /bin/pwd
+        else
+            local HERE=$PWD
+            local T=
+            while [ \( ! \( -f $TOPFILE \) \) -a \( "$PWD" != "/" \) ]; do
+                \cd ..
+                T=`PWD= /bin/pwd -P`
+            done
+            \cd "$HERE"
+            if [ -f "$T/$TOPFILE" ]; then
+                echo "$T"
+            fi
+        fi
+    fi
+}
+
 function croot()
 {
     local T=$(gettop)
@@ -1824,6 +1854,21 @@
     _wrap_build $(get_make_command "$@") "$@"
 }
 
+function _multitree_lunch_error()
+{
+      >&2 echo "Couldn't locate the top of the tree. Please run \'source build/envsetup.sh\' and multitree_lunch from the root of your workspace."
+}
+
+function multitree_build()
+{
+    if T="$(multitree_gettop)"; then
+      "$T/build/build/orchestrator/core/orchestrator.py" "$@"
+    else
+      _multitree_lunch_error
+      return 1
+    fi
+}
+
 function provision()
 {
     if [ ! "$ANDROID_PRODUCT_OUT" ]; then
diff --git a/finalize_branch_for_release.sh b/finalize_branch_for_release.sh
new file mode 100755
index 0000000..c942eb2
--- /dev/null
+++ b/finalize_branch_for_release.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+set -e
+
+source "$(dirname "$0")"/envsetup.sh
+
+# default target to modify tree and build SDK
+lunch aosp_arm64-userdebug
+
+set -x
+
+# This script is WIP and only finalizes part of the Android branch for release.
+# The full process can be found at (INTERNAL) go/android-sdk-finalization.
+
+# VNDK snapshot (TODO)
+# SDK snapshots (TODO)
+# Update references in the codebase to new API version (TODO)
+# ...
+
+AIDL_TRANSITIVE_FREEZE=true m aidl-freeze-api
+
+m check-vndk-list || update-vndk-list.sh # for new versions of AIDL interfaces
+
+# for now, we simulate the release state for AIDL, but in the future, we would want
+# to actually turn the branch into the REL state and test with that
+AIDL_FROZEN_REL=true m # test build
+
+# Build SDK (TODO)
+# lunch sdk...
+# m ...
diff --git a/orchestrator/README b/orchestrator/README
new file mode 100644
index 0000000..ce6f5c3
--- /dev/null
+++ b/orchestrator/README
@@ -0,0 +1,7 @@
+DEMO
+
+from the root of the workspace
+
+ln -fs ../build/build/orchestrator/inner_build/inner_build_demo.py master/.inner_build
+ln -fs ../build/build/orchestrator/inner_build/inner_build_demo.py sc-mainline-prod/.inner_build
+
diff --git a/orchestrator/core/api_assembly.py b/orchestrator/core/api_assembly.py
new file mode 100644
index 0000000..d87a83d
--- /dev/null
+++ b/orchestrator/core/api_assembly.py
@@ -0,0 +1,151 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+
+def assemble_apis(inner_trees):
+
+    # Find all of the contributions from the inner tree
+    contribution_files_dict = inner_trees.for_each_tree(api_contribution_files_for_inner_tree)
+
+    # Load and validate the contribution files
+    # TODO: Check timestamps and skip unnecessary work
+    contributions = []
+    for tree_key, filenames in contribution_files_dict.items():
+        for filename in filenames:
+            contribution_data = load_contribution_file(filename)
+            if not contribution_data:
+                continue
+            # TODO: Validate the configs, especially that the domains match what we asked for
+            # from the lunch config.
+            contributions.append(contribution_data)
+
+    # Group contributions by language and API surface
+    stub_libraries = collate_contributions(contributions)
+
+    # Iterate through all of the stub libraries and generate rules to assemble them
+    # and Android.bp/BUILD files to make those available to inner trees.
+    # TODO: Parallelize? Skip unnecessary work?
+    ninja_file = NinjaFile() # TODO: parameters?
+    build_file = BuildFile() # TODO: parameters?
+    for stub_library in stub_libraries:
+        STUB_LANGUAGE_HANDLERS[stub_library.language](ninja_file, build_file, stub_library)
+
+    # TODO: Handle host_executables separately or as a StubLibrary language?
+
+
+def api_contribution_files_for_inner_tree(tree_key, inner_tree, cookie):
+    "Scan an inner_tree's out dir for the api contribution files."
+    directory = inner_tree.out.api_contributions_dir()
+    result = []
+    with os.scandir(directory) as it:
+        for dirent in it:
+            if not dirent.is_file():
+                break
+            if dirent.name.endswith(".json"):
+                result.append(os.path.join(directory, dirent.name))
+    return result
+
+
+def load_contribution_file(filename):
+    "Load and return the API contribution at filename. On error report error and return None."
+    with open(filename) as f:
+        try:
+            return json.load(f)
+        except json.decoder.JSONDecodeError as ex:
+            # TODO: Error reporting
+            raise ex
+
+
+class StubLibraryContribution(object):
+    def __init__(self, api_domain, library_contribution):
+        self.api_domain = api_domain
+        self.library_contribution = library_contribution
+
+
+class StubLibrary(object):
+    def __init__(self, language, api_surface, api_surface_version, name):
+        self.language = language
+        self.api_surface = api_surface
+        self.api_surface_version = api_surface_version
+        self.name = name
+        self.contributions = []
+
+    def add_contribution(self, contrib):
+        self.contributions.append(contrib)
+
+
+def collate_contributions(contributions):
+    """Take the list of parsed API contribution files, and group targets by API Surface, version,
+    language and library name, and return a StubLibrary object for each of those.
+    """
+    grouped = {}
+    for contribution in contributions:
+        for language in STUB_LANGUAGE_HANDLERS.keys():
+            for library in contribution.get(language, []):
+                key = (language, contribution["name"], contribution["version"], library["name"])
+                stub_library = grouped.get(key)
+                if not stub_library:
+                    stub_library = StubLibrary(language, contribution["name"],
+                            contribution["version"], library["name"])
+                    grouped[key] = stub_library
+                stub_library.add_contribution(StubLibraryContribution(
+                        contribution["api_domain"], library))
+    return list(grouped.values())
+
+
+def assemble_cc_api_library(ninja_file, build_file, stub_library):
+    print("assembling cc_api_library %s-%s %s from:" % (stub_library.api_surface, stub_library.api_surface_version,
+            stub_library.name))
+    for contrib in stub_library.contributions:
+        print("  %s %s" % (contrib.api_domain, contrib.library_contribution["api"]))
+    # TODO: Implement me
+
+
+def assemble_java_api_library(ninja_file, build_file, stub_library):
+    print("assembling java_api_library %s-%s %s from:" % (stub_library.api_surface, stub_library.api_surface_version,
+            stub_library.name))
+    for contrib in stub_library.contributions:
+        print("  %s %s" % (contrib.api_domain, contrib.library_contribution["api"]))
+    # TODO: Implement me
+
+
+def assemble_resource_api_library(ninja_file, build_file, stub_library):
+    print("assembling resource_api_library %s-%s %s from:" % (stub_library.api_surface, stub_library.api_surface_version,
+            stub_library.name))
+    for contrib in stub_library.contributions:
+        print("  %s %s" % (contrib.api_domain, contrib.library_contribution["api"]))
+    # TODO: Implement me
+
+
+STUB_LANGUAGE_HANDLERS = {
+    "cc_libraries": assemble_cc_api_library,
+    "java_libraries": assemble_java_api_library,
+    "resource_libraries": assemble_resource_api_library,
+}
+
+
+class NinjaFile(object):
+    "Generator for build actions and dependencies."
+    pass
+
+
+class BuildFile(object):
+    "Abstract generator for Android.bp files and BUILD files."
+    pass
+
+
diff --git a/orchestrator/core/api_domain.py b/orchestrator/core/api_domain.py
new file mode 100644
index 0000000..bb7306c
--- /dev/null
+++ b/orchestrator/core/api_domain.py
@@ -0,0 +1,28 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2022 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.
+
+class ApiDomain(object):
+    def __init__(self, name, tree, product):
+        # Product will be null for modules
+        self.name = name
+        self.tree = tree
+        self.product = product
+
+    def __str__(self):
+        return "ApiDomain(name=\"%s\" tree.root=\"%s\" product=%s)" % (
+                self.name, self.tree.root,
+                "None" if self.product is None else "\"%s\"" % self.product)
+
diff --git a/orchestrator/core/api_export.py b/orchestrator/core/api_export.py
new file mode 100644
index 0000000..2f26b02
--- /dev/null
+++ b/orchestrator/core/api_export.py
@@ -0,0 +1,20 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2022 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.
+
+def export_apis_from_tree(tree_key, inner_tree, cookie):
+    inner_tree.invoke(["export_api_contributions"])
+
+
diff --git a/orchestrator/core/inner_tree.py b/orchestrator/core/inner_tree.py
new file mode 100644
index 0000000..cdb0d85
--- /dev/null
+++ b/orchestrator/core/inner_tree.py
@@ -0,0 +1,155 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import subprocess
+import sys
+import textwrap
+
+class InnerTreeKey(object):
+    """Trees are identified uniquely by their root and the TARGET_PRODUCT they will use to build.
+    If a single tree uses two different prdoucts, then we won't make assumptions about
+    them sharing _anything_.
+    TODO: This is true for soong. It's more likely that bazel could do analysis for two
+    products at the same time in a single tree, so there's an optimization there to do
+    eventually."""
+    def __init__(self, root, product):
+        self.root = root
+        self.product = product
+
+    def __str__(self):
+        return "TreeKey(root=%s product=%s)" % (enquote(self.root), enquote(self.product))
+
+    def __hash__(self):
+        return hash((self.root, self.product))
+
+    def __eq__(self, other):
+        return (self.root == other.root and self.product == other.product)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __lt__(self, other):
+        return (self.root, self.product) < (other.root, other.product)
+
+    def __le__(self, other):
+        return (self.root, self.product) <= (other.root, other.product)
+
+    def __gt__(self, other):
+        return (self.root, self.product) > (other.root, other.product)
+
+    def __ge__(self, other):
+        return (self.root, self.product) >= (other.root, other.product)
+
+
+class InnerTree(object):
+    def __init__(self, root, product):
+        """Initialize with the inner tree root (relative to the workspace root)"""
+        self.root = root
+        self.product = product
+        self.domains = {}
+        # TODO: Base directory on OUT_DIR
+        self.out = OutDirLayout(os.path.join("out", "trees", root))
+
+    def __str__(self):
+        return "InnerTree(root=%s product=%s domains=[%s])" % (enquote(self.root),
+                enquote(self.product),
+                " ".join([enquote(d) for d in sorted(self.domains.keys())]))
+
+    def invoke(self, args):
+        """Call the inner tree command for this inner tree. Exits on failure."""
+        # TODO: Build time tracing
+
+        # Validate that there is a .inner_build command to run at the root of the tree
+        # so we can print a good error message
+        inner_build_tool = os.path.join(self.root, ".inner_build")
+        if not os.access(inner_build_tool, os.X_OK):
+            sys.stderr.write(("Unable to execute %s. Is there an inner tree or lunch combo"
+                    + " misconfiguration?\n") % inner_build_tool)
+            sys.exit(1)
+
+        # TODO: This is where we should set up the shared trees
+
+        # Build the command
+        cmd = [inner_build_tool, "--out_dir", self.out.root()]
+        for domain_name in sorted(self.domains.keys()):
+            cmd.append("--api_domain")
+            cmd.append(domain_name)
+        cmd += args
+
+        # Run the command
+        process = subprocess.run(cmd, shell=False)
+
+        # TODO: Probably want better handling of inner tree failures
+        if process.returncode:
+            sys.stderr.write("Build error in inner tree: %s\nstopping multitree build.\n"
+                    % self.root)
+            sys.exit(1)
+
+
+class InnerTrees(object):
+    def __init__(self, trees, domains):
+        self.trees = trees
+        self.domains = domains
+
+    def __str__(self):
+        "Return a debugging dump of this object"
+        return textwrap.dedent("""\
+        InnerTrees {
+            trees: [
+                %(trees)s
+            ]
+            domains: [
+                %(domains)s
+            ]
+        }""" % {
+            "trees": "\n        ".join(sorted([str(t) for t in self.trees.values()])),
+            "domains": "\n        ".join(sorted([str(d) for d in self.domains.values()])),
+        })
+
+
+    def for_each_tree(self, func, cookie=None):
+        """Call func for each of the inner trees once for each product that will be built in it.
+
+        The calls will be in a stable order.
+
+        Return a map of the InnerTreeKey to any results returned from func().
+        """
+        result = {}
+        for key in sorted(self.trees.keys()):
+            result[key] = func(key, self.trees[key], cookie)
+        return result
+
+
+class OutDirLayout(object):
+    def __init__(self, root):
+        "Initialize with the root of the OUT_DIR for the inner tree."
+        self._root = root
+
+    def root(self):
+        return self._root
+
+    def tree_info_file(self):
+        return os.path.join(self._root, "tree_info.json")
+
+    def api_contributions_dir(self):
+        return os.path.join(self._root, "api_contributions")
+
+
+def enquote(s):
+    return "None" if s is None else "\"%s\"" % s
+
+
diff --git a/orchestrator/core/interrogate.py b/orchestrator/core/interrogate.py
new file mode 100644
index 0000000..9fe769e
--- /dev/null
+++ b/orchestrator/core/interrogate.py
@@ -0,0 +1,29 @@
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+
+def interrogate_tree(tree_key, inner_tree, cookie):
+    inner_tree.invoke(["describe"])
+
+    info_json_filename = inner_tree.out.tree_info_file()
+
+    # TODO: Error handling
+    with open(info_json_filename) as f:
+        info_json = json.load(f)
+
+    # TODO: Check orchestrator protocol
+
diff --git a/orchestrator/core/lunch.py b/orchestrator/core/lunch.py
index 35dac73..a648478 100755
--- a/orchestrator/core/lunch.py
+++ b/orchestrator/core/lunch.py
@@ -24,8 +24,10 @@
 EXIT_STATUS_ERROR = 1
 EXIT_STATUS_NEED_HELP = 2
 
-def FindDirs(path, name, ttl=6):
-    """Search at most ttl directories deep inside path for a directory called name."""
+
+def find_dirs(path, name, ttl=6):
+    """Search at most ttl directories deep inside path for a directory called name
+    and yield directories that match."""
     # The dance with subdirs is so that we recurse in sorted order.
     subdirs = []
     with os.scandir(path) as it:
@@ -40,10 +42,10 @@
                 # Consume filesystem errors, e.g. too many links, permission etc.
                 pass
     for subdir in subdirs:
-        yield from FindDirs(os.path.join(path, subdir), name, ttl-1)
+        yield from find_dirs(os.path.join(path, subdir), name, ttl-1)
 
 
-def WalkPaths(path, matcher, ttl=10):
+def walk_paths(path, matcher, ttl=10):
     """Do a traversal of all files under path yielding each file that matches
     matcher."""
     # First look for files, then recurse into directories as needed.
@@ -62,22 +64,22 @@
                 # Consume filesystem errors, e.g. too many links, permission etc.
                 pass
     for subdir in sorted(subdirs):
-        yield from WalkPaths(os.path.join(path, subdir), matcher, ttl-1)
+        yield from walk_paths(os.path.join(path, subdir), matcher, ttl-1)
 
 
-def FindFile(path, filename):
+def find_file(path, filename):
     """Return a file called filename inside path, no more than ttl levels deep.
 
     Directories are searched alphabetically.
     """
-    for f in WalkPaths(path, lambda x: x == filename):
+    for f in walk_paths(path, lambda x: x == filename):
         return f
 
 
-def FindConfigDirs(workspace_root):
+def find_config_dirs(workspace_root):
     """Find the configuration files in the well known locations inside workspace_root
 
-        <workspace_root>/build/orchestrator/multitree_combos
+        <workspace_root>/build/build/orchestrator/multitree_combos
            (AOSP devices, such as cuttlefish)
 
         <workspace_root>/vendor/**/multitree_combos
@@ -89,29 +91,30 @@
     Directories are returned specifically in this order, so that aosp can't be
     overridden, but vendor overrides device.
     """
+    # TODO: This is not looking in inner trees correctly.
 
     # TODO: When orchestrator is in its own git project remove the "make/" here
-    yield os.path.join(workspace_root, "build/make/orchestrator/multitree_combos")
+    yield os.path.join(workspace_root, "build/build/make/orchestrator/multitree_combos")
 
     dirs = ["vendor", "device"]
     for d in dirs:
-        yield from FindDirs(os.path.join(workspace_root, d), "multitree_combos")
+        yield from find_dirs(os.path.join(workspace_root, d), "multitree_combos")
 
 
-def FindNamedConfig(workspace_root, shortname):
+def find_named_config(workspace_root, shortname):
     """Find the config with the given shortname inside workspace_root.
 
-    Config directories are searched in the order described in FindConfigDirs,
+    Config directories are searched in the order described in find_config_dirs,
     and inside those directories, alphabetically."""
     filename = shortname + ".mcombo"
-    for config_dir in FindConfigDirs(workspace_root):
-        found = FindFile(config_dir, filename)
+    for config_dir in find_config_dirs(workspace_root):
+        found = find_file(config_dir, filename)
         if found:
             return found
     return None
 
 
-def ParseProductVariant(s):
+def parse_product_variant(s):
     """Split a PRODUCT-VARIANT name, or return None if it doesn't match that pattern."""
     split = s.split("-")
     if len(split) != 2:
@@ -119,15 +122,15 @@
     return split
 
 
-def ChooseConfigFromArgs(workspace_root, args):
+def choose_config_from_args(workspace_root, args):
     """Return the config file we should use for the given argument,
     or null if there's no file that matches that."""
     if len(args) == 1:
         # Prefer PRODUCT-VARIANT syntax so if there happens to be a matching
         # file we don't match that.
-        pv = ParseProductVariant(args[0])
+        pv = parse_product_variant(args[0])
         if pv:
-            config = FindNamedConfig(workspace_root, pv[0])
+            config = find_named_config(workspace_root, pv[0])
             if config:
                 return (config, pv[1])
             return None, None
@@ -139,10 +142,12 @@
 
 
 class ConfigException(Exception):
+    ERROR_IDENTIFY = "identify"
     ERROR_PARSE = "parse"
     ERROR_CYCLE = "cycle"
+    ERROR_VALIDATE = "validate"
 
-    def __init__(self, kind, message, locations, line=0):
+    def __init__(self, kind, message, locations=[], line=0):
         """Error thrown when loading and parsing configurations.
 
         Args:
@@ -169,13 +174,13 @@
         self.line = line
 
 
-def LoadConfig(filename):
+def load_config(filename):
     """Load a config, including processing the inherits fields.
 
     Raises:
         ConfigException on errors
     """
-    def LoadAndMerge(fn, visited):
+    def load_and_merge(fn, visited):
         with open(fn) as f:
             try:
                 contents = json.load(f)
@@ -191,34 +196,74 @@
                 if parent in visited:
                     raise ConfigException(ConfigException.ERROR_CYCLE, "Cycle detected in inherits",
                             visited)
-                DeepMerge(inherited_data, LoadAndMerge(parent, [parent,] + visited))
+                deep_merge(inherited_data, load_and_merge(parent, [parent,] + visited))
             # Then merge inherited_data into contents, but what's already there will win.
-            DeepMerge(contents, inherited_data)
+            deep_merge(contents, inherited_data)
             contents.pop("inherits", None)
         return contents
-    return LoadAndMerge(filename, [filename,])
+    return load_and_merge(filename, [filename,])
 
 
-def DeepMerge(merged, addition):
+def deep_merge(merged, addition):
     """Merge all fields of addition into merged. Pre-existing fields win."""
     for k, v in addition.items():
         if k in merged:
             if isinstance(v, dict) and isinstance(merged[k], dict):
-                DeepMerge(merged[k], v)
+                deep_merge(merged[k], v)
         else:
             merged[k] = v
 
 
-def Lunch(args):
+def make_config_header(config_file, config, variant):
+    def make_table(rows):
+        maxcols = max([len(row) for row in rows])
+        widths = [0] * maxcols
+        for row in rows:
+            for i in range(len(row)):
+                widths[i] = max(widths[i], len(row[i]))
+        text = []
+        for row in rows:
+            rowtext = []
+            for i in range(len(row)):
+                cell = row[i]
+                rowtext.append(str(cell))
+                rowtext.append(" " * (widths[i] - len(cell)))
+                rowtext.append("  ")
+            text.append("".join(rowtext))
+        return "\n".join(text)
+
+    trees = [("Component", "Path", "Product"),
+             ("---------", "----", "-------")]
+    entry = config.get("system", None)
+    def add_config_tuple(trees, entry, name):
+        if entry:
+            trees.append((name, entry.get("tree"), entry.get("product", "")))
+    add_config_tuple(trees, config.get("system"), "system")
+    add_config_tuple(trees, config.get("vendor"), "vendor")
+    for k, v in config.get("modules", {}).items():
+        add_config_tuple(trees, v, k)
+
+    return """========================================
+TARGET_BUILD_COMBO=%(TARGET_BUILD_COMBO)s
+TARGET_BUILD_VARIANT=%(TARGET_BUILD_VARIANT)s
+
+%(trees)s
+========================================\n""" % {
+        "TARGET_BUILD_COMBO": config_file,
+        "TARGET_BUILD_VARIANT": variant,
+        "trees": make_table(trees),
+    }
+
+
+def do_lunch(args):
     """Handle the lunch command."""
-    # Check that we're at the top of a multitree workspace
-    # TODO: Choose the right sentinel file
-    if not os.path.exists("build/make/orchestrator"):
+    # Check that we're at the top of a multitree workspace by seeing if this script exists.
+    if not os.path.exists("build/build/make/orchestrator/core/lunch.py"):
         sys.stderr.write("ERROR: lunch.py must be run from the root of a multi-tree workspace\n")
         return EXIT_STATUS_ERROR
 
     # Choose the config file
-    config_file, variant = ChooseConfigFromArgs(".", args)
+    config_file, variant = choose_config_from_args(".", args)
 
     if config_file == None:
         sys.stderr.write("Can't find lunch combo file for: %s\n" % " ".join(args))
@@ -229,7 +274,7 @@
 
     # Parse the config file
     try:
-        config = LoadConfig(config_file)
+        config = load_config(config_file)
     except ConfigException as ex:
         sys.stderr.write(str(ex))
         return EXIT_STATUS_ERROR
@@ -244,47 +289,81 @@
     sys.stdout.write("%s\n" % config_file)
     sys.stdout.write("%s\n" % variant)
 
+    # Write confirmation message to stderr
+    sys.stderr.write(make_config_header(config_file, config, variant))
+
     return EXIT_STATUS_OK
 
 
-def FindAllComboFiles(workspace_root):
+def find_all_combo_files(workspace_root):
     """Find all .mcombo files in the prescribed locations in the tree."""
-    for dir in FindConfigDirs(workspace_root):
-        for file in WalkPaths(dir, lambda x: x.endswith(".mcombo")):
+    for dir in find_config_dirs(workspace_root):
+        for file in walk_paths(dir, lambda x: x.endswith(".mcombo")):
             yield file
 
 
-def IsFileLunchable(config_file):
+def is_file_lunchable(config_file):
     """Parse config_file, flatten the inheritance, and return whether it can be
     used as a lunch target."""
     try:
-        config = LoadConfig(config_file)
+        config = load_config(config_file)
     except ConfigException as ex:
         sys.stderr.write("%s" % ex)
         return False
     return config.get("lunchable", False)
 
 
-def FindAllLunchable(workspace_root):
+def find_all_lunchable(workspace_root):
     """Find all mcombo files in the tree (rooted at workspace_root) that when
     parsed (and inheritance is flattened) have lunchable: true."""
-    for f in [x for x in FindAllComboFiles(workspace_root) if IsFileLunchable(x)]:
+    for f in [x for x in find_all_combo_files(workspace_root) if is_file_lunchable(x)]:
         yield f
 
 
-def List():
+def load_current_config():
+    """Load, validate and return the config as specified in TARGET_BUILD_COMBO.  Throws
+    ConfigException if there is a problem."""
+
+    # Identify the config file
+    config_file = os.environ.get("TARGET_BUILD_COMBO")
+    if not config_file:
+        raise ConfigException(ConfigException.ERROR_IDENTIFY,
+                "TARGET_BUILD_COMBO not set. Run lunch or pass a combo file.")
+
+    # Parse the config file
+    config = load_config(config_file)
+
+    # Validate the config file
+    if not config.get("lunchable", False):
+        raise ConfigException(ConfigException.ERROR_VALIDATE,
+                "Lunch config file (or inherited files) does not have the 'lunchable'"
+                    + " flag set, which means it is probably not a complete lunch spec.",
+                [config_file,])
+
+    # TODO: Validate that:
+    #   - there are no modules called system or vendor
+    #   - everything has all the required files
+
+    variant = os.environ.get("TARGET_BUILD_VARIANT")
+    if not variant:
+        variant = "eng" # TODO: Is this the right default?
+    # Validate variant is user, userdebug or eng
+
+    return config_file, config, variant
+
+def do_list():
     """Handle the --list command."""
-    for f in sorted(FindAllLunchable(".")):
+    for f in sorted(find_all_lunchable(".")):
         print(f)
 
 
-def Print(args):
+def do_print(args):
     """Handle the --print command."""
     # Parse args
     if len(args) == 0:
         config_file = os.environ.get("TARGET_BUILD_COMBO")
         if not config_file:
-            sys.stderr.write("TARGET_BUILD_COMBO not set. Run lunch or pass a combo file.\n")
+            sys.stderr.write("TARGET_BUILD_COMBO not set. Run lunch before building.\n")
             return EXIT_STATUS_NEED_HELP
     elif len(args) == 1:
         config_file = args[0]
@@ -293,7 +372,7 @@
 
     # Parse the config file
     try:
-        config = LoadConfig(config_file)
+        config = load_config(config_file)
     except ConfigException as ex:
         sys.stderr.write(str(ex))
         return EXIT_STATUS_ERROR
@@ -309,15 +388,15 @@
         return EXIT_STATUS_NEED_HELP
 
     if len(argv) == 2 and argv[1] == "--list":
-        List()
+        do_list()
         return EXIT_STATUS_OK
 
     if len(argv) == 2 and argv[1] == "--print":
-        return Print(argv[2:])
+        return do_print(argv[2:])
         return EXIT_STATUS_OK
 
-    if (len(argv) == 2 or len(argv) == 3) and argv[1] == "--lunch":
-        return Lunch(argv[2:])
+    if (len(argv) == 3 or len(argv) == 4) and argv[1] == "--lunch":
+        return do_lunch(argv[2:])
 
     sys.stderr.write("Unknown lunch command: %s\n" % " ".join(argv[1:]))
     return EXIT_STATUS_NEED_HELP
diff --git a/orchestrator/core/orchestrator.py b/orchestrator/core/orchestrator.py
new file mode 100755
index 0000000..e99c956
--- /dev/null
+++ b/orchestrator/core/orchestrator.py
@@ -0,0 +1,123 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import subprocess
+import sys
+
+sys.dont_write_bytecode = True
+import api_assembly
+import api_domain
+import api_export
+import inner_tree
+import interrogate
+import lunch
+
+EXIT_STATUS_OK = 0
+EXIT_STATUS_ERROR = 1
+
+API_DOMAIN_SYSTEM = "system"
+API_DOMAIN_VENDOR = "vendor"
+API_DOMAIN_MODULE = "module"
+
+def process_config(lunch_config):
+    """Returns a InnerTrees object based on the configuration requested in the lunch config."""
+    def add(domain_name, tree_root, product):
+        tree_key = inner_tree.InnerTreeKey(tree_root, product)
+        if tree_key in trees:
+            tree = trees[tree_key]
+        else:
+            tree = inner_tree.InnerTree(tree_root, product)
+            trees[tree_key] = tree
+        domain = api_domain.ApiDomain(domain_name, tree, product)
+        domains[domain_name] = domain
+        tree.domains[domain_name] = domain
+
+    trees = {}
+    domains = {}
+
+    system_entry = lunch_config.get("system")
+    if system_entry:
+        add(API_DOMAIN_SYSTEM, system_entry["tree"], system_entry["product"])
+
+    vendor_entry = lunch_config.get("vendor")
+    if vendor_entry:
+        add(API_DOMAIN_VENDOR, vendor_entry["tree"], vendor_entry["product"])
+
+    for module_name, module_entry in lunch_config.get("modules", []).items():
+        add(module_name, module_entry["tree"], None)
+
+    return inner_tree.InnerTrees(trees, domains)
+
+
+def build():
+    #
+    # Load lunch combo
+    #
+
+    # Read the config file
+    try:
+        config_file, config, variant = lunch.load_current_config()
+    except lunch.ConfigException as ex:
+        sys.stderr.write("%s\n" % ex)
+        return EXIT_STATUS_ERROR
+    sys.stdout.write(lunch.make_config_header(config_file, config, variant))
+
+    # Construct the trees and domains dicts
+    inner_trees = process_config(config)
+
+    #
+    # 1. Interrogate the trees
+    #
+    inner_trees.for_each_tree(interrogate.interrogate_tree)
+    # TODO: Detect bazel-only mode
+
+    #
+    # 2a. API Export
+    #
+    inner_trees.for_each_tree(api_export.export_apis_from_tree)
+
+    #
+    # 2b. API Surface Assembly
+    #
+    api_assembly.assemble_apis(inner_trees)
+
+    #
+    # 3a. API Domain Analysis
+    #
+
+    #
+    # 3b. Final Packaging Rules
+    #
+
+    #
+    # 4. Build Execution
+    #
+
+
+    #
+    # Success!
+    #
+    return EXIT_STATUS_OK
+
+def main(argv):
+    return build()
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
+
+
+# vim: sts=4:ts=4:sw=4
diff --git a/orchestrator/core/test_lunch.py b/orchestrator/core/test_lunch.py
index 3c39493..2d85d05 100755
--- a/orchestrator/core/test_lunch.py
+++ b/orchestrator/core/test_lunch.py
@@ -23,73 +23,73 @@
 class TestStringMethods(unittest.TestCase):
 
     def test_find_dirs(self):
-        self.assertEqual([x for x in lunch.FindDirs("test/configs", "multitree_combos")], [
+        self.assertEqual([x for x in lunch.find_dirs("test/configs", "multitree_combos")], [
                     "test/configs/build/make/orchestrator/multitree_combos",
                     "test/configs/device/aa/bb/multitree_combos",
                     "test/configs/vendor/aa/bb/multitree_combos"])
 
     def test_find_file(self):
         # Finds the one in device first because this is searching from the root,
-        # not using FindNamedConfig.
-        self.assertEqual(lunch.FindFile("test/configs", "v.mcombo"),
+        # not using find_named_config.
+        self.assertEqual(lunch.find_file("test/configs", "v.mcombo"),
                    "test/configs/device/aa/bb/multitree_combos/v.mcombo")
 
     def test_find_config_dirs(self):
-        self.assertEqual([x for x in lunch.FindConfigDirs("test/configs")], [
+        self.assertEqual([x for x in lunch.find_config_dirs("test/configs")], [
                     "test/configs/build/make/orchestrator/multitree_combos",
                     "test/configs/vendor/aa/bb/multitree_combos",
                     "test/configs/device/aa/bb/multitree_combos"])
 
     def test_find_named_config(self):
         # Inside build/orchestrator, overriding device and vendor
-        self.assertEqual(lunch.FindNamedConfig("test/configs", "b"),
+        self.assertEqual(lunch.find_named_config("test/configs", "b"),
                     "test/configs/build/make/orchestrator/multitree_combos/b.mcombo")
 
         # Nested dir inside a combo dir
-        self.assertEqual(lunch.FindNamedConfig("test/configs", "nested"),
+        self.assertEqual(lunch.find_named_config("test/configs", "nested"),
                     "test/configs/build/make/orchestrator/multitree_combos/nested/nested.mcombo")
 
         # Inside vendor, overriding device
-        self.assertEqual(lunch.FindNamedConfig("test/configs", "v"),
+        self.assertEqual(lunch.find_named_config("test/configs", "v"),
                     "test/configs/vendor/aa/bb/multitree_combos/v.mcombo")
 
         # Inside device
-        self.assertEqual(lunch.FindNamedConfig("test/configs", "d"),
+        self.assertEqual(lunch.find_named_config("test/configs", "d"),
                     "test/configs/device/aa/bb/multitree_combos/d.mcombo")
 
         # Make sure we don't look too deep (for performance)
-        self.assertIsNone(lunch.FindNamedConfig("test/configs", "too_deep"))
+        self.assertIsNone(lunch.find_named_config("test/configs", "too_deep"))
 
 
     def test_choose_config_file(self):
         # Empty string argument
-        self.assertEqual(lunch.ChooseConfigFromArgs("test/configs", [""]),
+        self.assertEqual(lunch.choose_config_from_args("test/configs", [""]),
                     (None, None))
 
         # A PRODUCT-VARIANT name
-        self.assertEqual(lunch.ChooseConfigFromArgs("test/configs", ["v-eng"]),
+        self.assertEqual(lunch.choose_config_from_args("test/configs", ["v-eng"]),
                     ("test/configs/vendor/aa/bb/multitree_combos/v.mcombo", "eng"))
 
         # A PRODUCT-VARIANT name that conflicts with a file
-        self.assertEqual(lunch.ChooseConfigFromArgs("test/configs", ["b-eng"]),
+        self.assertEqual(lunch.choose_config_from_args("test/configs", ["b-eng"]),
                     ("test/configs/build/make/orchestrator/multitree_combos/b.mcombo", "eng"))
 
         # A PRODUCT-VARIANT that doesn't exist
-        self.assertEqual(lunch.ChooseConfigFromArgs("test/configs", ["z-user"]),
+        self.assertEqual(lunch.choose_config_from_args("test/configs", ["z-user"]),
                     (None, None))
 
         # An explicit file
-        self.assertEqual(lunch.ChooseConfigFromArgs("test/configs",
+        self.assertEqual(lunch.choose_config_from_args("test/configs",
                         ["test/configs/build/make/orchestrator/multitree_combos/b.mcombo", "eng"]),
                     ("test/configs/build/make/orchestrator/multitree_combos/b.mcombo", "eng"))
 
         # An explicit file that doesn't exist
-        self.assertEqual(lunch.ChooseConfigFromArgs("test/configs",
+        self.assertEqual(lunch.choose_config_from_args("test/configs",
                         ["test/configs/doesnt_exist.mcombo", "eng"]),
                     (None, None))
 
         # An explicit file without a variant should fail
-        self.assertEqual(lunch.ChooseConfigFromArgs("test/configs",
+        self.assertEqual(lunch.choose_config_from_args("test/configs",
                         ["test/configs/build/make/orchestrator/multitree_combos/b.mcombo"]),
                     ("test/configs/build/make/orchestrator/multitree_combos/b.mcombo", None))
 
@@ -97,12 +97,12 @@
     def test_config_cycles(self):
         # Test that we catch cycles
         with self.assertRaises(lunch.ConfigException) as context:
-            lunch.LoadConfig("test/configs/parsing/cycles/1.mcombo")
+            lunch.load_config("test/configs/parsing/cycles/1.mcombo")
         self.assertEqual(context.exception.kind, lunch.ConfigException.ERROR_CYCLE)
 
     def test_config_merge(self):
         # Test the merge logic
-        self.assertEqual(lunch.LoadConfig("test/configs/parsing/merge/1.mcombo"), {
+        self.assertEqual(lunch.load_config("test/configs/parsing/merge/1.mcombo"), {
                             "in_1": "1",
                             "in_1_2": "1",
                             "merged": {"merged_1": "1",
@@ -119,7 +119,7 @@
                         })
 
     def test_list(self):
-        self.assertEqual(sorted(lunch.FindAllLunchable("test/configs")),
+        self.assertEqual(sorted(lunch.find_all_lunchable("test/configs")),
                 ["test/configs/build/make/orchestrator/multitree_combos/b.mcombo"])
 
 if __name__ == "__main__":
diff --git a/orchestrator/demo/buffet_helper.py b/orchestrator/demo/buffet_helper.py
new file mode 100644
index 0000000..fa29aeb
--- /dev/null
+++ b/orchestrator/demo/buffet_helper.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+import os
+import sys
+import yaml
+
+from hierarchy import parse_hierarchy
+
+
+def main():
+  if len(sys.argv) != 2:
+    print('usage: %s target' % sys.argv[0])
+    exit(1)
+
+  args = sys.argv[1].split('-')
+  if len(args) != 2:
+    print('target format: {target}-{variant}')
+    exit(1)
+
+  target, variant = args
+
+  if variant not in ['eng', 'user', 'userdebug']:
+    print('unknown variant "%s": expected "eng", "user" or "userdebug"' %
+          variant)
+    exit(1)
+
+  build_top = os.getenv('BUFFET_BUILD_TOP')
+  if not build_top:
+    print('BUFFET_BUILD_TOP is not set; Did you correctly run envsetup.sh?')
+    exit(1)
+
+  hierarchy_map = parse_hierarchy(build_top)
+
+  if target not in hierarchy_map:
+    raise RuntimeError(
+        "unknown target '%s': couldn't find the target. Supported targets are: %s"
+        % (target, list(hierarchy_map.keys())))
+
+  hierarchy = [target]
+  while hierarchy_map[hierarchy[-1]]:
+    hierarchy.append(hierarchy_map[hierarchy[-1]])
+
+  print('Target hierarchy for %s: %s' % (target, hierarchy))
+
+
+if __name__ == '__main__':
+  main()
diff --git a/orchestrator/demo/build_helper.py b/orchestrator/demo/build_helper.py
new file mode 100644
index 0000000..c481f80
--- /dev/null
+++ b/orchestrator/demo/build_helper.py
@@ -0,0 +1,367 @@
+#!/usr/bin/env python3
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import collections
+import copy
+import hierarchy
+import json
+import logging
+import filecmp
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import collect_metadata
+import utils
+
+BUILD_CMD_TO_ALL = (
+  'clean',
+  'installclean',
+  'update-meta',
+)
+BUILD_ALL_EXEMPTION = (
+  'art',
+)
+
+def get_supported_product(ctx, supported_products):
+  hierarchy_map = hierarchy.parse_hierarchy(ctx.build_top())
+  target = ctx.target_product()
+
+  while target not in supported_products:
+    if target not in hierarchy_map:
+      return None
+    target = hierarchy_map[target]
+  return target
+
+
+def parse_goals(ctx, metadata, goals):
+  """Parse goals and returns a map from each component to goals.
+
+    e.g.
+
+    "m main art timezone:foo timezone:bar" will return the following dict: {
+        "main": {"all"},
+        "art": {"all"},
+        "timezone": {"foo", "bar"},
+    }
+  """
+  # for now, goal should look like:
+  # {component} or {component}:{subgoal}
+
+  ret = collections.defaultdict(set)
+
+  for goal in goals:
+    # check if the command is for all components
+    if goal in BUILD_CMD_TO_ALL:
+      ret['all'].add(goal)
+      continue
+
+    # should be {component} or {component}:{subgoal}
+    try:
+      component, subgoal = goal.split(':') if ':' in goal else (goal, 'all')
+    except ValueError:
+      raise RuntimeError(
+          'unknown goal: %s: should be {component} or {component}:{subgoal}' %
+          goal)
+    if component not in metadata:
+      raise RuntimeError('unknown goal: %s: component %s not found' %
+                         (goal, component))
+    if not get_supported_product(ctx, metadata[component]['lunch_targets']):
+      raise RuntimeError("can't find matching target. Supported targets are: " +
+                         str(metadata[component]['lunch_targets']))
+
+    ret[component].add(subgoal)
+
+  return ret
+
+
+def find_cycle(metadata):
+  """ Finds a cyclic dependency among components.
+
+  This is for debugging.
+  """
+  visited = set()
+  parent_node = dict()
+  in_stack = set()
+
+  # Returns a cycle if one is found
+  def dfs(node):
+    # visit_order[visit_time[node] - 1] == node
+    nonlocal visited, parent_node, in_stack
+
+    visited.add(node)
+    in_stack.add(node)
+    if 'deps' not in metadata[node]:
+      in_stack.remove(node)
+      return None
+    for next in metadata[node]['deps']:
+      # We found a cycle (next ~ node) if next is still in the stack
+      if next in in_stack:
+        cycle = [node]
+        while cycle[-1] != next:
+          cycle.append(parent_node[cycle[-1]])
+        return cycle
+
+      # Else, continue searching
+      if next in visited:
+        continue
+
+      parent_node[next] = node
+      result = dfs(next)
+      if result:
+        return result
+
+    in_stack.remove(node)
+    return None
+
+  for component in metadata:
+    if component in visited:
+      continue
+
+    result = dfs(component)
+    if result:
+      return result
+
+  return None
+
+
+def topological_sort_components(metadata):
+  """ Performs topological sort on components.
+
+  If A depends on B, B appears first.
+  """
+  # If A depends on B, we want B to appear before A. But the graph in metadata
+  # is represented as A -> B (B in metadata[A]['deps']). So we sort in the
+  # reverse order, and then reverse the result again to get the desired order.
+  indegree = collections.defaultdict(int)
+  for component in metadata:
+    if 'deps' not in metadata[component]:
+      continue
+    for dep in metadata[component]['deps']:
+      indegree[dep] += 1
+
+  component_queue = collections.deque()
+  for component in metadata:
+    if indegree[component] == 0:
+      component_queue.append(component)
+
+  result = []
+  while component_queue:
+    component = component_queue.popleft()
+    result.append(component)
+    if 'deps' not in metadata[component]:
+      continue
+    for dep in metadata[component]['deps']:
+      indegree[dep] -= 1
+      if indegree[dep] == 0:
+        component_queue.append(dep)
+
+  # If topological sort fails, there must be a cycle.
+  if len(result) != len(metadata):
+    cycle = find_cycle(metadata)
+    raise RuntimeError('circular dependency found among metadata: %s' % cycle)
+
+  return result[::-1]
+
+
+def add_dependency_goals(ctx, metadata, component, goals):
+  """ Adds goals that given component depends on."""
+  # For now, let's just add "all"
+  # TODO: add detailed goals (e.g. API build rules, library build rules, etc.)
+  if 'deps' not in metadata[component]:
+    return
+
+  for dep in metadata[component]['deps']:
+    goals[dep].add('all')
+
+
+def sorted_goals_with_dependencies(ctx, metadata, parsed_goals):
+  """ Analyzes the dependency graph among components, adds build commands for
+
+  dependencies, and then sorts the goals.
+
+  Returns a list of tuples: (component_name, set of subgoals).
+  Builds should be run in the list's order.
+  """
+  # TODO(inseob@): after topological sort, some components may be built in
+  # parallel.
+
+  topological_order = topological_sort_components(metadata)
+  combined_goals = copy.deepcopy(parsed_goals)
+
+  # Add build rules for each component's dependencies
+  # We do this in reverse order, so it can be transitive.
+  # e.g. if A depends on B and B depends on C, and we build A,
+  # C should also be built, in addition to B.
+  for component in topological_order[::-1]:
+    if component in combined_goals:
+      add_dependency_goals(ctx, metadata, component, combined_goals)
+
+  ret = []
+  for component in ['all'] + topological_order:
+    if component in combined_goals:
+      ret.append((component, combined_goals[component]))
+
+  return ret
+
+
+def run_build(ctx, metadata, component, subgoals):
+  build_cmd = metadata[component]['build_cmd']
+  out_dir = metadata[component]['out_dir']
+  default_goals = ''
+  if 'default_goals' in metadata[component]:
+    default_goals = metadata[component]['default_goals']
+
+  if 'all' in subgoals:
+    goal = default_goals
+  else:
+    goal = ' '.join(subgoals)
+
+  build_vars = ''
+  if 'update-meta' in subgoals:
+    build_vars = 'TARGET_MULTITREE_UPDATE_META=true'
+  # TODO(inseob@): shell escape
+  cmd = [
+      '/bin/bash', '-c',
+      'source build/envsetup.sh && lunch %s-%s && %s %s %s' %
+      (get_supported_product(ctx, metadata[component]['lunch_targets']),
+       ctx.target_build_variant(), build_vars, build_cmd, goal)
+  ]
+  logging.debug('cwd: ' + metadata[component]['path'])
+  logging.debug('running build: ' + str(cmd))
+
+  subprocess.run(cmd, cwd=metadata[component]['path'], check=True)
+
+
+def run_build_all(ctx, metadata, subgoals):
+  for component in metadata:
+    if component in BUILD_ALL_EXEMPTION:
+      continue
+    run_build(ctx, metadata, component, subgoals)
+
+
+def find_components(metadata, predicate):
+  for component in metadata:
+    if predicate(component):
+      yield component
+
+
+def import_filegroups(metadata, component, exporting_component, target_file_pairs):
+  imported_filegroup_dir = os.path.join(metadata[component]['path'], 'imported', exporting_component)
+
+  bp_content = ''
+  for name, outpaths in target_file_pairs:
+    bp_content += ('filegroup {{\n'
+                   '    name: "{fname}",\n'
+                   '    srcs: [\n'.format(fname=name))
+    for outpath in outpaths:
+      bp_content += '        "{outfile}",\n'.format(outfile=os.path.basename(outpath))
+    bp_content += ('    ],\n'
+                   '}\n')
+
+    with tempfile.TemporaryDirectory() as tmp_dir:
+      with open(os.path.join(tmp_dir, 'Android.bp'), 'w') as fout:
+        fout.write(bp_content)
+      for _, outpaths in target_file_pairs:
+        for outpath in outpaths:
+          os.symlink(os.path.join(metadata[exporting_component]['path'], outpath),
+                    os.path.join(tmp_dir, os.path.basename(outpath)))
+      cmp_result = filecmp.dircmp(tmp_dir, imported_filegroup_dir)
+      if os.path.exists(imported_filegroup_dir) and len(
+          cmp_result.left_only) + len(cmp_result.right_only) + len(
+              cmp_result.diff_files) == 0:
+        # Files are identical, it doesn't need to be written
+        logging.info(
+            'imported files exists and the contents are identical: {} -> {}'
+            .format(component, exporting_component))
+        continue
+      logging.info('creating symlinks for imported files: {} -> {}'.format(
+          component, exporting_component))
+      os.makedirs(imported_filegroup_dir, exist_ok=True)
+      shutil.rmtree(imported_filegroup_dir, ignore_errors=True)
+      shutil.move(tmp_dir, imported_filegroup_dir)
+
+
+def prepare_build(metadata, component):
+  imported_dir = os.path.join(metadata[component]['path'], 'imported')
+  if utils.META_DEPS not in metadata[component]:
+    if os.path.exists(imported_dir):
+      logging.debug('remove {}'.format(imported_dir))
+      shutil.rmtree(imported_dir)
+    return
+
+  imported_components = set()
+  for exp_comp in metadata[component][utils.META_DEPS]:
+    if utils.META_FILEGROUP in metadata[component][utils.META_DEPS][exp_comp]:
+      filegroups = metadata[component][utils.META_DEPS][exp_comp][utils.META_FILEGROUP]
+      target_file_pairs = []
+      for name in filegroups:
+        target_file_pairs.append((name, filegroups[name]))
+      import_filegroups(metadata, component, exp_comp, target_file_pairs)
+      imported_components.add(exp_comp)
+
+  # Remove directories that are not generated this time.
+  if os.path.exists(imported_dir):
+    if len(imported_components) == 0:
+      shutil.rmtree(imported_dir)
+    else:
+      for remove_target in set(os.listdir(imported_dir)) - imported_components:
+        logging.info('remove unnecessary imported dir: {}'.format(remove_target))
+        shutil.rmtree(os.path.join(imported_dir, remove_target))
+
+
+def main():
+  utils.set_logging_config(logging.DEBUG)
+  ctx = utils.get_build_context()
+
+  logging.info('collecting metadata')
+
+  utils.set_logging_config(True)
+
+  goals = sys.argv[1:]
+  if not goals:
+    logging.debug('empty goals. defaults to main')
+    goals = ['main']
+
+  logging.debug('goals: ' + str(goals))
+
+  # Force update the metadata for the 'update-meta' build
+  metadata_collector = collect_metadata.MetadataCollector(
+      ctx.components_top(), ctx.out_dir(),
+      collect_metadata.COMPONENT_METADATA_DIR,
+      collect_metadata.COMPONENT_METADATA_FILE,
+      force_update='update-meta' in goals)
+  metadata_collector.collect()
+
+  metadata = metadata_collector.get_metadata()
+  logging.debug('metadata: ' + str(metadata))
+
+  parsed_goals = parse_goals(ctx, metadata, goals)
+  logging.debug('parsed goals: ' + str(parsed_goals))
+
+  sorted_goals = sorted_goals_with_dependencies(ctx, metadata, parsed_goals)
+  logging.debug('sorted goals with deps: ' + str(sorted_goals))
+
+  for component, subgoals in sorted_goals:
+    if component == 'all':
+      run_build_all(ctx, metadata, subgoals)
+      continue
+    prepare_build(metadata, component)
+    run_build(ctx, metadata, component, subgoals)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/orchestrator/demo/collect_metadata.py b/orchestrator/demo/collect_metadata.py
new file mode 100755
index 0000000..148167d
--- /dev/null
+++ b/orchestrator/demo/collect_metadata.py
@@ -0,0 +1,428 @@
+#!/usr/bin/env python3
+# 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.
+
+import argparse
+import copy
+import json
+import logging
+import os
+import sys
+import yaml
+from collections import defaultdict
+from typing import (
+  List,
+  Set,
+)
+
+import utils
+
+# SKIP_COMPONENT_SEARCH = (
+#    'tools',
+# )
+COMPONENT_METADATA_DIR = '.repo'
+COMPONENT_METADATA_FILE = 'treeinfo.yaml'
+GENERATED_METADATA_FILE = 'metadata.json'
+COMBINED_METADATA_FILENAME = 'multitree_meta.json'
+
+
+class Dep(object):
+  def __init__(self, name, component, deps_type):
+    self.name = name
+    self.component = component
+    self.type = deps_type
+    self.out_paths = list()
+
+
+class ExportedDep(Dep):
+  def __init__(self, name, component, deps_type):
+    super().__init__(name, component, deps_type)
+
+  def setOutputPaths(self, output_paths: list):
+    self.out_paths = output_paths
+
+
+class ImportedDep(Dep):
+  required_type_map = {
+    # import type: (required type, get imported module list)
+    utils.META_FILEGROUP: (utils.META_MODULES, True),
+  }
+
+  def __init__(self, name, component, deps_type, import_map):
+    super().__init__(name, component, deps_type)
+    self.exported_deps: Set[ExportedDep] = set()
+    self.imported_modules: List[str] = list()
+    self.required_type = deps_type
+    get_imported_module = False
+    if deps_type in ImportedDep.required_type_map:
+      self.required_type, get_imported_module = ImportedDep.required_type_map[deps_type]
+    if get_imported_module:
+      self.imported_modules = import_map[name]
+    else:
+      self.imported_modules.append(name)
+
+  def verify_and_add(self, exported: ExportedDep):
+    if self.required_type != exported.type:
+      raise RuntimeError(
+          '{comp} components imports {module} for {imp_type} but it is exported as {exp_type}.'
+          .format(comp=self.component, module=exported.name, imp_type=self.required_type, exp_type=exported.type))
+    self.exported_deps.add(exported)
+    self.out_paths.extend(exported.out_paths)
+    # Remove duplicates. We may not use set() which is not JSON serializable
+    self.out_paths = list(dict.fromkeys(self.out_paths))
+
+
+class MetadataCollector(object):
+  """Visit all component directories and collect the metadata from them.
+
+Example of metadata:
+==========
+build_cmd: m    # build command for this component. 'm' if omitted
+out_dir: out    # out dir of this component. 'out' if omitted
+exports:
+  libraries:
+    - name: libopenjdkjvm
+    - name: libopenjdkjvmd
+      build_cmd: mma      # build command for libopenjdkjvmd if specified
+      out_dir: out/soong  # out dir for libopenjdkjvmd if specified
+    - name: libctstiagent
+  APIs:
+    - api1
+    - api2
+imports:
+  libraries:
+    - lib1
+    - lib2
+  APIs:
+    - import_api1
+    - import_api2
+lunch_targets:
+  - arm64
+  - x86_64
+"""
+
+  def __init__(self, component_top, out_dir, meta_dir, meta_file, force_update=False):
+    if not os.path.exists(out_dir):
+      os.makedirs(out_dir)
+
+    self.__component_top = component_top
+    self.__out_dir = out_dir
+    self.__metadata_path = os.path.join(meta_dir, meta_file)
+    self.__combined_metadata_path = os.path.join(self.__out_dir,
+                                                 COMBINED_METADATA_FILENAME)
+    self.__force_update = force_update
+
+    self.__metadata = dict()
+    self.__map_exports = dict()
+    self.__component_set = set()
+
+  def collect(self):
+    """ Read precomputed combined metadata from the json file.
+
+    If any components have updated their metadata, update the metadata
+    information and the json file.
+    """
+    timestamp = self.__restore_metadata()
+    if timestamp and os.path.getmtime(__file__) > timestamp:
+      logging.info('Update the metadata as the orchestrator has been changed')
+      self.__force_update = True
+    self.__collect_from_components(timestamp)
+
+  def get_metadata(self):
+    """ Returns collected metadata from all components"""
+    if not self.__metadata:
+      logging.warning('Metadata is empty')
+    return copy.deepcopy(self.__metadata)
+
+  def __collect_from_components(self, timestamp):
+    """ Read metadata from all components
+
+    If any components have newer metadata files or are removed, update the
+    combined metadata.
+    """
+    metadata_updated = False
+    for component in os.listdir(self.__component_top):
+      # if component in SKIP_COMPONENT_SEARCH:
+      #     continue
+      if self.__read_component_metadata(timestamp, component):
+        metadata_updated = True
+      if self.__read_generated_metadata(timestamp, component):
+        metadata_updated = True
+
+    deleted_components = set()
+    for meta in self.__metadata:
+      if meta not in self.__component_set:
+        logging.info('Component {} is removed'.format(meta))
+        deleted_components.add(meta)
+        metadata_updated = True
+    for meta in deleted_components:
+      del self.__metadata[meta]
+
+    if metadata_updated:
+      self.__update_dependencies()
+      self.__store_metadata()
+      logging.info('Metadata updated')
+
+  def __read_component_metadata(self, timestamp, component):
+    """ Search for the metadata file from a component.
+
+    If the metadata is modified, read the file and update the metadata.
+    """
+    component_path = os.path.join(self.__component_top, component)
+    metadata_file = os.path.join(component_path, self.__metadata_path)
+    logging.info(
+        'Reading a metadata file from {} component ...'.format(component))
+    if not os.path.isfile(metadata_file):
+      logging.warning('Metadata file {} not found!'.format(metadata_file))
+      return False
+
+    self.__component_set.add(component)
+    if not self.__force_update and timestamp and timestamp > os.path.getmtime(metadata_file):
+      logging.info('... yaml not changed. Skip')
+      return False
+
+    with open(metadata_file) as f:
+      meta = yaml.load(f, Loader=yaml.SafeLoader)
+
+    meta['path'] = component_path
+    if utils.META_BUILDCMD not in meta:
+      meta[utils.META_BUILDCMD] = utils.DEFAULT_BUILDCMD
+    if utils.META_OUTDIR not in meta:
+      meta[utils.META_OUTDIR] = utils.DEFAULT_OUTDIR
+
+    if utils.META_IMPORTS not in meta:
+      meta[utils.META_IMPORTS] = defaultdict(dict)
+    if utils.META_EXPORTS not in meta:
+      meta[utils.META_EXPORTS] = defaultdict(dict)
+
+    self.__metadata[component] = meta
+    return True
+
+  def __read_generated_metadata(self, timestamp, component):
+    """ Read a metadata gerated by 'update-meta' build command from the soong build system
+
+    Soong generate the metadata that has the information of import/export module/files.
+    Build orchestrator read the generated metadata to collect the dependency information.
+
+    Generated metadata has the following format:
+    {
+      "Imported": {
+        "FileGroups": {
+          "<name_of_filegroup>": [
+            "<exported_module_name>",
+            ...
+          ],
+          ...
+        }
+      }
+      "Exported": {
+        "<exported_module_name>": [
+          "<output_file_path>",
+          ...
+        ],
+        ...
+      }
+    }
+    """
+    if component not in self.__component_set:
+      # skip reading generated metadata if the component metadata file was missing
+      return False
+    component_out = os.path.join(self.__component_top, component, self.__metadata[component][utils.META_OUTDIR])
+    generated_metadata_file = os.path.join(component_out, 'soong', 'multitree', GENERATED_METADATA_FILE)
+    if not os.path.isfile(generated_metadata_file):
+      logging.info('... Soong did not generated the metadata file. Skip')
+      return False
+    if not self.__force_update and timestamp and timestamp > os.path.getmtime(generated_metadata_file):
+      logging.info('... Soong generated metadata not changed. Skip')
+      return False
+
+    with open(generated_metadata_file, 'r') as gen_meta_json:
+      try:
+        gen_metadata = json.load(gen_meta_json)
+      except json.decoder.JSONDecodeError:
+        logging.warning('JSONDecodeError!!!: skip reading the {} file'.format(
+            generated_metadata_file))
+        return False
+
+    if utils.SOONG_IMPORTED in gen_metadata:
+      imported = gen_metadata[utils.SOONG_IMPORTED]
+      if utils.SOONG_IMPORTED_FILEGROUPS in imported:
+        self.__metadata[component][utils.META_IMPORTS][utils.META_FILEGROUP] = imported[utils.SOONG_IMPORTED_FILEGROUPS]
+    if utils.SOONG_EXPORTED in gen_metadata:
+      self.__metadata[component][utils.META_EXPORTS][utils.META_MODULES] = gen_metadata[utils.SOONG_EXPORTED]
+
+    return True
+
+  def __update_export_map(self):
+    """ Read metadata of all components and update the export map
+
+    'libraries' and 'APIs' are special exproted types that are provided manually
+    from the .yaml metadata files. These need to be replaced with the implementation
+    in soong gerated metadata.
+    The export type 'module' is generated from the soong build system from the modules
+    with 'export: true' property. This export type includes a dictionary with module
+    names as keys and their output files as values. These output files will be used as
+    prebuilt sources when generating the imported modules.
+    """
+    self.__map_exports = dict()
+    for comp in self.__metadata:
+      if utils.META_EXPORTS not in self.__metadata[comp]:
+        continue
+      exports = self.__metadata[comp][utils.META_EXPORTS]
+
+      for export_type in exports:
+        for module in exports[export_type]:
+          if export_type == utils.META_LIBS:
+            name = module[utils.META_LIB_NAME]
+          else:
+            name = module
+
+          if name in self.__map_exports:
+            raise RuntimeError(
+                'Exported libs conflict!!!: "{name}" in the {comp} component is already exported by the {prev} component.'
+                .format(name=name, comp=comp, prev=self.__map_exports[name][utils.EXP_COMPONENT]))
+          exported_deps = ExportedDep(name, comp, export_type)
+          if export_type == utils.META_MODULES:
+            exported_deps.setOutputPaths(exports[export_type][module])
+          self.__map_exports[name] = exported_deps
+
+  def __verify_and_add_dependencies(self, component):
+    """ Search all imported items from the export_map.
+
+    If any imported items are not provided by the other components, report
+    an error.
+    Otherwise, add the component dependency and update the exported information to the
+    import maps.
+    """
+    def verify_and_add_dependencies(imported_dep: ImportedDep):
+      for module in imported_dep.imported_modules:
+        if module not in self.__map_exports:
+          raise RuntimeError(
+              'Imported item not found!!!: Imported module "{module}" in the {comp} component is not exported from any other components.'
+              .format(module=module, comp=imported_dep.component))
+        imported_dep.verify_and_add(self.__map_exports[module])
+
+        deps = self.__metadata[component][utils.META_DEPS]
+        exp_comp = self.__map_exports[module].component
+        if exp_comp not in deps:
+          deps[exp_comp] = defaultdict(defaultdict)
+        deps[exp_comp][imported_dep.type][imported_dep.name] = imported_dep.out_paths
+
+    self.__metadata[component][utils.META_DEPS] = defaultdict()
+    imports = self.__metadata[component][utils.META_IMPORTS]
+    for import_type in imports:
+      for module in imports[import_type]:
+        verify_and_add_dependencies(ImportedDep(module, component, import_type, imports[import_type]))
+
+  def __check_imports(self):
+    """ Search the export map to find the component to import libraries or APIs.
+
+    Update the 'deps' field that includes the dependent components.
+    """
+    for component in self.__metadata:
+      self.__verify_and_add_dependencies(component)
+      if utils.META_DEPS in self.__metadata[component]:
+        logging.debug('{comp} depends on {list} components'.format(
+            comp=component, list=self.__metadata[component][utils.META_DEPS]))
+
+  def __update_dependencies(self):
+    """ Generate a dependency graph for the components
+
+    Update __map_exports and the dependency graph with the maps.
+    """
+    self.__update_export_map()
+    self.__check_imports()
+
+  def __store_metadata(self):
+    """ Store the __metadata dictionary as json format"""
+    with open(self.__combined_metadata_path, 'w') as json_file:
+      json.dump(self.__metadata, json_file, indent=2)
+
+  def __restore_metadata(self):
+    """ Read the stored json file and return the time stamps of the
+
+        metadata file.
+        """
+    if not os.path.exists(self.__combined_metadata_path):
+      return None
+
+    with open(self.__combined_metadata_path, 'r') as json_file:
+      try:
+        self.__metadata = json.load(json_file)
+      except json.decoder.JSONDecodeError:
+        logging.warning('JSONDecodeError!!!: skip reading the {} file'.format(
+            self.__combined_metadata_path))
+        return None
+
+    logging.info('Metadata restored from {}'.format(
+        self.__combined_metadata_path))
+    self.__update_export_map()
+    return os.path.getmtime(self.__combined_metadata_path)
+
+
+def get_args():
+
+  def check_dir(path):
+    if os.path.exists(path) and os.path.isdir(path):
+      return os.path.normpath(path)
+    else:
+      raise argparse.ArgumentTypeError('\"{}\" is not a directory'.format(path))
+
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--component-top',
+      help='Scan all components under this directory.',
+      default=os.path.join(os.path.dirname(__file__), '../../../components'),
+      type=check_dir)
+  parser.add_argument(
+      '--meta-file',
+      help='Name of the metadata file.',
+      default=COMPONENT_METADATA_FILE,
+      type=str)
+  parser.add_argument(
+      '--meta-dir',
+      help='Each component has the metadata in this directory.',
+      default=COMPONENT_METADATA_DIR,
+      type=str)
+  parser.add_argument(
+      '--out-dir',
+      help='Out dir for the outer tree. The orchestrator stores the collected metadata in this directory.',
+      default=os.path.join(os.path.dirname(__file__), '../../../out'),
+      type=os.path.normpath)
+  parser.add_argument(
+      '--force',
+      '-f',
+      action='store_true',
+      help='Force to collect metadata',
+  )
+  parser.add_argument(
+      '--verbose',
+      '-v',
+      help='Increase output verbosity, e.g. "-v", "-vv".',
+      action='count',
+      default=0)
+  return parser.parse_args()
+
+
+def main():
+  args = get_args()
+  utils.set_logging_config(args.verbose)
+
+  metadata_collector = MetadataCollector(args.component_top, args.out_dir,
+                                         args.meta_dir, args.meta_file, args.force)
+  metadata_collector.collect()
+
+
+if __name__ == '__main__':
+  main()
diff --git a/orchestrator/demo/envsetup.sh b/orchestrator/demo/envsetup.sh
new file mode 100644
index 0000000..902a37c
--- /dev/null
+++ b/orchestrator/demo/envsetup.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+
+function buffet()
+{
+    local product variant selection
+    if [[ $# -ne 1 ]]; then
+        echo "usage: buffet [target]" >&2
+        return 1
+    fi
+
+    selection=$1
+    product=${selection%%-*} # Trim everything after first dash
+    variant=${selection#*-} # Trim everything up to first dash
+
+    if [ -z "$product" ]
+    then
+        echo
+        echo "Invalid lunch combo: $selection"
+        return 1
+    fi
+
+    if [ -z "$variant" ]
+    then
+        if [[ "$product" =~ .*_(eng|user|userdebug) ]]
+        then
+            echo "Did you mean -${product/*_/}? (dash instead of underscore)"
+        fi
+        return 1
+    fi
+
+    BUFFET_BUILD_TOP=$(pwd) python3 tools/build/orchestrator/buffet_helper.py $1 || return 1
+
+    export BUFFET_BUILD_TOP=$(pwd)
+    export BUFFET_COMPONENTS_TOP=$BUFFET_BUILD_TOP/components
+    export BUFFET_TARGET_PRODUCT=$product
+    export BUFFET_TARGET_BUILD_VARIANT=$variant
+    export BUFFET_TARGET_BUILD_TYPE=release
+}
+
+function m()
+{
+    if [ -z "$BUFFET_BUILD_TOP" ]
+    then
+        echo "Run \"buffet [target]\" first"
+        return 1
+    fi
+    python3 $BUFFET_BUILD_TOP/tools/build/orchestrator/build_helper.py "$@"
+}
diff --git a/orchestrator/demo/hierarchy.py b/orchestrator/demo/hierarchy.py
new file mode 100644
index 0000000..ae1825c
--- /dev/null
+++ b/orchestrator/demo/hierarchy.py
@@ -0,0 +1,79 @@
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os
+import yaml
+
+
+def parse_hierarchy(build_top):
+  """Parse build hierarchy file from given build top directory, and returns a dict from child targets to parent targets.
+
+  Example of hierarchy file:
+  ==========
+  aosp_arm64:
+  - armv8
+  - aosp_cf_arm64_phone
+
+  armv8:
+  - aosp_oriole
+  - aosp_sunfish
+
+  aosp_oriole:
+  - oriole
+
+  aosp_sunfish:
+  - sunfish
+
+  oriole:
+  # leaf
+
+  sunfish:
+  # leaf
+  ==========
+
+  If we parse this yaml, we get a dict looking like:
+
+  {
+      "sunfish": "aosp_sunfish",
+      "oriole": "aosp_oriole",
+      "aosp_oriole": "armv8",
+      "aosp_sunfish": "armv8",
+      "armv8": "aosp_arm64",
+      "aosp_cf_arm64_phone": "aosp_arm64",
+      "aosp_arm64": None, # no parent
+  }
+  """
+  metadata_path = os.path.join(build_top, 'tools', 'build', 'hierarchy.yaml')
+  if not os.path.isfile(metadata_path):
+    raise RuntimeError("target metadata file %s doesn't exist" % metadata_path)
+
+  with open(metadata_path, 'r') as f:
+    hierarchy_yaml = yaml.load(f, Loader=yaml.SafeLoader)
+
+  hierarchy_map = dict()
+
+  for parent_target, child_targets in hierarchy_yaml.items():
+    if not child_targets:
+      # leaf
+      continue
+    for child_target in child_targets:
+      hierarchy_map[child_target] = parent_target
+
+  for parent_target in hierarchy_yaml:
+    # targets with no parent
+    if parent_target not in hierarchy_map:
+      hierarchy_map[parent_target] = None
+
+  return hierarchy_map
diff --git a/orchestrator/demo/hierarchy.yaml b/orchestrator/demo/hierarchy.yaml
new file mode 100644
index 0000000..cc6de4d
--- /dev/null
+++ b/orchestrator/demo/hierarchy.yaml
@@ -0,0 +1,37 @@
+# hierarchy of targets
+
+aosp_arm64:
+- armv8
+- aosp_cf_arm64_phone
+
+armv8:
+- mainline_modules_arm64
+
+mainline_modules_arm64:
+- aosp_oriole
+- aosp_sunfish
+- aosp_raven
+
+aosp_oriole:
+- oriole
+
+aosp_sunfish:
+- sunfish
+
+aosp_raven:
+- raven
+
+oriole:
+# leaf
+
+sunfish:
+# leaf
+
+raven:
+# leaf
+
+aosp_cf_arm64_phone:
+- cf_arm64_phone
+
+cf_arm64_phone:
+# leaf
diff --git a/orchestrator/demo/utils.py b/orchestrator/demo/utils.py
new file mode 100644
index 0000000..5dbbe4a
--- /dev/null
+++ b/orchestrator/demo/utils.py
@@ -0,0 +1,89 @@
+# 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.
+#
+
+import logging
+import os
+
+# default build configuration for each component
+DEFAULT_BUILDCMD = 'm'
+DEFAULT_OUTDIR = 'out'
+
+# yaml fields
+META_BUILDCMD = 'build_cmd'
+META_OUTDIR = 'out_dir'
+META_EXPORTS = 'exports'
+META_IMPORTS = 'imports'
+META_TARGETS = 'lunch_targets'
+META_DEPS = 'deps'
+# fields under 'exports' and 'imports'
+META_LIBS = 'libraries'
+META_APIS = 'APIs'
+META_FILEGROUP = 'filegroup'
+META_MODULES = 'modules'
+# fields under 'libraries'
+META_LIB_NAME = 'name'
+
+# fields for generated metadata file
+SOONG_IMPORTED = 'Imported'
+SOONG_IMPORTED_FILEGROUPS = 'FileGroups'
+SOONG_EXPORTED = 'Exported'
+
+# export map items
+EXP_COMPONENT = 'component'
+EXP_TYPE = 'type'
+EXP_OUTPATHS = 'outpaths'
+
+class BuildContext:
+
+  def __init__(self):
+    self._build_top = os.getenv('BUFFET_BUILD_TOP')
+    self._components_top = os.getenv('BUFFET_COMPONENTS_TOP')
+    self._target_product = os.getenv('BUFFET_TARGET_PRODUCT')
+    self._target_build_variant = os.getenv('BUFFET_TARGET_BUILD_VARIANT')
+    self._target_build_type = os.getenv('BUFFET_TARGET_BUILD_TYPE')
+    self._out_dir = os.path.join(self._build_top, 'out')
+
+    if not self._build_top:
+      raise RuntimeError("Can't find root. Did you run buffet?")
+
+  def build_top(self):
+    return self._build_top
+
+  def components_top(self):
+    return self._components_top
+
+  def target_product(self):
+    return self._target_product
+
+  def target_build_variant(self):
+    return self._target_build_variant
+
+  def target_build_type(self):
+    return self._target_build_type
+
+  def out_dir(self):
+    return self._out_dir
+
+
+def get_build_context():
+  return BuildContext()
+
+
+def set_logging_config(verbose_level):
+  verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
+  verbosity = min(verbose_level, 2)
+  logging.basicConfig(
+      format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s',
+      level=verbose_map[verbosity])
diff --git a/orchestrator/inner_build/common.py b/orchestrator/inner_build/common.py
new file mode 100644
index 0000000..6919e04
--- /dev/null
+++ b/orchestrator/inner_build/common.py
@@ -0,0 +1,56 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import sys
+
+def _parse_arguments(argv):
+    argv = argv[1:]
+    """Return an argparse options object."""
+    # Top-level parser
+    parser = argparse.ArgumentParser(prog=".inner_build")
+
+    parser.add_argument("--out_dir", action="store", required=True,
+            help="root of the output directory for this inner tree's API contributions")
+
+    parser.add_argument("--api_domain", action="append", required=True,
+            help="which API domains are to be built in this inner tree")
+
+    subparsers = parser.add_subparsers(required=True, dest="command",
+            help="subcommands")
+
+    # inner_build describe command
+    describe_parser = subparsers.add_parser("describe",
+            help="describe the capabilities of this inner tree's build system")
+
+    # create the parser for the "b" command
+    export_parser = subparsers.add_parser("export_api_contributions",
+            help="export the API contributions of this inner tree")
+
+    # Parse the arguments
+    return parser.parse_args(argv)
+
+
+class Commands(object):
+    def Run(self, argv):
+        """Parse the command arguments and call the corresponding subcommand method on
+        this object.
+
+        Throws AttributeError if the method for the command wasn't found.
+        """
+        args = _parse_arguments(argv)
+        return getattr(self, args.command)(args)
+
diff --git a/orchestrator/inner_build/inner_build_demo.py b/orchestrator/inner_build/inner_build_demo.py
new file mode 100755
index 0000000..9aafb4d
--- /dev/null
+++ b/orchestrator/inner_build/inner_build_demo.py
@@ -0,0 +1,143 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import textwrap
+
+sys.dont_write_bytecode = True
+import common
+
+def mkdirs(path):
+    try:
+        os.makedirs(path)
+    except FileExistsError:
+        pass
+
+
+class InnerBuildSoong(common.Commands):
+    def describe(self, args):
+        mkdirs(args.out_dir)
+
+        with open(os.path.join(args.out_dir, "tree_info.json"), "w") as f:
+            f.write(textwrap.dedent("""\
+            {
+                "requires_ninja": true,
+                "orchestrator_protocol_version": 1
+            }"""))
+
+    def export_api_contributions(self, args):
+        contributions_dir = os.path.join(args.out_dir, "api_contributions")
+        mkdirs(contributions_dir)
+
+        if "system" in args.api_domain:
+            with open(os.path.join(contributions_dir, "public_api-1.json"), "w") as f:
+                # 'name: android' is android.jar
+                f.write(textwrap.dedent("""\
+                {
+                    "name": "public_api",
+                    "version": 1,
+                    "api_domain": "system",
+                    "cc_libraries": [
+                        {
+                            "name": "libhwui",
+                            "headers": [
+                                {
+                                    "root": "frameworks/base/libs/hwui/apex/include",
+                                    "files": [
+                                        "android/graphics/jni_runtime.h",
+                                        "android/graphics/paint.h",
+                                        "android/graphics/matrix.h",
+                                        "android/graphics/canvas.h",
+                                        "android/graphics/renderthread.h",
+                                        "android/graphics/bitmap.h",
+                                        "android/graphics/region.h"
+                                    ]
+                                }
+                            ],
+                            "api": [
+                                "frameworks/base/libs/hwui/libhwui.map.txt"
+                            ]
+                        }
+                    ],
+                    "java_libraries": [
+                        {
+                            "name": "android",
+                            "api": [
+                                "frameworks/base/core/api/current.txt"
+                            ]
+                        }
+                    ],
+                    "resource_libraries": [
+                        {
+                            "name": "android",
+                            "api": "frameworks/base/core/res/res/values/public.xml"
+                        }
+                    ],
+                    "host_executables": [
+                        {
+                            "name": "aapt2",
+                            "binary": "out/host/bin/aapt2",
+                            "runfiles": [
+                                "../lib/todo.so"
+                            ]
+                        }
+                    ]
+                }"""))
+        elif "com.android.bionic" in args.api_domain:
+            with open(os.path.join(contributions_dir, "public_api-1.json"), "w") as f:
+                # 'name: android' is android.jar
+                f.write(textwrap.dedent("""\
+                {
+                    "name": "public_api",
+                    "version": 1,
+                    "api_domain": "system",
+                    "cc_libraries": [
+                        {
+                            "name": "libc",
+                            "headers": [
+                                {
+                                    "root": "bionic/libc/include",
+                                    "files": [
+                                        "stdio.h",
+                                        "sys/klog.h"
+                                    ]
+                                }
+                            ],
+                            "api": "bionic/libc/libc.map.txt"
+                        }
+                    ],
+                    "java_libraries": [
+                        {
+                            "name": "android",
+                            "api": [
+                                "frameworks/base/libs/hwui/api/current.txt"
+                            ]
+                        }
+                    ]
+                }"""))
+
+
+
+def main(argv):
+    return InnerBuildSoong().Run(argv)
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
+
+
+# vim: sts=4:ts=4:sw=4
diff --git a/orchestrator/inner_build/inner_build_soong.py b/orchestrator/inner_build/inner_build_soong.py
new file mode 100755
index 0000000..a653dcc
--- /dev/null
+++ b/orchestrator/inner_build/inner_build_soong.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import sys
+
+sys.dont_write_bytecode = True
+import common
+
+class InnerBuildSoong(common.Commands):
+    def describe(self, args):
+        pass
+
+
+    def export_api_contributions(self, args):
+        pass
+
+
+def main(argv):
+    return InnerBuildSoong().Run(argv)
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
diff --git a/orchestrator/multitree_combos/aosp_cf_arm64_phone.mcombo b/orchestrator/multitree_combos/aosp_cf_arm64_phone.mcombo
new file mode 100644
index 0000000..0790226
--- /dev/null
+++ b/orchestrator/multitree_combos/aosp_cf_arm64_phone.mcombo
@@ -0,0 +1,16 @@
+{
+    "lunchable": true,
+    "system": {
+        "tree": "master",
+        "product": "aosp_cf_arm64_phone"
+    },
+    "vendor": {
+        "tree": "master",
+        "product": "aosp_cf_arm64_phone"
+    },
+    "modules": {
+        "com.android.bionic": {
+            "tree": "sc-mainline-prod"
+        }
+    }
+}
diff --git a/orchestrator/ninja/ninja_syntax.py b/orchestrator/ninja/ninja_syntax.py
new file mode 100644
index 0000000..328c99c
--- /dev/null
+++ b/orchestrator/ninja/ninja_syntax.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2022 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.
+
+from abc import ABC, abstractmethod
+
+from collections.abc import Iterator
+from typing import List
+
+TAB = "  "
+
+class Node(ABC):
+  '''An abstract class that can be serialized to a ninja file
+  All other ninja-serializable classes inherit from this class'''
+
+  @abstractmethod
+  def stream(self) -> Iterator[str]:
+    pass
+
+class Variable(Node):
+  '''A ninja variable that can be reused across build actions
+  https://ninja-build.org/manual.html#_variables'''
+
+  def __init__(self, name:str, value:str, indent=0):
+    self.name = name
+    self.value = value
+    self.indent = indent
+
+  def stream(self) -> Iterator[str]:
+    indent = TAB * self.indent
+    yield f"{indent}{self.name} = {self.value}"
+
+class RuleException(Exception):
+  pass
+
+# Ninja rules recognize a limited set of variables
+# https://ninja-build.org/manual.html#ref_rule
+# Keep this list sorted
+RULE_VARIABLES = ["command",
+                  "depfile",
+                  "deps",
+                  "description",
+                  "dyndep",
+                  "generator",
+                  "msvc_deps_prefix",
+                  "restat",
+                  "rspfile",
+                  "rspfile_content"]
+
+class Rule(Node):
+  '''A shorthand for a command line that can be reused
+  https://ninja-build.org/manual.html#_rules'''
+
+  def __init__(self, name:str):
+    self.name = name
+    self.variables = []
+
+  def add_variable(self, name: str, value: str):
+    if name not in RULE_VARIABLES:
+      raise RuleException(f"{name} is not a recognized variable in a ninja rule")
+
+    self.variables.append(Variable(name=name, value=value, indent=1))
+
+  def stream(self) -> Iterator[str]:
+    self._validate_rule()
+
+    yield f"rule {self.name}"
+    # Yield rule variables sorted by `name`
+    for var in sorted(self.variables, key=lambda x: x.name):
+      # variables yield a single item, next() is sufficient
+      yield next(var.stream())
+
+  def _validate_rule(self):
+    # command is a required variable in a ninja rule
+    self._assert_variable_is_not_empty(variable_name="command")
+
+  def _assert_variable_is_not_empty(self, variable_name: str):
+    if not any(var.name == variable_name for var in self.variables):
+      raise RuleException(f"{variable_name} is required in a ninja rule")
+
+class BuildActionException(Exception):
+  pass
+
+class BuildAction(Node):
+  '''Describes the dependency edge between inputs and output
+  https://ninja-build.org/manual.html#_build_statements'''
+
+  def __init__(self, output: str, rule: str, inputs: List[str]=None, implicits: List[str]=None, order_only: List[str]=None):
+    self.output = output
+    self.rule = rule
+    self.inputs = self._as_list(inputs)
+    self.implicits = self._as_list(implicits)
+    self.order_only = self._as_list(order_only)
+    self.variables = []
+
+  def add_variable(self, name: str, value: str):
+    '''Variables limited to the scope of this build action'''
+    self.variables.append(Variable(name=name, value=value, indent=1))
+
+  def stream(self) -> Iterator[str]:
+    self._validate()
+
+    build_statement = f"build {self.output}: {self.rule}"
+    if len(self.inputs) > 0:
+      build_statement += " "
+      build_statement += " ".join(self.inputs)
+    if len(self.implicits) > 0:
+      build_statement += " | "
+      build_statement += " ".join(self.implicits)
+    if len(self.order_only) > 0:
+      build_statement += " || "
+      build_statement += " ".join(self.order_only)
+    yield build_statement
+    # Yield variables sorted by `name`
+    for var in sorted(self.variables, key=lambda x: x.name):
+      # variables yield a single item, next() is sufficient
+      yield next(var.stream())
+
+  def _validate(self):
+    if not self.output:
+      raise BuildActionException("Output is required in a ninja build statement")
+    if not self.rule:
+      raise BuildActionException("Rule is required in a ninja build statement")
+
+  def _as_list(self, list_like):
+    if list_like is None:
+      return []
+    if isinstance(list_like, list):
+      return list_like
+    return [list_like]
+
+class Pool(Node):
+  '''https://ninja-build.org/manual.html#ref_pool'''
+
+  def __init__(self, name: str, depth: int):
+    self.name = name
+    self.depth = Variable(name="depth", value=depth, indent=1)
+
+  def stream(self) -> Iterator[str]:
+    yield f"pool {self.name}"
+    yield next(self.depth.stream())
+
+class Subninja(Node):
+
+  def __init__(self, subninja: str, chDir: str):
+    self.subninja = subninja
+    self.chDir = chDir
+
+  # TODO(spandandas): Update the syntax when aosp/2064612 lands
+  def stream() -> Iterator[str]:
+    yield f"subninja {self.subninja}"
+
+class Line(Node):
+  '''Generic class that can be used for comments/newlines/default_target etc'''
+
+  def __init__(self, value:str):
+    self.value = value
+
+  def stream(self) -> Iterator[str]:
+    yield self.value
diff --git a/orchestrator/ninja/ninja_writer.py b/orchestrator/ninja/ninja_writer.py
new file mode 100644
index 0000000..e3070bb
--- /dev/null
+++ b/orchestrator/ninja/ninja_writer.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2022 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.
+
+from ninja_syntax import Variable, BuildAction, Rule, Pool, Subninja, Line
+
+# TODO: Format the output according to a configurable width variable
+# This will ensure that the generated content fits on a screen and does not
+# require horizontal scrolling
+class Writer:
+
+  def __init__(self, file):
+    self.file = file
+    self.nodes = [] # type Node
+
+  def add_variable(self, variable: Variable):
+    self.nodes.append(variable)
+
+  def add_rule(self, rule: Rule):
+    self.nodes.append(rule)
+
+  def add_build_action(self, build_action: BuildAction):
+    self.nodes.append(build_action)
+
+  def add_pool(self, pool: Pool):
+    self.nodes.append(pool)
+
+  def add_comment(self, comment: str):
+    self.nodes.append(Line(value=f"# {comment}"))
+
+  def add_default(self, default: str):
+    self.nodes.append(Line(value=f"default {default}"))
+
+  def add_newline(self):
+    self.nodes.append(Line(value=""))
+
+  def add_subninja(self, subninja: Subninja):
+    self.nodes.append(subninja)
+
+  def write(self):
+    for node in self.nodes:
+      for line in node.stream():
+        print(line, file=self.file)
diff --git a/orchestrator/ninja/test_ninja_syntax.py b/orchestrator/ninja/test_ninja_syntax.py
new file mode 100644
index 0000000..d922fd2
--- /dev/null
+++ b/orchestrator/ninja/test_ninja_syntax.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+from ninja_syntax import Variable, Rule, RuleException, BuildAction, BuildActionException, Pool
+
+class TestVariable(unittest.TestCase):
+
+  def test_assignment(self):
+    variable = Variable(name="key", value="value")
+    self.assertEqual("key = value", next(variable.stream()))
+    variable = Variable(name="key", value="value with spaces")
+    self.assertEqual("key = value with spaces", next(variable.stream()))
+    variable = Variable(name="key", value="$some_other_variable")
+    self.assertEqual("key = $some_other_variable", next(variable.stream()))
+
+  def test_indentation(self):
+    variable = Variable(name="key", value="value", indent=0)
+    self.assertEqual("key = value", next(variable.stream()))
+    variable = Variable(name="key", value="value", indent=1)
+    self.assertEqual("  key = value", next(variable.stream()))
+
+class TestRule(unittest.TestCase):
+
+  def test_rulename_comes_first(self):
+    rule = Rule(name="myrule")
+    rule.add_variable("command", "/bin/bash echo")
+    self.assertEqual("rule myrule", next(rule.stream()))
+
+  def test_command_is_a_required_variable(self):
+    rule = Rule(name="myrule")
+    with self.assertRaises(RuleException):
+      next(rule.stream())
+
+  def test_bad_rule_variable(self):
+    rule = Rule(name="myrule")
+    with self.assertRaises(RuleException):
+      rule.add_variable(name="unrecognize_rule_variable", value="value")
+
+  def test_rule_variables_are_indented(self):
+    rule = Rule(name="myrule")
+    rule.add_variable("command", "/bin/bash echo")
+    stream = rule.stream()
+    self.assertEqual("rule myrule", next(stream)) # top-level rule should not be indented
+    self.assertEqual("  command = /bin/bash echo", next(stream))
+
+  def test_rule_variables_are_sorted(self):
+    rule = Rule(name="myrule")
+    rule.add_variable("description", "Adding description before command")
+    rule.add_variable("command", "/bin/bash echo")
+    stream = rule.stream()
+    self.assertEqual("rule myrule", next(stream)) # rule always comes first
+    self.assertEqual("  command = /bin/bash echo", next(stream))
+    self.assertEqual("  description = Adding description before command", next(stream))
+
+class TestBuildAction(unittest.TestCase):
+
+  def test_no_inputs(self):
+    build = BuildAction(output="out", rule="phony")
+    stream = build.stream()
+    self.assertEqual("build out: phony", next(stream))
+    # Empty output
+    build = BuildAction(output="", rule="phony")
+    with self.assertRaises(BuildActionException):
+      next(build.stream())
+    # Empty rule
+    build = BuildAction(output="out", rule="")
+    with self.assertRaises(BuildActionException):
+      next(build.stream())
+
+  def test_inputs(self):
+    build = BuildAction(output="out", rule="cat", inputs=["input1", "input2"])
+    self.assertEqual("build out: cat input1 input2", next(build.stream()))
+    build = BuildAction(output="out", rule="cat", inputs=["input1", "input2"], implicits=["implicits1", "implicits2"], order_only=["order_only1", "order_only2"])
+    self.assertEqual("build out: cat input1 input2 | implicits1 implicits2 || order_only1 order_only2", next(build.stream()))
+
+  def test_variables(self):
+    build = BuildAction(output="out", rule="cat", inputs=["input1", "input2"])
+    build.add_variable(name="myvar", value="myval")
+    stream = build.stream()
+    next(stream)
+    self.assertEqual("  myvar = myval", next(stream))
+
+class TestPool(unittest.TestCase):
+
+  def test_pool(self):
+    pool = Pool(name="mypool", depth=10)
+    stream = pool.stream()
+    self.assertEqual("pool mypool", next(stream))
+    self.assertEqual("  depth = 10", next(stream))
+
+if __name__ == "__main__":
+  unittest.main()
diff --git a/orchestrator/ninja/test_ninja_writer.py b/orchestrator/ninja/test_ninja_writer.py
new file mode 100644
index 0000000..703dd4d
--- /dev/null
+++ b/orchestrator/ninja/test_ninja_writer.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+from io import StringIO
+
+from ninja_writer import Writer
+from ninja_syntax import Variable, Rule, BuildAction
+
+class TestWriter(unittest.TestCase):
+
+  def test_simple_writer(self):
+    with StringIO() as f:
+      writer = Writer(f)
+      writer.add_variable(Variable(name="cflags", value="-Wall"))
+      writer.add_newline()
+      cc = Rule(name="cc")
+      cc.add_variable(name="command", value="gcc $cflags -c $in -o $out")
+      writer.add_rule(cc)
+      writer.add_newline()
+      build_action = BuildAction(output="foo.o", rule="cc", inputs=["foo.c"])
+      writer.add_build_action(build_action)
+      writer.write()
+      self.assertEqual('''cflags = -Wall
+
+rule cc
+  command = gcc $cflags -c $in -o $out
+
+build foo.o: cc foo.c
+''', f.getvalue())
+
+  def test_comment(self):
+    with StringIO() as f:
+      writer = Writer(f)
+      writer.add_comment("This is a comment in a ninja file")
+      writer.write()
+      self.assertEqual("# This is a comment in a ninja file\n", f.getvalue())
+
+if __name__ == "__main__":
+  unittest.main()
diff --git a/target/board/BoardConfigGsiCommon.mk b/target/board/BoardConfigGsiCommon.mk
index 45cd515..53714a8 100644
--- a/target/board/BoardConfigGsiCommon.mk
+++ b/target/board/BoardConfigGsiCommon.mk
@@ -38,6 +38,12 @@
 #   updating the last seen rollback index in the tamper-evident storage.
 BOARD_AVB_ROLLBACK_INDEX := 0
 
+# The chained vbmeta settings for boot images.
+BOARD_AVB_BOOT_KEY_PATH := external/avb/test/data/testkey_rsa4096.pem
+BOARD_AVB_BOOT_ALGORITHM := SHA256_RSA4096
+BOARD_AVB_BOOT_ROLLBACK_INDEX := $(PLATFORM_SECURITY_PATCH_TIMESTAMP)
+BOARD_AVB_BOOT_ROLLBACK_INDEX_LOCATION := 2
+
 # Enable AVB chained partition for system.
 # https://android.googlesource.com/platform/external/avb/+/master/README.md
 BOARD_AVB_SYSTEM_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem
diff --git a/target/board/linux_bionic/BoardConfig.mk b/target/board/linux_bionic/BoardConfig.mk
new file mode 100644
index 0000000..7938bdb
--- /dev/null
+++ b/target/board/linux_bionic/BoardConfig.mk
@@ -0,0 +1,24 @@
+# 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.
+#
+
+# This "device" is only intended to be used for host Bionic build targets, so
+# (device) target architectures are irrelevant. However, the build system isn't
+# prepared to handle no target architectures at all, so pick something
+# arbitrarily.
+TARGET_ARCH_SUITE := ndk
+
+HOST_CROSS_OS := linux_bionic
+HOST_CROSS_ARCH := x86_64
+HOST_CROSS_2ND_ARCH :=
diff --git a/target/board/linux_bionic/README.md b/target/board/linux_bionic/README.md
new file mode 100644
index 0000000..8db77f2
--- /dev/null
+++ b/target/board/linux_bionic/README.md
@@ -0,0 +1,6 @@
+This "device" is suitable for Soong-only builds to create Bionic binaries for
+Linux hosts:
+
+```
+build/soong/soong_ui.bash --make-mode --soong-only TARGET_PRODUCT=linux_bionic ...
+```
diff --git a/target/product/AndroidProducts.mk b/target/product/AndroidProducts.mk
index ee702e5..67b0b17 100644
--- a/target/product/AndroidProducts.mk
+++ b/target/product/AndroidProducts.mk
@@ -74,6 +74,7 @@
 endif
 
 PRODUCT_MAKEFILES += \
+    $(LOCAL_DIR)/linux_bionic.mk \
     $(LOCAL_DIR)/mainline_sdk.mk \
     $(LOCAL_DIR)/module_arm.mk \
     $(LOCAL_DIR)/module_arm64.mk \
diff --git a/target/product/gsi/current.txt b/target/product/gsi/current.txt
index f9c1f3d..fd86d87 100644
--- a/target/product/gsi/current.txt
+++ b/target/product/gsi/current.txt
@@ -101,6 +101,7 @@
 VNDK-core: android.hardware.security.sharedsecret-V1-ndk.so
 VNDK-core: android.hardware.soundtrigger@2.0-core.so
 VNDK-core: android.hardware.soundtrigger@2.0.so
+VNDK-core: android.hardware.usb-V1-ndk.so
 VNDK-core: android.hardware.vibrator-V2-ndk.so
 VNDK-core: android.hardware.weaver-V1-ndk.so
 VNDK-core: android.hardware.wifi.hostapd-V1-ndk.so
diff --git a/target/product/linux_bionic.mk b/target/product/linux_bionic.mk
new file mode 100644
index 0000000..da6b890
--- /dev/null
+++ b/target/product/linux_bionic.mk
@@ -0,0 +1,18 @@
+# Copyright (C) 2022 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.
+#
+
+PRODUCT_NAME := linux_bionic
+PRODUCT_BRAND := Android
+PRODUCT_DEVICE := linux_bionic
diff --git a/tests/prefixed_sort_order/base-secondary.rbc b/tests/prefixed_sort_order/base-secondary.rbc
new file mode 100644
index 0000000..5446e8f
--- /dev/null
+++ b/tests/prefixed_sort_order/base-secondary.rbc
@@ -0,0 +1,21 @@
+# Copyright 2022 Google LLC
+#
+# 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
+#
+#      https://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.
+
+load("//build/make/core:product_config.rbc", "rblf")
+
+def init(g, handle):
+  cfg = rblf.cfg(handle)
+
+  g.setdefault("MY_VAR", [])
+  g["MY_VAR"] += ["foo"]
diff --git a/tests/prefixed_sort_order/base.rbc b/tests/prefixed_sort_order/base.rbc
new file mode 100644
index 0000000..05b0d5d
--- /dev/null
+++ b/tests/prefixed_sort_order/base.rbc
@@ -0,0 +1,21 @@
+# Copyright 2022 Google LLC
+#
+# 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
+#
+#      https://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.
+
+load("//build/make/core:product_config.rbc", "rblf")
+
+def init(g, handle):
+  cfg = rblf.cfg(handle)
+
+  g.setdefault("MY_VAR", [])
+  g["MY_VAR"] += ["bar"]
diff --git a/tests/prefixed_sort_order/product.rbc b/tests/prefixed_sort_order/product.rbc
new file mode 100644
index 0000000..619b2c0
--- /dev/null
+++ b/tests/prefixed_sort_order/product.rbc
@@ -0,0 +1,29 @@
+# Copyright 2022 Google LLC
+#
+# 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
+#
+#      https://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.
+
+load("//build/make/core:product_config.rbc", "rblf")
+load(":base.rbc", _base_init = "init")
+load(":base-secondary.rbc", _base_secondary_init = "init")
+
+def init(g, handle):
+  cfg = rblf.cfg(handle)
+
+  # It's important that base-secondary uses a dash, an underscore won't expose the sort order issue:
+  # >>> sorted(["base", "base-secondary"])
+  # ['base', 'base-secondary']
+  # >>> sorted(["base.mk", "base-secondary.mk"])
+  # ['base-secondary.mk', 'base.mk']
+
+  rblf.inherit(handle, "base", _base_init)
+  rblf.inherit(handle, "base-secondary", _base_secondary_init)
diff --git a/tests/prefixed_sort_order/test.rbc b/tests/prefixed_sort_order/test.rbc
new file mode 100644
index 0000000..e59a509
--- /dev/null
+++ b/tests/prefixed_sort_order/test.rbc
@@ -0,0 +1,26 @@
+# Copyright 2022 Google LLC
+#
+# 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
+#
+#      https://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.
+
+load("//build/make/core:product_config.rbc", "rblf")
+load("//build/make/tests/input_variables.rbc", input_variables_init = "init")
+load(":product.rbc", "init")
+
+
+def assert_eq(expected, actual):
+    if expected != actual:
+        fail("Expected '%s', got '%s'" % (expected, actual))
+
+def test():
+    (globals, globals_base) = rblf.product_configuration("test/device", init, input_variables_init)
+    assert_eq(["foo", "bar"], globals["MY_VAR"])
diff --git a/tests/run.rbc b/tests/run.rbc
index 56ba394..c6dfeba 100644
--- a/tests/run.rbc
+++ b/tests/run.rbc
@@ -27,6 +27,7 @@
 load(":board_input_vars.rbc", board_input_vars_init = "init")
 load("//build/make/tests/single_value_inheritance:test.rbc", test_single_value_inheritance = "test")
 load("//build/make/tests/artifact_path_requirements:test.rbc", test_artifact_path_requirements = "test")
+load("//build/make/tests/prefixed_sort_order:test.rbc", test_prefixed_sort_order = "test")
 
 def assert_eq(expected, actual):
     if expected != actual:
@@ -42,7 +43,7 @@
 
 assert_eq("", rblf.mkstrip(" \n \t    "))
 assert_eq("a b c", rblf.mkstrip("  a b   \n  c \t"))
-assert_eq(1, rblf.mkstrip(1))
+assert_eq("1", rblf.mkstrip("1 "))
 
 assert_eq("b1 b2", rblf.mksubst("a", "b", "a1 a2"))
 assert_eq(["b1", "x2"], rblf.mksubst("a", "b", ["a1", "x2"]))
@@ -72,6 +73,27 @@
 assert_eq("", rblf.notdir("/"))
 assert_eq("", rblf.notdir(""))
 
+cwd = rblf_shell('pwd')
+assert_eq(cwd+"/foo/bar", rblf.abspath("foo/bar"))
+assert_eq(cwd+"/bar", rblf.abspath("foo/.././bar"))
+assert_eq(cwd+"/bar", rblf.abspath("foo/..////bar//"))
+assert_eq("/foo/baz", rblf.abspath("/foo/bar/../baz"))
+assert_eq(cwd+"/foo/bar "+cwd+"/foo/baz", rblf.abspath("foo/bar foo/baz"))
+assert_eq("/baz", rblf.abspath("/../../../../../../../../../../../../../../../../baz"))
+
+assert_eq("foo", rblf.first_word("foo bar"))
+assert_eq("foo", rblf.first_word(["foo", "bar"]))
+assert_eq("", rblf.first_word(""))
+assert_eq("", rblf.first_word([]))
+assert_eq("bar", rblf.last_word("foo bar"))
+assert_eq("bar", rblf.last_word(["foo", "bar"]))
+assert_eq("", rblf.last_word(""))
+assert_eq("", rblf.last_word([]))
+
+assert_eq(["foo", "bar"], rblf.flatten_2d_list([["foo", "bar"]]))
+assert_eq(["foo", "bar"], rblf.flatten_2d_list([["foo"], ["bar"]]))
+assert_eq([], rblf.flatten_2d_list([]))
+
 assert_eq(
     ["build/make/tests/board.rbc", "build/make/tests/board_input_vars.rbc"],
     rblf.expand_wildcard("build/make/tests/board*.rbc")
@@ -142,3 +164,4 @@
 
 test_single_value_inheritance()
 test_artifact_path_requirements()
+test_prefixed_sort_order()
diff --git a/tools/rbcrun/host.go b/tools/rbcrun/host.go
index c6e89f0..32afa45 100644
--- a/tools/rbcrun/host.go
+++ b/tools/rbcrun/host.go
@@ -20,6 +20,7 @@
 	"os"
 	"os/exec"
 	"path/filepath"
+	"sort"
 	"strings"
 
 	"go.starlark.net/starlark"
@@ -111,19 +112,6 @@
 	return e.globals, e.err
 }
 
-// fileExists returns True if file with given name exists.
-func fileExists(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
-	kwargs []starlark.Tuple) (starlark.Value, error) {
-	var path string
-	if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &path); err != nil {
-		return starlark.None, err
-	}
-	if _, err := os.Stat(path); err != nil {
-		return starlark.False, nil
-	}
-	return starlark.True, nil
-}
-
 // wildcard(pattern, top=None) expands shell's glob pattern. If 'top' is present,
 // the 'top/pattern' is globbed and then 'top/' prefix is removed.
 func wildcard(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
@@ -150,6 +138,10 @@
 			files[i] = strings.TrimPrefix(files[i], prefix)
 		}
 	}
+	// Kati uses glob(3) with no flags, which means it's sorted
+	// because GLOB_NOSORT is not passed. Go's glob is not
+	// guaranteed to sort the results.
+	sort.Strings(files)
 	return makeStringList(files), nil
 }
 
@@ -269,8 +261,6 @@
 		"struct":   starlark.NewBuiltin("struct", starlarkstruct.Make),
 		"rblf_cli": structFromEnv(env),
 		"rblf_env": structFromEnv(os.Environ()),
-		// To convert makefile's $(wildcard foo)
-		"rblf_file_exists": starlark.NewBuiltin("rblf_file_exists", fileExists),
 		// To convert find-copy-subdir and product-copy-files-by pattern
 		"rblf_find_files": starlark.NewBuiltin("rblf_find_files", find),
 		// To convert makefile's $(shell cmd)
diff --git a/tools/rbcrun/testdata/file_ops.star b/tools/rbcrun/testdata/file_ops.star
index 50e39bf..2ee78fc 100644
--- a/tools/rbcrun/testdata/file_ops.star
+++ b/tools/rbcrun/testdata/file_ops.star
@@ -4,9 +4,6 @@
 
 def test():
     myname = "file_ops.star"
-    assert.true(rblf_file_exists("."), "./ exists ")
-    assert.true(rblf_file_exists(myname), "the file %s does exist" % myname)
-    assert.true(not rblf_file_exists("no_such_file"), "the file no_such_file does not exist")
     files = rblf_wildcard("*.star")
     assert.true(myname in files, "expected %s in  %s" % (myname, files))
     files = rblf_wildcard("*.star", rblf_env.TEST_DATA_DIR)
diff --git a/tools/rbcrun/testdata/module1.star b/tools/rbcrun/testdata/module1.star
index 913fb7d..be04f75 100644
--- a/tools/rbcrun/testdata/module1.star
+++ b/tools/rbcrun/testdata/module1.star
@@ -2,6 +2,6 @@
 load("assert.star", "assert")
 
 # Make sure that builtins are defined for the loaded module, too
-assert.true(rblf_file_exists("module1.star"))
-assert.true(not rblf_file_exists("no_such file"))
+assert.true(rblf_wildcard("module1.star"))
+assert.true(not rblf_wildcard("no_such file"))
 test = "module1"
diff --git a/tools/releasetools/add_img_to_target_files.py b/tools/releasetools/add_img_to_target_files.py
index e3db161..09f69d0 100644
--- a/tools/releasetools/add_img_to_target_files.py
+++ b/tools/releasetools/add_img_to_target_files.py
@@ -871,7 +871,7 @@
 
   if has_vendor_kernel_boot:
     banner("vendor_kernel_boot")
-    vendor_kernel_boot_image = common.GetVendorBootImage(
+    vendor_kernel_boot_image = common.GetVendorKernelBootImage(
         "IMAGES/vendor_kernel_boot.img", "vendor_kernel_boot.img", OPTIONS.input_tmp,
         "VENDOR_KERNEL_BOOT")
     if vendor_kernel_boot_image:
diff --git a/tools/releasetools/build_image.py b/tools/releasetools/build_image.py
index e33b581..7fdf4ba 100755
--- a/tools/releasetools/build_image.py
+++ b/tools/releasetools/build_image.py
@@ -348,6 +348,8 @@
       build_command.extend(["-C", prop_dict["erofs_pcluster_size"]])
     if "erofs_share_dup_blocks" in prop_dict:
       build_command.extend(["--chunksize", "4096"])
+    if "erofs_use_legacy_compression" in prop_dict:
+      build_command.extend(["-E", "legacy-compress"])
 
     build_command.extend([out_file, in_dir])
     if "erofs_sparse_flag" in prop_dict and not disable_sparse:
@@ -653,6 +655,7 @@
       "erofs_pcluster_size",
       "erofs_share_dup_blocks",
       "erofs_sparse_flag",
+      "erofs_use_legacy_compression",
       "squashfs_sparse_flag",
       "system_f2fs_compress",
       "system_f2fs_sldc_flags",
@@ -698,6 +701,7 @@
       (True, "avb_{}_hashtree_enable", "avb_hashtree_enable"),
       (True, "avb_{}_key_path", "avb_key_path"),
       (True, "avb_{}_salt", "avb_salt"),
+      (True, "erofs_use_legacy_compression", "erofs_use_legacy_compression"),
       (True, "ext4_share_dup_blocks", "ext4_share_dup_blocks"),
       (True, "{}_base_fs_file", "base_fs_file"),
       (True, "{}_disable_sparse", "disable_sparse"),
@@ -810,16 +814,18 @@
 
 
 def main(argv):
-  if len(argv) != 4:
+  args = common.ParseOptions(argv, __doc__)
+
+  if len(args) != 4:
     print(__doc__)
     sys.exit(1)
 
   common.InitLogging()
 
-  in_dir = argv[0]
-  glob_dict_file = argv[1]
-  out_file = argv[2]
-  target_out = argv[3]
+  in_dir = args[0]
+  glob_dict_file = args[1]
+  out_file = args[2]
+  target_out = args[3]
 
   glob_dict = LoadGlobalDict(glob_dict_file)
   if "mount_point" in glob_dict:
diff --git a/tools/releasetools/common.py b/tools/releasetools/common.py
index bd3af68..cff7542 100644
--- a/tools/releasetools/common.py
+++ b/tools/releasetools/common.py
@@ -114,7 +114,7 @@
 # AVB_FOOTER_ARGS_BY_PARTITION in sign_target_files_apks need to be updated
 # accordingly.
 AVB_PARTITIONS = ('boot', 'init_boot', 'dtbo', 'odm', 'product', 'pvmfw', 'recovery',
-                  'system', 'system_ext', 'vendor', 'vendor_boot',
+                  'system', 'system_ext', 'vendor', 'vendor_boot', 'vendor_kernel_boot',
                   'vendor_dlkm', 'odm_dlkm', 'system_dlkm')
 
 # Chained VBMeta partitions.
@@ -455,6 +455,11 @@
     return vabc_enabled
 
   @property
+  def is_android_r(self):
+    system_prop = self.info_dict.get("system.build.prop")
+    return system_prop and system_prop.GetProp("ro.build.version.release") == "11"
+
+  @property
   def is_vabc_xor(self):
     vendor_prop = self.info_dict.get("vendor.build.prop")
     vabc_xor_enabled = vendor_prop and \
@@ -725,7 +730,7 @@
   GZ = 2
 
 
-def _GetRamdiskFormat(info_dict):
+def GetRamdiskFormat(info_dict):
   if info_dict.get('lz4_ramdisks') == 'true':
     ramdisk_format = RamdiskFormat.LZ4
   else:
@@ -834,7 +839,7 @@
 
   # Load recovery fstab if applicable.
   d["fstab"] = _FindAndLoadRecoveryFstab(d, input_file, read_helper)
-  ramdisk_format = _GetRamdiskFormat(d)
+  ramdisk_format = GetRamdiskFormat(d)
 
   # Tries to load the build props for all partitions with care_map, including
   # system and vendor.
@@ -1188,10 +1193,14 @@
     return " ".join(sorted(combined))
 
   if (framework_dict.get("use_dynamic_partitions") !=
-          "true") or (vendor_dict.get("use_dynamic_partitions") != "true"):
+        "true") or (vendor_dict.get("use_dynamic_partitions") != "true"):
     raise ValueError("Both dictionaries must have use_dynamic_partitions=true")
 
   merged_dict = {"use_dynamic_partitions": "true"}
+  # For keys-value pairs that are the same, copy to merged dict
+  for key in vendor_dict.keys():
+    if key in framework_dict and framework_dict[key] == vendor_dict[key]:
+      merged_dict[key] = vendor_dict[key]
 
   merged_dict["dynamic_partition_list"] = uniq_concat(
       framework_dict.get("dynamic_partition_list", ""),
@@ -1575,7 +1584,7 @@
   img = tempfile.NamedTemporaryFile()
 
   if has_ramdisk:
-    ramdisk_format = _GetRamdiskFormat(info_dict)
+    ramdisk_format = GetRamdiskFormat(info_dict)
     ramdisk_img = _MakeRamdisk(sourcedir, fs_config_file,
                                ramdisk_format=ramdisk_format)
 
@@ -1782,6 +1791,9 @@
   if info_dict.get("recovery_as_boot") == "true":
     return True  # the recovery-as-boot boot.img has a RECOVERY ramdisk.
 
+  if info_dict.get("gki_boot_image_without_ramdisk") == "true":
+    return False  # A GKI boot.img has no ramdisk since Android-13.
+
   if info_dict.get("system_root_image") == "true":
     # The ramdisk content is merged into the system.img, so there is NO
     # ramdisk in the boot.img or boot-<kernel version>.img.
@@ -1838,7 +1850,7 @@
   return None
 
 
-def _BuildVendorBootImage(sourcedir, info_dict=None):
+def _BuildVendorBootImage(sourcedir, partition_name, info_dict=None):
   """Build a vendor boot image from the specified sourcedir.
 
   Take a ramdisk, dtb, and vendor_cmdline from the input (in 'sourcedir'), and
@@ -1853,7 +1865,7 @@
 
   img = tempfile.NamedTemporaryFile()
 
-  ramdisk_format = _GetRamdiskFormat(info_dict)
+  ramdisk_format = GetRamdiskFormat(info_dict)
   ramdisk_img = _MakeRamdisk(sourcedir, ramdisk_format=ramdisk_format)
 
   # use MKBOOTIMG from environ, or "mkbootimg" if empty or not set
@@ -1863,8 +1875,13 @@
 
   fn = os.path.join(sourcedir, "dtb")
   if os.access(fn, os.F_OK):
-    cmd.append("--dtb")
-    cmd.append(fn)
+    has_vendor_kernel_boot = (info_dict.get("vendor_kernel_boot", "").lower() == "true")
+
+    # Pack dtb into vendor_kernel_boot if building vendor_kernel_boot.
+    # Otherwise pack dtb into vendor_boot.
+    if not has_vendor_kernel_boot or partition_name == "vendor_kernel_boot":
+      cmd.append("--dtb")
+      cmd.append(fn)
 
   fn = os.path.join(sourcedir, "vendor_cmdline")
   if os.access(fn, os.F_OK):
@@ -1924,11 +1941,11 @@
   # AVB: if enabled, calculate and add hash.
   if info_dict.get("avb_enable") == "true":
     avbtool = info_dict["avb_avbtool"]
-    part_size = info_dict["vendor_boot_size"]
+    part_size = info_dict[f'{partition_name}_size']
     cmd = [avbtool, "add_hash_footer", "--image", img.name,
-           "--partition_size", str(part_size), "--partition_name", "vendor_boot"]
-    AppendAVBSigningArgs(cmd, "vendor_boot")
-    args = info_dict.get("avb_vendor_boot_add_hash_footer_args")
+           "--partition_size", str(part_size), "--partition_name", partition_name]
+    AppendAVBSigningArgs(cmd, partition_name)
+    args = info_dict.get(f'avb_{partition_name}_add_hash_footer_args')
     if args and args.strip():
       cmd.extend(shlex.split(args))
     RunAndCheckOutput(cmd)
@@ -1962,7 +1979,31 @@
     info_dict = OPTIONS.info_dict
 
   data = _BuildVendorBootImage(
-      os.path.join(unpack_dir, tree_subdir), info_dict)
+      os.path.join(unpack_dir, tree_subdir), "vendor_boot", info_dict)
+  if data:
+    return File(name, data)
+  return None
+
+
+def GetVendorKernelBootImage(name, prebuilt_name, unpack_dir, tree_subdir,
+                       info_dict=None):
+  """Return a File object with the desired vendor kernel boot image.
+
+  Look for it under 'unpack_dir'/IMAGES, otherwise construct it from
+  the source files in 'unpack_dir'/'tree_subdir'."""
+
+  prebuilt_path = os.path.join(unpack_dir, "IMAGES", prebuilt_name)
+  if os.path.exists(prebuilt_path):
+    logger.info("using prebuilt %s from IMAGES...", prebuilt_name)
+    return File.FromLocalFile(name, prebuilt_path)
+
+  logger.info("building image from target_files %s...", tree_subdir)
+
+  if info_dict is None:
+    info_dict = OPTIONS.info_dict
+
+  data = _BuildVendorBootImage(
+      os.path.join(unpack_dir, tree_subdir), "vendor_kernel_boot", info_dict)
   if data:
     return File(name, data)
   return None
diff --git a/tools/releasetools/merge/merge_target_files.py b/tools/releasetools/merge/merge_target_files.py
index c06fd4c..c95cead 100755
--- a/tools/releasetools/merge/merge_target_files.py
+++ b/tools/releasetools/merge/merge_target_files.py
@@ -149,6 +149,13 @@
 OPTIONS.vendor_dexpreopt_config = None
 
 
+def move_only_exists(source, destination):
+  """Judge whether the file exists and then move the file."""
+
+  if os.path.exists(source):
+    shutil.move(source, destination)
+
+
 def create_merged_package(temp_dir):
   """Merges two target files packages into one target files structure.
 
@@ -286,9 +293,8 @@
   shutil.move(
       os.path.join(vendor_target_files_dir, 'IMAGES', partition_img),
       os.path.join(target_files_dir, 'IMAGES', partition_img))
-  shutil.move(
-      os.path.join(vendor_target_files_dir, 'IMAGES', partition_map),
-      os.path.join(target_files_dir, 'IMAGES', partition_map))
+  move_only_exists(os.path.join(vendor_target_files_dir, 'IMAGES', partition_map),
+        os.path.join(target_files_dir, 'IMAGES', partition_map))
 
   def copy_recovery_file(filename):
     for subdir in ('VENDOR', 'SYSTEM/vendor'):
diff --git a/tools/releasetools/merge/merge_utils.py b/tools/releasetools/merge/merge_utils.py
index f623ad2..e253b02 100644
--- a/tools/releasetools/merge/merge_utils.py
+++ b/tools/releasetools/merge/merge_utils.py
@@ -100,20 +100,16 @@
   has_error = False
 
   # Check that partitions only come from one input.
-  for partition in _FRAMEWORK_PARTITIONS.union(_VENDOR_PARTITIONS):
-    image_path = 'IMAGES/{}.img'.format(partition.lower().replace('/', ''))
-    in_framework = (
-        any(item.startswith(partition) for item in OPTIONS.framework_item_list)
-        or image_path in OPTIONS.framework_item_list)
-    in_vendor = (
-        any(item.startswith(partition) for item in OPTIONS.vendor_item_list) or
-        image_path in OPTIONS.vendor_item_list)
-    if in_framework and in_vendor:
-      logger.error(
-          'Cannot extract items from %s for both the framework and vendor'
-          ' builds. Please ensure only one merge config item list'
-          ' includes %s.', partition, partition)
-      has_error = True
+  framework_partitions = ItemListToPartitionSet(OPTIONS.framework_item_list)
+  vendor_partitions = ItemListToPartitionSet(OPTIONS.vendor_item_list)
+  from_both = framework_partitions.intersection(vendor_partitions)
+  if from_both:
+    logger.error(
+        'Cannot extract items from the same partition in both the '
+        'framework and vendor builds. Please ensure only one merge config '
+        'item list (or inferred list) includes each partition: %s' %
+        ','.join(from_both))
+    has_error = True
 
   if any([
       key in OPTIONS.framework_misc_info_keys
@@ -131,7 +127,8 @@
 # system partition). The following regex matches this and extracts the
 # partition name.
 
-_PARTITION_ITEM_PATTERN = re.compile(r'^([A-Z_]+)/\*$')
+_PARTITION_ITEM_PATTERN = re.compile(r'^([A-Z_]+)/.*$')
+_IMAGE_PARTITION_PATTERN = re.compile(r'^IMAGES/(.*)\.img$')
 
 
 def ItemListToPartitionSet(item_list):
@@ -154,62 +151,89 @@
   partition_set = set()
 
   for item in item_list:
-    partition_match = _PARTITION_ITEM_PATTERN.search(item.strip())
-    partition_tag = partition_match.group(
-        1).lower() if partition_match else None
-
-    if partition_tag:
-      partition_set.add(partition_tag)
+    for pattern in (_PARTITION_ITEM_PATTERN, _IMAGE_PARTITION_PATTERN):
+      partition_match = pattern.search(item.strip())
+      if partition_match:
+        partition = partition_match.group(1).lower()
+        # These directories in target-files are not actual partitions.
+        if partition not in ('meta', 'images'):
+          partition_set.add(partition)
 
   return partition_set
 
 
 # Partitions that are grabbed from the framework partial build by default.
 _FRAMEWORK_PARTITIONS = {
-    'system', 'product', 'system_ext', 'system_other', 'root', 'system_dlkm'
-}
-# Partitions that are grabbed from the vendor partial build by default.
-_VENDOR_PARTITIONS = {
-    'vendor', 'odm', 'oem', 'boot', 'vendor_boot', 'recovery',
-    'prebuilt_images', 'radio', 'data', 'vendor_dlkm', 'odm_dlkm'
+    'system', 'product', 'system_ext', 'system_other', 'root', 'system_dlkm',
+    'vbmeta_system'
 }
 
 
 def InferItemList(input_namelist, framework):
-  item_list = []
+  item_set = set()
 
-  # Some META items are grabbed from partial builds directly.
+  # Some META items are always grabbed from partial builds directly.
   # Others are combined in merge_meta.py.
   if framework:
-    item_list.extend([
+    item_set.update([
         'META/liblz4.so',
         'META/postinstall_config.txt',
         'META/update_engine_config.txt',
         'META/zucchini_config.txt',
     ])
   else:  # vendor
-    item_list.extend([
+    item_set.update([
         'META/kernel_configs.txt',
         'META/kernel_version.txt',
         'META/otakeys.txt',
+        'META/pack_radioimages.txt',
         'META/releasetools.py',
-        'OTA/android-info.txt',
     ])
 
   # Grab a set of items for the expected partitions in the partial build.
-  for partition in (_FRAMEWORK_PARTITIONS if framework else _VENDOR_PARTITIONS):
-    for namelist in input_namelist:
-      if namelist.startswith('%s/' % partition.upper()):
-        fs_config_prefix = '' if partition == 'system' else '%s_' % partition
-        item_list.extend([
-            '%s/*' % partition.upper(),
-            'IMAGES/%s.img' % partition,
-            'IMAGES/%s.map' % partition,
-            'META/%sfilesystem_config.txt' % fs_config_prefix,
-        ])
-        break
+  seen_partitions = []
+  for namelist in input_namelist:
+    if namelist.endswith('/'):
+      continue
 
-  return sorted(item_list)
+    partition = namelist.split('/')[0].lower()
+
+    # META items are grabbed above, or merged later.
+    if partition == 'meta':
+      continue
+
+    if partition == 'images':
+      image_partition, extension = os.path.splitext(os.path.basename(namelist))
+      if image_partition == 'vbmeta':
+        # Always regenerate vbmeta.img since it depends on hash information
+        # from both builds.
+        continue
+      if extension in ('.img', '.map'):
+        # Include image files in IMAGES/* if the partition comes from
+        # the expected set.
+        if (framework and image_partition in _FRAMEWORK_PARTITIONS) or (
+            not framework and image_partition not in _FRAMEWORK_PARTITIONS):
+          item_set.add(namelist)
+      elif not framework:
+        # Include all miscellaneous non-image files in IMAGES/* from
+        # the vendor build.
+        item_set.add(namelist)
+      continue
+
+    # Skip already-visited partitions.
+    if partition in seen_partitions:
+      continue
+    seen_partitions.append(partition)
+
+    if (framework and partition in _FRAMEWORK_PARTITIONS) or (
+        not framework and partition not in _FRAMEWORK_PARTITIONS):
+      fs_config_prefix = '' if partition == 'system' else '%s_' % partition
+      item_set.update([
+          '%s/*' % partition.upper(),
+          'META/%sfilesystem_config.txt' % fs_config_prefix,
+      ])
+
+  return sorted(item_set)
 
 
 def InferFrameworkMiscInfoKeys(input_namelist):
@@ -223,8 +247,8 @@
   ]
 
   for partition in _FRAMEWORK_PARTITIONS:
-    for namelist in input_namelist:
-      if namelist.startswith('%s/' % partition.upper()):
+    for partition_dir in ('%s/' % partition.upper(), 'SYSTEM/%s/' % partition):
+      if partition_dir in input_namelist:
         fs_type_prefix = '' if partition == 'system' else '%s_' % partition
         keys.extend([
             'avb_%s_hashtree_enable' % partition,
diff --git a/tools/releasetools/merge/test_merge_utils.py b/tools/releasetools/merge/test_merge_utils.py
index 1949050..eceb734 100644
--- a/tools/releasetools/merge/test_merge_utils.py
+++ b/tools/releasetools/merge/test_merge_utils.py
@@ -108,20 +108,27 @@
 
   def test_ItemListToPartitionSet(self):
     item_list = [
+        'IMAGES/system_ext.img',
         'META/apexkeys.txt',
         'META/apkcerts.txt',
         'META/filesystem_config.txt',
         'PRODUCT/*',
         'SYSTEM/*',
-        'SYSTEM_EXT/*',
+        'SYSTEM/system_ext/*',
     ]
     partition_set = merge_utils.ItemListToPartitionSet(item_list)
     self.assertEqual(set(['product', 'system', 'system_ext']), partition_set)
 
   def test_InferItemList_Framework(self):
     zip_namelist = [
+        'IMAGES/product.img',
+        'IMAGES/product.map',
+        'IMAGES/system.img',
+        'IMAGES/system.map',
         'SYSTEM/my_system_file',
         'PRODUCT/my_product_file',
+        # Device does not use a separate system_ext partition.
+        'SYSTEM/system_ext/system_ext_file',
     ]
 
     item_list = merge_utils.InferItemList(zip_namelist, framework=True)
@@ -147,37 +154,55 @@
     zip_namelist = [
         'VENDOR/my_vendor_file',
         'ODM/my_odm_file',
+        'IMAGES/odm.img',
+        'IMAGES/odm.map',
+        'IMAGES/vendor.img',
+        'IMAGES/vendor.map',
+        'IMAGES/my_custom_image.img',
+        'IMAGES/my_custom_file.txt',
+        'IMAGES/vbmeta.img',
+        'CUSTOM_PARTITION/my_custom_file',
+        # Leftover framework pieces that shouldn't be grabbed.
+        'IMAGES/system.img',
+        'SYSTEM/system_file',
     ]
 
     item_list = merge_utils.InferItemList(zip_namelist, framework=False)
 
     expected_vendor_item_list = [
+        'CUSTOM_PARTITION/*',
+        'IMAGES/my_custom_file.txt',
+        'IMAGES/my_custom_image.img',
         'IMAGES/odm.img',
         'IMAGES/odm.map',
         'IMAGES/vendor.img',
         'IMAGES/vendor.map',
+        'META/custom_partition_filesystem_config.txt',
         'META/kernel_configs.txt',
         'META/kernel_version.txt',
         'META/odm_filesystem_config.txt',
         'META/otakeys.txt',
+        'META/pack_radioimages.txt',
         'META/releasetools.py',
         'META/vendor_filesystem_config.txt',
         'ODM/*',
-        'OTA/android-info.txt',
         'VENDOR/*',
     ]
     self.assertEqual(item_list, expected_vendor_item_list)
 
   def test_InferFrameworkMiscInfoKeys(self):
     zip_namelist = [
-        'SYSTEM/my_system_file',
-        'SYSTEM_EXT/my_system_ext_file',
+        'PRODUCT/',
+        'SYSTEM/',
+        'SYSTEM/system_ext/',
     ]
 
     keys = merge_utils.InferFrameworkMiscInfoKeys(zip_namelist)
 
     expected_keys = [
         'ab_update',
+        'avb_product_add_hashtree_footer_args',
+        'avb_product_hashtree_enable',
         'avb_system_add_hashtree_footer_args',
         'avb_system_ext_add_hashtree_footer_args',
         'avb_system_ext_hashtree_enable',
@@ -186,10 +211,13 @@
         'avb_vbmeta_system_algorithm',
         'avb_vbmeta_system_key_path',
         'avb_vbmeta_system_rollback_index_location',
+        'building_product_image',
         'building_system_ext_image',
         'building_system_image',
         'default_system_dev_certificate',
         'fs_type',
+        'product_disable_sparse',
+        'product_fs_type',
         'system_disable_sparse',
         'system_ext_disable_sparse',
         'system_ext_fs_type',
diff --git a/tools/releasetools/ota_from_target_files.py b/tools/releasetools/ota_from_target_files.py
index 522d489..5384699 100755
--- a/tools/releasetools/ota_from_target_files.py
+++ b/tools/releasetools/ota_from_target_files.py
@@ -1068,10 +1068,11 @@
         pre_partition_state, post_partition_state):
   assert pre_partition_state is not None
   partition_timestamps = {}
-  for part in pre_partition_state:
-    partition_timestamps[part.partition_name] = part.version
   for part in post_partition_state:
-    partition_timestamps[part.partition_name] = \
+    partition_timestamps[part.partition_name] = part.version
+  for part in pre_partition_state:
+    if part.partition_name in partition_timestamps:
+      partition_timestamps[part.partition_name] = \
         max(part.version, partition_timestamps[part.partition_name])
   return [
       "--partition_timestamps",
@@ -1145,6 +1146,14 @@
       logger.info("Either source or target does not support VABC, disabling.")
       OPTIONS.disable_vabc = True
 
+    # Virtual AB Compression was introduced in Androd S.
+    # Later, we backported VABC to Android R. But verity support was not
+    # backported, so if VABC is used and we are on Android R, disable
+    # verity computation.
+    if not OPTIONS.disable_vabc and source_info.is_android_r:
+      OPTIONS.disable_verity_computation = True
+      OPTIONS.disable_fec_computation = True
+
   else:
     assert "ab_partitions" in OPTIONS.info_dict, \
         "META/ab_partitions.txt is required for ab_update."
@@ -1208,6 +1217,8 @@
         metadata.postcondition.partition_state)
 
   if not ota_utils.IsZucchiniCompatible(source_file, target_file):
+    logger.warning(
+        "Builds doesn't support zucchini, or source/target don't have compatible zucchini versions. Disabling zucchini.")
     OPTIONS.enable_zucchini = False
 
   additional_args += ["--enable_zucchini",
diff --git a/tools/releasetools/ota_utils.py b/tools/releasetools/ota_utils.py
index 5d403dc..ef1dca2 100644
--- a/tools/releasetools/ota_utils.py
+++ b/tools/releasetools/ota_utils.py
@@ -22,7 +22,8 @@
 import ota_metadata_pb2
 from common import (ZipDelete, ZipClose, OPTIONS, MakeTempFile,
                     ZipWriteStr, BuildInfo, LoadDictionaryFromFile,
-                    SignFile, PARTITIONS_WITH_BUILD_PROP, PartitionBuildProps)
+                    SignFile, PARTITIONS_WITH_BUILD_PROP, PartitionBuildProps,
+                    GetRamdiskFormat)
 
 logger = logging.getLogger(__name__)
 
@@ -371,15 +372,18 @@
     for partition in PARTITIONS_WITH_BUILD_PROP:
       partition_prop_key = "{}.build.prop".format(partition)
       input_file = info_dict[partition_prop_key].input_file
+      ramdisk = GetRamdiskFormat(info_dict)
       if isinstance(input_file, zipfile.ZipFile):
         with zipfile.ZipFile(input_file.filename, allowZip64=True) as input_zip:
           info_dict[partition_prop_key] = \
               PartitionBuildProps.FromInputFile(input_zip, partition,
-                                                placeholder_values)
+                                                placeholder_values,
+                                                ramdisk)
       else:
         info_dict[partition_prop_key] = \
             PartitionBuildProps.FromInputFile(input_file, partition,
-                                              placeholder_values)
+                                              placeholder_values,
+                                              ramdisk)
     info_dict["build.prop"] = info_dict["system.build.prop"]
     build_info_set.add(BuildInfo(info_dict, default_build_info.oem_dicts))
 
@@ -693,6 +697,7 @@
       if os.path.exists(entry_path):
         with open(entry_path, "r") as fp:
           return fp.read()
-      else:
-        return ""
-  return ReadEntry(source_file, _ZUCCHINI_CONFIG_ENTRY_NAME) == ReadEntry(target_file, _ZUCCHINI_CONFIG_ENTRY_NAME)
+    return False
+  sourceEntry = ReadEntry(source_file, _ZUCCHINI_CONFIG_ENTRY_NAME)
+  targetEntry = ReadEntry(target_file, _ZUCCHINI_CONFIG_ENTRY_NAME)
+  return sourceEntry and targetEntry and sourceEntry == targetEntry
diff --git a/tools/releasetools/sign_target_files_apks.py b/tools/releasetools/sign_target_files_apks.py
index c803340..f363afd 100755
--- a/tools/releasetools/sign_target_files_apks.py
+++ b/tools/releasetools/sign_target_files_apks.py
@@ -226,6 +226,7 @@
     'pvmfw': 'avb_pvmfw_add_hash_footer_args',
     'vendor': 'avb_vendor_add_hashtree_footer_args',
     'vendor_boot': 'avb_vendor_boot_add_hash_footer_args',
+    'vendor_kernel_boot': 'avb_vendor_kernel_boot_add_hash_footer_args',
     'vendor_dlkm': "avb_vendor_dlkm_add_hashtree_footer_args",
     'vbmeta': 'avb_vbmeta_args',
     'vbmeta_system': 'avb_vbmeta_system_args',
@@ -1354,12 +1355,13 @@
     vendor_misc_info["no_boot"] = "true"  # boot
     vendor_misc_info["vendor_boot"] = "false"  # vendor_boot
     vendor_misc_info["no_recovery"] = "true"  # recovery
+    vendor_misc_info["avb_enable"] = "false"  # vbmeta
 
   vendor_misc_info["board_bpt_enable"] = "false"  # partition-table
   vendor_misc_info["has_dtbo"] = "false"  # dtbo
   vendor_misc_info["has_pvmfw"] = "false"  # pvmfw
   vendor_misc_info["avb_custom_images_partition_list"] = ""  # custom images
-  vendor_misc_info["avb_enable"] = "false"  # vbmeta
+  vendor_misc_info["avb_building_vbmeta_image"] = "false" # skip building vbmeta
   vendor_misc_info["use_dynamic_partitions"] = "false"  # super_empty
   vendor_misc_info["build_super_partition"] = "false"  # super split
   with open(vendor_misc_info_path, "w") as output:
@@ -1414,8 +1416,12 @@
       map_file_path = "IMAGES/{}.map".format(p)
       common.ZipWrite(output_zip, os.path.join(vendor_tempdir, img_file_path), img_file_path)
       common.ZipWrite(output_zip, os.path.join(vendor_tempdir, map_file_path), map_file_path)
-    # copy recovery patch & install.sh
+    # copy recovery.img, boot.img, recovery patch & install.sh
     if OPTIONS.rebuild_recovery:
+      recovery_img = "IMAGES/recovery.img"
+      boot_img = "IMAGES/boot.img"
+      common.ZipWrite(output_zip, os.path.join(vendor_tempdir, recovery_img), recovery_img)
+      common.ZipWrite(output_zip, os.path.join(vendor_tempdir, boot_img), boot_img)
       recovery_patch_path = "VENDOR/recovery-from-boot.p"
       recovery_sh_path = "VENDOR/bin/install-recovery.sh"
       common.ZipWrite(output_zip, os.path.join(vendor_tempdir, recovery_patch_path), recovery_patch_path)
diff --git a/tools/signapk/src/com/android/signapk/SignApk.java b/tools/signapk/src/com/android/signapk/SignApk.java
index c127dbe..36a220c 100644
--- a/tools/signapk/src/com/android/signapk/SignApk.java
+++ b/tools/signapk/src/com/android/signapk/SignApk.java
@@ -901,7 +901,7 @@
      * Tries to load a JSE Provider by class name. This is for custom PrivateKey
      * types that might be stored in PKCS#11-like storage.
      */
-    private static void loadProviderIfNecessary(String providerClassName) {
+    private static void loadProviderIfNecessary(String providerClassName, String providerArg) {
         if (providerClassName == null) {
             return;
         }
@@ -920,27 +920,41 @@
             return;
         }
 
-        Constructor<?> constructor = null;
-        for (Constructor<?> c : klass.getConstructors()) {
-            if (c.getParameterTypes().length == 0) {
-                constructor = c;
-                break;
+        Constructor<?> constructor;
+        Object o = null;
+        if (providerArg == null) {
+            try {
+                constructor = klass.getConstructor();
+                o = constructor.newInstance();
+            } catch (ReflectiveOperationException e) {
+                e.printStackTrace();
+                System.err.println("Unable to instantiate " + providerClassName
+                        + " with a zero-arg constructor");
+                System.exit(1);
+            }
+        } else {
+            try {
+                constructor = klass.getConstructor(String.class);
+                o = constructor.newInstance(providerArg);
+            } catch (ReflectiveOperationException e) {
+                // This is expected from JDK 9+; the single-arg constructor accepting the
+                // configuration has been replaced with a configure(String) method to be invoked
+                // after instantiating the Provider with the zero-arg constructor.
+                try {
+                    constructor = klass.getConstructor();
+                    o = constructor.newInstance();
+                    // The configure method will return either the modified Provider or a new
+                    // Provider if this one cannot be configured in-place.
+                    o = klass.getMethod("configure", String.class).invoke(o, providerArg);
+                } catch (ReflectiveOperationException roe) {
+                    roe.printStackTrace();
+                    System.err.println("Unable to instantiate " + providerClassName
+                            + " with the provided argument " + providerArg);
+                    System.exit(1);
+                }
             }
         }
-        if (constructor == null) {
-            System.err.println("No zero-arg constructor found for " + providerClassName);
-            System.exit(1);
-            return;
-        }
 
-        final Object o;
-        try {
-            o = constructor.newInstance();
-        } catch (Exception e) {
-            e.printStackTrace();
-            System.exit(1);
-            return;
-        }
         if (!(o instanceof Provider)) {
             System.err.println("Not a Provider class: " + providerClassName);
             System.exit(1);
@@ -1049,6 +1063,7 @@
                            "[-a <alignment>] " +
                            "[--align-file-size] " +
                            "[-providerClass <className>] " +
+                           "[-providerArg <configureArg>] " +
                            "[-loadPrivateKeysFromKeyStore <keyStoreName>]" +
                            "[-keyStorePin <pin>]" +
                            "[--min-sdk-version <n>] " +
@@ -1073,6 +1088,7 @@
 
         boolean signWholeFile = false;
         String providerClass = null;
+        String providerArg = null;
         String keyStoreName = null;
         String keyStorePin = null;
         int alignment = 4;
@@ -1093,6 +1109,12 @@
                 }
                 providerClass = args[++argstart];
                 ++argstart;
+            } else if("-providerArg".equals(args[argstart])) {
+                if (argstart + 1 >= args.length) {
+                    usage();
+                }
+                providerArg = args[++argstart];
+                ++argstart;
             } else if ("-loadPrivateKeysFromKeyStore".equals(args[argstart])) {
                 if (argstart + 1 >= args.length) {
                     usage();
@@ -1153,7 +1175,7 @@
             System.exit(2);
         }
 
-        loadProviderIfNecessary(providerClass);
+        loadProviderIfNecessary(providerClass, providerArg);
 
         String inputFilename = args[numArgsExcludeV4FilePath - 2];
         String outputFilename = args[numArgsExcludeV4FilePath - 1];