Merge "Install new snapuserd_ramdisk stem"
diff --git a/core/Makefile b/core/Makefile
index 94b4803..78382bd 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -694,8 +694,8 @@
 	@rm -f $@
 	echo "# Modules using -Wno-error" >> $@
 	for m in $(sort $(SOONG_MODULES_USING_WNO_ERROR) $(MODULES_USING_WNO_ERROR)); do echo $$m >> $@; done
-	echo "# Modules added default -Wall" >> $@
-	for m in $(sort $(SOONG_MODULES_ADDED_WALL) $(MODULES_ADDED_WALL)); do echo $$m >> $@; done
+	echo "# Modules that allow warnings" >> $@
+	for m in $(sort $(SOONG_MODULES_WARNINGS_ALLOWED) $(MODULES_WARNINGS_ALLOWED)); do echo $$m >> $@; done
 
 $(call declare-0p-target,$(WALL_WERROR))
 
@@ -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)
@@ -1831,6 +1824,7 @@
 define add-common-ro-flags-to-image-props
 $(eval _var := $(call to-upper,$(1)))
 $(if $(BOARD_$(_var)IMAGE_EROFS_COMPRESSOR),$(hide) echo "$(1)_erofs_compressor=$(BOARD_$(_var)IMAGE_EROFS_COMPRESSOR)" >> $(2))
+$(if $(BOARD_$(_var)IMAGE_EROFS_COMPRESS_HINTS),$(hide) echo "$(1)_erofs_compress_hints=$(BOARD_$(_var)IMAGE_EROFS_COMPRESS_HINTS)" >> $(2))
 $(if $(BOARD_$(_var)IMAGE_EROFS_PCLUSTER_SIZE),$(hide) echo "$(1)_erofs_pcluster_size=$(BOARD_$(_var)IMAGE_EROFS_PCLUSTER_SIZE)" >> $(2))
 $(if $(BOARD_$(_var)IMAGE_EXTFS_INODE_COUNT),$(hide) echo "$(1)_extfs_inode_count=$(BOARD_$(_var)IMAGE_EXTFS_INODE_COUNT)" >> $(2))
 $(if $(BOARD_$(_var)IMAGE_EXTFS_RSV_PCT),$(hide) echo "$(1)_extfs_rsv_pct=$(BOARD_$(_var)IMAGE_EXTFS_RSV_PCT)" >> $(2))
@@ -1916,6 +1910,7 @@
 $(if $(INTERNAL_USERIMAGES_SPARSE_SQUASHFS_FLAG),$(hide) echo "squashfs_sparse_flag=$(INTERNAL_USERIMAGES_SPARSE_SQUASHFS_FLAG)" >> $(1))
 $(if $(INTERNAL_USERIMAGES_SPARSE_F2FS_FLAG),$(hide) echo "f2fs_sparse_flag=$(INTERNAL_USERIMAGES_SPARSE_F2FS_FLAG)" >> $(1))
 $(if $(BOARD_EROFS_COMPRESSOR),$(hide) echo "erofs_default_compressor=$(BOARD_EROFS_COMPRESSOR)" >> $(1))
+$(if $(BOARD_EROFS_COMPRESS_HINTS),$(hide) echo "erofs_default_compress_hints=$(BOARD_EROFS_COMPRESS_HINTS)" >> $(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))
@@ -3108,10 +3103,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
@@ -5974,6 +5965,8 @@
 	$(hide) mkdir -p $(zip_root)/PREBUILT_IMAGES
 	$(hide) cp $(INSTALLED_PVMFWIMAGE_TARGET) $(zip_root)/PREBUILT_IMAGES/
 	$(hide) cp $(INSTALLED_PVMFW_EMBEDDED_AVBKEY_TARGET) $(zip_root)/PREBUILT_IMAGES/
+	$(hide) mkdir -p $(zip_root)/PVMFW
+	$(hide) cp $(PREBUILT_PVMFWIMAGE_TARGET) $(zip_root)/PVMFW/
 endif
 ifdef BOARD_PREBUILT_BOOTLOADER
 	$(hide) mkdir -p $(zip_root)/IMAGES
@@ -6798,8 +6791,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
@@ -6828,18 +6819,7 @@
 deps := \
 	$(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 \
diff --git a/core/OWNERS b/core/OWNERS
index dae34ff..980186c 100644
--- a/core/OWNERS
+++ b/core/OWNERS
@@ -1,4 +1,4 @@
-per-file dex_preopt*.* = ngeoffray@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
diff --git a/core/binary.mk b/core/binary.mk
index 665270e..3f32fa9 100644
--- a/core/binary.mk
+++ b/core/binary.mk
@@ -1506,7 +1506,7 @@
         ifeq (,$(strip $(call find_warning_allowed_projects,$(LOCAL_PATH))))
           my_cflags := -Wall -Werror $(my_cflags)
         else
-          $(eval MODULES_ADDED_WALL := $(MODULES_ADDED_WALL) $(LOCAL_MODULE_MAKEFILE):$(LOCAL_MODULE))
+          $(eval MODULES_WARNINGS_ALLOWED := $(MODULES_USING_WNO_ERROR) $(LOCAL_MODULE_MAKEFILE):$(LOCAL_MODULE))
           my_cflags := -Wall $(my_cflags)
         endif
       endif
diff --git a/core/config.mk b/core/config.mk
index 4db33f1..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
 
diff --git a/core/definitions.mk b/core/definitions.mk
index 8fe5edb..e424bc2 100644
--- a/core/definitions.mk
+++ b/core/definitions.mk
@@ -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/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/main.mk b/core/main.mk
index 171a761..78f38f3 100644
--- a/core/main.mk
+++ b/core/main.mk
@@ -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_config.mk b/core/product_config.mk
index 35f018d..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))
 
@@ -273,8 +272,6 @@
 ############################################################################
 
 current_product_makefile :=
-all_product_makefiles :=
-all_product_configs :=
 
 #############################################################################
 # Quick check and assign default values
diff --git a/core/proguard.flags b/core/proguard.flags
index 185275e..aee5271 100644
--- a/core/proguard.flags
+++ b/core/proguard.flags
@@ -15,6 +15,12 @@
 @**.VisibleForTesting *;
 }
 
+# Keep rule for members that are needed solely to keep alive downstream weak
+# references, and could otherwise be removed after tree shaking optimizations.
+-keepclassmembers,allowaccessmodification,allowobfuscation,allowshrinking class * {
+  @com.android.internal.annotations.KeepForWeakReference <fields>;
+}
+
 # Understand the common @Keep annotation from various Android packages:
 #  * android.support.annotation
 #  * androidx.annotation
diff --git a/core/tasks/README.dex_preopt_check.md b/core/tasks/README.dex_preopt_check.md
new file mode 100644
index 0000000..b0baa9e
--- /dev/null
+++ b/core/tasks/README.dex_preopt_check.md
@@ -0,0 +1,43 @@
+# `dex_preopt_check`
+
+`dex_preopt_check` is a build-time check to make sure that all system server
+jars are dexpreopted. When the check fails, you will see the following error
+message:
+
+```
+FAILED:
+build/make/core/tasks/dex_preopt_check.mk:13: warning:  Missing compilation artifacts. Dexpreopting is not working for some system server jars
+Offending entries:
+```
+
+Possible causes are:
+
+1.  There is an APEX/SDK mismatch. (E.g., the APEX is built from source while
+    the SDK is built from prebuilt.)
+
+1.  The `systemserverclasspath_fragment` is not added as
+    `systemserverclasspath_fragments` of the corresponding `apex` module, or not
+    added as `exported_systemserverclasspath_fragments` of the corresponding
+    `prebuilt_apex`/`apex_set` module when building from prebuilt.
+
+1.  The expected version of the system server java library is not preferred.
+    (E.g., the `java_import` module has `prefer: false` when building from
+    prebuilt.)
+
+1.  Dexpreopting is disabled for the system server java library. This can be due
+    to various reasons including but not limited to:
+
+    - The java library has `dex_preopt: { enabled: false }` in the Android.bp
+      file.
+
+    - The java library is listed in `DEXPREOPT_DISABLED_MODULES` in a Makefile.
+
+    - The java library is missing `installable: true` in the Android.bp
+      file when building from source.
+
+    - Sanitizer is enabled.
+
+1.  `PRODUCT_SYSTEM_SERVER_JARS`, `PRODUCT_APEX_SYSTEM_SERVER_JARS`,
+    `PRODUCT_STANDALONE_SYSTEM_SERVER_JARS`, or
+    `PRODUCT_APEX_STANDALONE_SYSTEM_SERVER_JARS` has an extra entry that is not
+    needed by the product.
diff --git a/core/tasks/dex_preopt_check.mk b/core/tasks/dex_preopt_check.mk
index bfa1ec5..5fd60c8 100644
--- a/core/tasks/dex_preopt_check.mk
+++ b/core/tasks/dex_preopt_check.mk
@@ -12,7 +12,8 @@
   ifneq (,$(filter services,$(PRODUCT_PACKAGES)))
     $(call maybe-print-list-and-error,\
       $(filter-out $(ALL_DEFAULT_INSTALLED_MODULES),$(DEXPREOPT_SYSTEMSERVER_ARTIFACTS)),\
-      Missing compilation artifacts. Dexpreopting is not working for some system server jars \
+      Missing compilation artifacts. Dexpreopting is not working for some system server jars. See \
+      https://cs.android.com/android/platform/superproject/+/master:build/make/core/tasks/README.dex_preopt_check.md \
     )
   endif
 endif
diff --git a/core/tasks/host-unit-tests.mk b/core/tasks/host-unit-tests.mk
index 4453c29..ed2f2a6 100644
--- a/core/tasks/host-unit-tests.mk
+++ b/core/tasks/host-unit-tests.mk
@@ -39,7 +39,7 @@
 	  echo $$shared_lib >> $@-host-libs.list; \
 	done
 	grep $(TARGET_OUT_TESTCASES) $@.list > $@-target.list || true
-	$(hide) $(SOONG_ZIP) -L 0 -d -o $@ -P host -C $(HOST_OUT) -l $@-host.list \
+	$(hide) $(SOONG_ZIP) -d -o $@ -P host -C $(HOST_OUT) -l $@-host.list \
 	  -P target -C $(PRODUCT_OUT) -l $@-target.list \
 	  -P host/testcases -C $(HOST_OUT) -l $@-host-libs.list
 	rm -f $@.list $@-host.list $@-target.list $@-host-libs.list
diff --git a/envsetup.sh b/envsetup.sh
index b49bb8a..f141ff5 100644
--- a/envsetup.sh
+++ b/envsetup.sh
@@ -403,7 +403,9 @@
     # e.g.
     # ENVSETUP_NO_COMPLETION=adb # -> disable adb completion
     # ENVSETUP_NO_COMPLETION=adb:bit # -> disable adb and bit completion
+    local T=$(gettop)
     for f in ${completion_files[*]}; do
+        f="$T/$f"
         if [ ! -f "$f" ]; then
           echo "Warning: completion file $f not found"
         elif should_add_completion "$f"; then
@@ -456,7 +458,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
@@ -467,7 +469,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
@@ -944,6 +946,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)
@@ -1826,6 +1856,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
index 8587b3a..c942eb2 100755
--- a/finalize_branch_for_release.sh
+++ b/finalize_branch_for_release.sh
@@ -17,8 +17,7 @@
 # Update references in the codebase to new API version (TODO)
 # ...
 
-# Adding -j1 option because of file(Android.bp) race condition.
-AIDL_TRANSITIVE_FREEZE=true m aidl-freeze-api -j1
+AIDL_TRANSITIVE_FREEZE=true m aidl-freeze-api
 
 m check-vndk-list || update-vndk-list.sh # for new versions of AIDL interfaces
 
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..bd1c440
--- /dev/null
+++ b/orchestrator/core/api_assembly.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 collections
+import json
+import os
+import sys
+
+import api_assembly_cc
+import ninja_tools
+
+
+ContributionData = collections.namedtuple("ContributionData", ("inner_tree", "json_data"))
+
+def assemble_apis(context, 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:
+            json_data = load_contribution_file(filename)
+            if not json_data:
+                continue
+            # TODO: Validate the configs, especially that the domains match what we asked for
+            # from the lunch config.
+            contributions.append(ContributionData(inner_trees.get(tree_key), json_data))
+
+    # Group contributions by language and API surface
+    stub_libraries = collate_contributions(contributions)
+
+    # Initialize the ninja file writer
+    with open(context.out.api_ninja_file(), "w") as ninja_file:
+        ninja = ninja_tools.Ninja(context, ninja_file)
+
+        # Initialize the build file writer
+        build_file = BuildFile() # TODO: parameters?
+
+        # 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?
+        for stub_library in stub_libraries:
+            STUB_LANGUAGE_HANDLERS[stub_library.language](context, ninja, build_file, stub_library)
+
+        # TODO: Handle host_executables separately or as a StubLibrary language?
+
+        # Finish writing the ninja file
+        ninja.write()
+
+
+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, inner_tree, api_domain, library_contribution):
+        self.inner_tree = inner_tree
+        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.json_data.get(language, []):
+                key = (language, contribution.json_data["name"],
+                        contribution.json_data["version"], library["name"])
+                stub_library = grouped.get(key)
+                if not stub_library:
+                    stub_library = StubLibrary(language, contribution.json_data["name"],
+                            contribution.json_data["version"], library["name"])
+                    grouped[key] = stub_library
+                stub_library.add_contribution(StubLibraryContribution(contribution.inner_tree,
+                        contribution.json_data["api_domain"], library))
+    return list(grouped.values())
+
+
+def assemble_java_api_library(context, ninja, 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(context, ninja, 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": api_assembly_cc.assemble_cc_api_library,
+    "java_libraries": assemble_java_api_library,
+    "resource_libraries": assemble_resource_api_library,
+}
+
+
+class BuildFile(object):
+    "Abstract generator for Android.bp files and BUILD files."
+    pass
+
+
diff --git a/orchestrator/core/api_assembly_cc.py b/orchestrator/core/api_assembly_cc.py
new file mode 100644
index 0000000..15bc98a
--- /dev/null
+++ b/orchestrator/core/api_assembly_cc.py
@@ -0,0 +1,55 @@
+#!/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
+
+def assemble_cc_api_library(context, ninja, build_file, stub_library):
+    print("\nassembling 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))
+
+    staging_dir = context.out.api_library_dir(stub_library.api_surface,
+            stub_library.api_surface_version, stub_library.name)
+    work_dir = context.out.api_library_work_dir(stub_library.api_surface,
+            stub_library.api_surface_version, stub_library.name)
+    print("staging_dir=%s" % (staging_dir))
+    print("work_dir=%s" % (work_dir))
+
+    # Generate rules to copy headers
+    includes = []
+    include_dir = os.path.join(staging_dir, "include")
+    for contrib in stub_library.contributions:
+        for headers in contrib.library_contribution["headers"]:
+            root = headers["root"]
+            for file in headers["files"]:
+                # TODO: Deal with collisions of the same name from multiple contributions
+                include = os.path.join(include_dir, file)
+                ninja.add_copy_file(include, os.path.join(contrib.inner_tree.root, root, file))
+                includes.append(include)
+
+    # Generate rule to run ndkstubgen
+
+
+    # Generate rule to compile stubs to library
+
+    # Generate phony rule to build the library
+    # TODO: This name probably conflictgs with something
+    ninja.add_phony("-".join((stub_library.api_surface, str(stub_library.api_surface_version),
+            stub_library.name)), includes)
+
+    # Generate build files
+
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/final_packaging.py b/orchestrator/core/final_packaging.py
new file mode 100644
index 0000000..693a716
--- /dev/null
+++ b/orchestrator/core/final_packaging.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 ninja_tools
+import ninja_syntax # Has to be after ninja_tools because of the path hack
+
+def final_packaging(context):
+    """Pull together all of the previously defined rules into the final build stems."""
+
+    with open(context.out.outer_ninja_file(), "w") as ninja_file:
+        ninja = ninja_tools.Ninja(context, ninja_file)
+
+        # Add the api surfaces file
+        ninja.add_subninja(ninja_syntax.Subninja(context.out.api_ninja_file(), chDir=None))
+
+        # Finish writing the ninja file
+        ninja.write()
diff --git a/orchestrator/core/inner_tree.py b/orchestrator/core/inner_tree.py
new file mode 100644
index 0000000..4383dd8
--- /dev/null
+++ b/orchestrator/core/inner_tree.py
@@ -0,0 +1,162 @@
+#!/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, context, 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(context.out.inner_tree_dir(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
+
+
+    def get(self, tree_key):
+        """Get an inner tree for tree_key"""
+        return self.trees.get(tree_key)
+
+class OutDirLayout(object):
+    """Encapsulates the logic about the layout of the inner tree out directories.
+    See also context.OutDir for outer tree out dir contents."""
+
+    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/ninja_runner.py b/orchestrator/core/ninja_runner.py
new file mode 100644
index 0000000..906f1ae
--- /dev/null
+++ b/orchestrator/core/ninja_runner.py
@@ -0,0 +1,36 @@
+#
+# 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 subprocess
+
+def run_ninja(context, targets):
+    """Run ninja.
+    """
+
+    # Construct the command
+    cmd = [
+            context.tools.ninja(),
+            "-f",
+            context.out.outer_ninja_file(),
+        ] + targets
+
+    # 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 outer tree.\nstopping multitree build.\n")
+        sys.exit(1)
+
diff --git a/orchestrator/core/ninja_tools.py b/orchestrator/core/ninja_tools.py
new file mode 100644
index 0000000..c676907
--- /dev/null
+++ b/orchestrator/core/ninja_tools.py
@@ -0,0 +1,46 @@
+# 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
+
+# Workaround for python include path
+_ninja_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "ninja"))
+if _ninja_dir not in sys.path:
+    sys.path.append(_ninja_dir)
+import ninja_writer
+from ninja_syntax import Variable, BuildAction, Rule, Pool, Subninja, Line
+
+
+class Ninja(ninja_writer.Writer):
+    """Some higher level constructs on top of raw ninja writing.
+    TODO: Not sure where these should be."""
+    def __init__(self, context, file):
+        super(Ninja, self).__init__(file)
+        self._context = context
+        self._did_copy_file = False
+
+    def add_copy_file(self, copy_to, copy_from):
+        if not self._did_copy_file:
+            self._did_copy_file = True
+            rule = Rule("copy_file")
+            rule.add_variable("command", "mkdir -p ${out_dir} && " + self._context.tools.acp()
+                    + " -f ${in} ${out}")
+            self.add_rule(rule)
+        build_action = BuildAction(copy_to, "copy_file", inputs=[copy_from,],
+                implicits=[self._context.tools.acp()])
+        build_action.add_variable("out_dir", os.path.dirname(copy_to))
+        self.add_build_action(build_action)
+
+
diff --git a/orchestrator/core/orchestrator.py b/orchestrator/core/orchestrator.py
new file mode 100755
index 0000000..bb0885d
--- /dev/null
+++ b/orchestrator/core/orchestrator.py
@@ -0,0 +1,135 @@
+#!/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 final_packaging
+import inner_tree
+import interrogate
+import lunch
+import ninja_runner
+import utils
+
+EXIT_STATUS_OK = 0
+EXIT_STATUS_ERROR = 1
+
+API_DOMAIN_SYSTEM = "system"
+API_DOMAIN_VENDOR = "vendor"
+API_DOMAIN_MODULE = "module"
+
+def process_config(context, 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(context, 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
+    #
+
+    # Choose the out directory, set up error handling, etc.
+    context = utils.Context(utils.choose_out_dir(), utils.Errors(sys.stderr))
+
+    # 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(context, 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(context, inner_trees)
+
+    #
+    # 3a. API Domain Analysis
+    #
+
+    #
+    # 3b. Final Packaging Rules
+    #
+    final_packaging.final_packaging(context)
+
+    #
+    # 4. Build Execution
+    #
+    # TODO: Decide what we want the UX for selecting targets to be across
+    # branches... since there are very likely to be conflicting soong short
+    # names.
+    print("Running ninja...")
+    targets = ["public_api-1-libhwui", "public_api-1-libc"]
+    ninja_runner.run_ninja(context, targets)
+
+    #
+    # 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/core/utils.py b/orchestrator/core/utils.py
new file mode 100644
index 0000000..bb7f8ad
--- /dev/null
+++ b/orchestrator/core/utils.py
@@ -0,0 +1,120 @@
+#!/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 platform
+
+class Context(object):
+    """Mockable container for global state."""
+    def __init__(self, out_root, errors):
+        self.out = OutDir(out_root)
+        self.errors = errors
+        self.tools = HostTools()
+
+class TestContext(Context):
+    "Context for testing. The real Context is manually constructed in orchestrator.py."
+
+    def __init__(self, test_work_dir, test_name):
+        super(MockContext, self).__init__(os.path.join(test_work_dir, test_name),
+                Errors(None))
+
+
+class OutDir(object):
+    """Encapsulates the logic about the out directory at the outer-tree level.
+    See also inner_tree.OutDirLayout for inner tree out dir contents."""
+
+    def __init__(self, root):
+        "Initialize with the root of the OUT_DIR for the outer tree."
+        self._root = root
+        self._intermediates = "intermediates"
+
+    def root(self):
+        return self._root
+
+    def inner_tree_dir(self, tree_root):
+        """Root directory for inner tree inside the out dir."""
+        return os.path.join(self._root, "trees", tree_root)
+
+    def api_ninja_file(self):
+        """The ninja file that assembles API surfaces."""
+        return os.path.join(self._root, "api_surfaces.ninja")
+
+    def api_library_dir(self, surface, version, library):
+        """Directory for all the contents of a library inside an API surface, including
+        the build files.  Any intermediates should go in api_library_work_dir."""
+        return os.path.join(self._root, "api_surfaces", surface, str(version), library)
+
+    def api_library_work_dir(self, surface, version, library):
+        """Intermediates / scratch directory for library inside an API surface."""
+        return os.path.join(self._root, self._intermediates, "api_surfaces", surface, str(version),
+                library)
+
+    def outer_ninja_file(self):
+        return os.path.join(self._root, "multitree.ninja")
+
+
+class Errors(object):
+    """Class for reporting and tracking errors."""
+    def __init__(self, stream):
+        """Initialize Error reporter with a file-like object."""
+        self._stream = stream
+        self._all = []
+
+    def error(self, message):
+        """Record the error message."""
+        s = str(s)
+        if s[-1] != "\n":
+            s += "\n"
+        self._all.append(s)
+        if self._stream:
+            self._stream.write(s)
+
+    def had_error(self):
+        """Return if there were any errors reported."""
+        return len(self._all)
+
+    def get_errors(self):
+        """Get all errors that were reported."""
+        return self._all
+
+
+class HostTools(object):
+    def __init__(self):
+        if platform.system() == "Linux":
+            self._arch = "linux-x86"
+        else:
+            raise Exception("Orchestrator running on an unknown system: %s" % platform.system())
+
+        # Some of these are called a lot, so pre-compute the strings to save memory
+        self._prebuilts = os.path.join("build", "prebuilts", "build-tools", self._arch, "bin")
+        self._acp = os.path.join(self._prebuilts, "acp")
+        self._ninja = os.path.join(self._prebuilts, "ninja")
+
+    def acp(self):
+        return self._acp
+
+    def ninja(self):
+        return self._ninja
+
+
+def choose_out_dir():
+    """Get the root of the out dir, either from the environment or by picking
+    a default."""
+    result = os.environ.get("OUT_DIR")
+    if result:
+        return result
+    else:
+        return "out"
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..df97b68
--- /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(self) -> 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..9e80b4b
--- /dev/null
+++ b/orchestrator/ninja/ninja_writer.py
@@ -0,0 +1,59 @@
+#!/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 add_phony(self, name, deps):
+    build_action = BuildAction(name, "phony", inputs=deps)
+    self.add_build_action(build_action)
+
+  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/product/base_system.mk b/target/product/base_system.mk
index 05ddfe5..2c2b5a9 100644
--- a/target/product/base_system.mk
+++ b/target/product/base_system.mk
@@ -24,7 +24,7 @@
     android.hidl.manager-V1.0-java \
     android.hidl.memory@1.0-impl \
     android.hidl.memory@1.0-impl.vendor \
-    android.system.suspend@1.0-service \
+    android.system.suspend-service \
     android.test.base \
     android.test.mock \
     android.test.runner \
diff --git a/target/product/default_art_config.mk b/target/product/default_art_config.mk
index 851a2cb..5695803 100644
--- a/target/product/default_art_config.mk
+++ b/target/product/default_art_config.mk
@@ -76,7 +76,11 @@
     com.android.media:service-media-s \
     com.android.permission:service-permission \
 
-PRODUCT_DEX_PREOPT_BOOT_IMAGE_PROFILE_LOCATION += art/build/boot/boot-image-profile.txt
+# Use $(wildcard) to avoid referencing the profile in thin manifests that don't have the
+# art project.
+ifneq (,$(wildcard art))
+  PRODUCT_DEX_PREOPT_BOOT_IMAGE_PROFILE_LOCATION += art/build/boot/boot-image-profile.txt
+endif
 
 # List of jars on the platform that system_server loads dynamically using separate classloaders.
 # Keep the list sorted library names.
diff --git a/target/product/sdk.mk b/target/product/sdk.mk
index 96d8cc9..fa7e1ad 100644
--- a/target/product/sdk.mk
+++ b/target/product/sdk.mk
@@ -14,8 +14,11 @@
 # limitations under the License.
 #
 
-# Don't modify this file - It's just an alias!
+# This is a simple product that uses configures the minimum amount
+# needed to build the SDK (without the emulator).
 
-$(call inherit-product, $(SRC_TARGET_DIR)/product/sdk_phone_armv7.mk)
+$(call inherit-product, $(SRC_TARGET_DIR)/product/languages_default.mk)
 
 PRODUCT_NAME := sdk
+PRODUCT_BRAND := Android
+PRODUCT_DEVICE := mainline_x86
diff --git a/tools/releasetools/build_image.py b/tools/releasetools/build_image.py
index 7fdf4ba..9567fdc 100755
--- a/tools/releasetools/build_image.py
+++ b/tools/releasetools/build_image.py
@@ -331,6 +331,14 @@
     if compressor:
       build_command.extend(["-z", compressor])
 
+    compress_hints = None
+    if "erofs_default_compress_hints" in prop_dict:
+      compress_hints = prop_dict["erofs_default_compress_hints"]
+    if "erofs_compress_hints" in prop_dict:
+      compress_hints = prop_dict["erofs_compress_hints"]
+    if compress_hints:
+      build_command.extend(["--compress-hints", compress_hints])
+
     build_command.extend(["--mount-point", prop_dict["mount_point"]])
     if target_out:
       build_command.extend(["--product-out", target_out])
@@ -652,6 +660,7 @@
   common_props = (
       "extfs_sparse_flag",
       "erofs_default_compressor",
+      "erofs_default_compress_hints",
       "erofs_pcluster_size",
       "erofs_share_dup_blocks",
       "erofs_sparse_flag",
@@ -706,6 +715,7 @@
       (True, "{}_base_fs_file", "base_fs_file"),
       (True, "{}_disable_sparse", "disable_sparse"),
       (True, "{}_erofs_compressor", "erofs_compressor"),
+      (True, "{}_erofs_compress_hints", "erofs_compress_hints"),
       (True, "{}_erofs_pcluster_size", "erofs_pcluster_size"),
       (True, "{}_erofs_share_dup_blocks", "erofs_share_dup_blocks"),
       (True, "{}_extfs_inode_count", "extfs_inode_count"),
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];
diff --git a/tools/warn/html_writer.py b/tools/warn/html_writer.py
index 3fa822a..46ba253 100644
--- a/tools/warn/html_writer.py
+++ b/tools/warn/html_writer.py
@@ -56,6 +56,7 @@
 
 from __future__ import print_function
 import csv
+import datetime
 import html
 import sys
 
@@ -258,7 +259,7 @@
 
 
 def dump_stats(writer, warn_patterns):
-  """Dump some stats about total number of warnings and such."""
+  """Dump some stats about total number of warnings and date."""
 
   known = 0
   skipped = 0
@@ -279,6 +280,8 @@
   if total < 1000:
     extra_msg = ' (low count may indicate incremental build)'
   writer('Total number of warnings: <b>' + str(total) + '</b>' + extra_msg)
+  date_time_str = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
+  writer('<p>(generated on ' + date_time_str + ')')
 
 
 # New base table of warnings, [severity, warn_id, project, warning_message]
@@ -662,15 +665,26 @@
   var warningsOfFiles = {};
   var warningsOfDirs = {};
   var subDirs = {};
-  function addOneWarning(map, key) {
-    map[key] = 1 + ((key in map) ? map[key] : 0);
+  function addOneWarning(map, key, type, unique) {
+    function increaseCounter(idx) {
+      map[idx] = 1 + ((idx in map) ? map[idx] : 0);
+    }
+    increaseCounter(key)
+    if (type != "") {
+      increaseCounter(type + " " + key)
+      if (unique) {
+        increaseCounter(type + " *")
+      }
+    }
   }
   for (var i = 0; i < numWarnings; i++) {
-    var file = WarningMessages[i].replace(/:.*/, "");
-    addOneWarning(warningsOfFiles, file);
+    var message = WarningMessages[i]
+    var file = message.replace(/:.*/, "");
+    var warningType = message.endsWith("]") ? message.replace(/.*\[/, "[") : "";
+    addOneWarning(warningsOfFiles, file, warningType, true);
     var dirs = file.split("/");
     var dir = dirs[0];
-    addOneWarning(warningsOfDirs, dir);
+    addOneWarning(warningsOfDirs, dir, warningType, true);
     for (var d = 1; d < dirs.length - 1; d++) {
       var subDir = dir + "/" + dirs[d];
       if (!(dir in subDirs)) {
@@ -678,7 +692,7 @@
       }
       subDirs[dir][subDir] = 1;
       dir = subDir;
-      addOneWarning(warningsOfDirs, dir);
+      addOneWarning(warningsOfDirs, dir, warningType, false);
     }
   }
   var minDirWarnings = numWarnings*(LimitPercentWarnings/100);
@@ -725,27 +739,33 @@
         document.getElementById(divName));
     table.draw(view, {allowHtml: true, alternatingRowStyle: true});
   }
-  addTable("Directory", "top_dirs_table", TopDirs, "selectDir");
-  addTable("File", "top_files_table", TopFiles, "selectFile");
+  addTable("[Warning Type] Directory", "top_dirs_table", TopDirs, "selectDir");
+  addTable("[Warning Type] File", "top_files_table", TopFiles, "selectFile");
 }
 function selectDirFile(idx, rows, dirFile) {
   if (rows.length <= idx) {
     return;
   }
   var name = rows[idx][2];
+  var type = "";
+  if (name.startsWith("[")) {
+    type = " " + name.replace(/ .*/, "");
+    name = name.replace(/.* /, "");
+  }
   var spanName = "selected_" + dirFile + "_name";
-  document.getElementById(spanName).innerHTML = name;
+  document.getElementById(spanName).innerHTML = name + type;
   var divName = "selected_" + dirFile + "_warnings";
   var numWarnings = rows[idx][1].v;
   var prefix = name.replace(/\\.\\.\\.$/, "");
   var data = new google.visualization.DataTable();
-  data.addColumn('string', numWarnings + ' warnings in ' + name);
+  data.addColumn('string', numWarnings + type + ' warnings in ' + name);
   var getWarningMessage = (FlagPlatform == "chrome")
         ? ((x) => addURLToLine(WarningMessages[Warnings[x][2]],
                                WarningLinks[Warnings[x][3]]))
         : ((x) => addURL(WarningMessages[Warnings[x][2]]));
   for (var i = 0; i < Warnings.length; i++) {
-    if (WarningMessages[Warnings[i][2]].startsWith(prefix)) {
+    if ((prefix.startsWith("*") || WarningMessages[Warnings[i][2]].startsWith(prefix)) &&
+        (type == "" || WarningMessages[Warnings[i][2]].endsWith(type))) {
       data.addRow([getWarningMessage(i)]);
     }
   }
@@ -827,14 +847,14 @@
   def section2():
     dump_dir_file_section(
         writer, 'directory', 'top_dirs_table',
-        'Directories with at least ' +
-        str(LIMIT_PERCENT_WARNINGS) + '% warnings')
+        'Directories/Warnings with at least ' +
+        str(LIMIT_PERCENT_WARNINGS) + '% of all cases')
   def section3():
     dump_dir_file_section(
         writer, 'file', 'top_files_table',
-        'Files with at least ' +
-        str(LIMIT_PERCENT_WARNINGS) + '% or ' +
-        str(LIMIT_WARNINGS_PER_FILE) + ' warnings')
+        'Files/Warnings with at least ' +
+        str(LIMIT_PERCENT_WARNINGS) + '% of all or ' +
+        str(LIMIT_WARNINGS_PER_FILE) + ' cases')
   def section4():
     writer('<script>')
     emit_js_data(writer, flags, warning_messages, warning_links,
diff --git a/tools/warn/warn_common.py b/tools/warn/warn_common.py
index 61c8676..aa68313 100755
--- a/tools/warn/warn_common.py
+++ b/tools/warn/warn_common.py
@@ -64,6 +64,10 @@
 from . import tidy_warn_patterns as tidy_patterns
 
 
+# Location of this file is used to guess the root of Android source tree.
+THIS_FILE_PATH = 'build/make/tools/warn/warn_common.py'
+
+
 def parse_args(use_google3):
   """Define and parse the args. Return the parse_args() result."""
   parser = argparse.ArgumentParser(
@@ -217,17 +221,27 @@
   return link
 
 
-def find_warn_py_and_android_root(path):
-  """Return android source root path if warn.py is found."""
+def find_this_file_and_android_root(path):
+  """Return android source root path if this file is found."""
   parts = path.split('/')
   for idx in reversed(range(2, len(parts))):
     root_path = '/'.join(parts[:idx])
     # Android root directory should contain this script.
-    if os.path.exists(root_path + '/build/make/tools/warn.py'):
+    if os.path.exists(root_path + '/' + THIS_FILE_PATH):
       return root_path
   return ''
 
 
+def find_android_root_top_dirs(root_dir):
+  """Return a list of directories under the root_dir, if it exists."""
+  # Root directory should contain at least build/make and build/soong.
+  if (not os.path.isdir(root_dir + '/build/make') or
+      not os.path.isdir(root_dir + '/build/soong')):
+    return None
+  return list(filter(lambda d: os.path.isdir(root_dir + '/' + d),
+                     os.listdir(root_dir)))
+
+
 def find_android_root(buildlog):
   """Guess android source root from common prefix of file paths."""
   # Use the longest common prefix of the absolute file paths
@@ -239,8 +253,8 @@
     # We want to find android_root of a local build machine.
     # Do not use RBE warning lines, which has '/b/f/w/' path prefix.
     # Do not use /tmp/ file warnings.
-    if warning_pattern.match(line) and (
-        '/b/f/w' not in line and not line.startswith('/tmp/')):
+    if ('/b/f/w' not in line and not line.startswith('/tmp/') and
+        warning_pattern.match(line)):
       warning_lines.append(line)
       count += 1
       if count > 9999:
@@ -249,17 +263,26 @@
       # the source tree root.
       if count < 100:
         path = os.path.normpath(re.sub(':.*$', '', line))
-        android_root = find_warn_py_and_android_root(path)
+        android_root = find_this_file_and_android_root(path)
         if android_root:
-          return android_root
+          return android_root, find_android_root_top_dirs(android_root)
   # Do not use common prefix of a small number of paths.
+  android_root = ''
   if count > 10:
     # pytype: disable=wrong-arg-types
     root_path = os.path.commonprefix(warning_lines)
     # pytype: enable=wrong-arg-types
     if len(root_path) > 2 and root_path[len(root_path) - 1] == '/':
-      return root_path[:-1]
-  return ''
+      android_root = root_path[:-1]
+  if android_root and os.path.isdir(android_root):
+    return android_root, find_android_root_top_dirs(android_root)
+  # When the build.log file is moved to a different machine where
+  # android_root is not found, use the location of this script
+  # to find the android source tree sub directories.
+  if __file__.endswith('/' + THIS_FILE_PATH):
+    script_root = __file__.replace('/' + THIS_FILE_PATH, '')
+    return android_root, find_android_root_top_dirs(script_root)
+  return android_root, None
 
 
 def remove_android_root_prefix(path, android_root):
@@ -310,8 +333,6 @@
   warning_pattern = re.compile(chrome_warning_pattern)
 
   # Collect all unique warning lines
-  # Remove the duplicated warnings save ~8% of time when parsing
-  # one typical build log than before
   unique_warnings = dict()
   for line in infile:
     if warning_pattern.match(line):
@@ -353,8 +374,7 @@
   target_product = 'unknown'
   target_variant = 'unknown'
   build_id = 'unknown'
-  use_rbe = False
-  android_root = find_android_root(infile)
+  android_root, root_top_dirs = find_android_root(infile)
   infile.seek(0)
 
   # rustc warning messages have two lines that should be combined:
@@ -367,24 +387,39 @@
   # C/C++ compiler warning messages have line and column numbers:
   #     some/path/file.c:line_number:column_number: warning: description
   warning_pattern = re.compile('(^[^ ]*/[^ ]*: warning: .*)|(^warning: .*)')
-  warning_without_file = re.compile('^warning: .*')
   rustc_file_position = re.compile('^[ ]+--> [^ ]*/[^ ]*:[0-9]+:[0-9]+')
 
-  # If RBE was used, try to reclaim some warning lines mixed with some
-  # leading chars from other concurrent job's stderr output .
+  # If RBE was used, try to reclaim some warning lines (from stdout)
+  # that contain leading characters from stderr.
   # The leading characters can be any character, including digits and spaces.
-  # It's impossible to correctly identify the starting point of the source
-  # file path without the file directory name knowledge.
-  # Here we can only be sure to recover lines containing "/b/f/w/".
-  rbe_warning_pattern = re.compile('.*/b/f/w/[^ ]*: warning: .*')
 
-   # Collect all unique warning lines
-  # Remove the duplicated warnings save ~8% of time when parsing
-  # one typical build log than before
+  # If a warning line's source file path contains the special RBE prefix
+  # /b/f/w/, we can remove all leading chars up to and including the "/b/f/w/".
+  bfw_warning_pattern = re.compile('.*/b/f/w/([^ ]*: warning: .*)')
+
+  # When android_root is known and available, we find its top directories
+  # and remove all leading chars before a top directory name.
+  # We assume that the leading chars from stderr do not contain "/".
+  # For example,
+  #   10external/...
+  #   12 warningsexternal/...
+  #   413 warningexternal/...
+  #   5 warnings generatedexternal/...
+  #   Suppressed 1000 warnings (packages/modules/...
+  if root_top_dirs:
+    extra_warning_pattern = re.compile(
+        '^.[^/]*((' + '|'.join(root_top_dirs) +
+        ')/[^ ]*: warning: .*)')
+  else:
+    extra_warning_pattern = re.compile('^[^/]* ([^ /]*/[^ ]*: warning: .*)')
+
+  # Collect all unique warning lines
   unique_warnings = dict()
+  checked_warning_lines = dict()
   line_counter = 0
   prev_warning = ''
   for line in infile:
+    line_counter += 1
     if prev_warning:
       if rustc_file_position.match(line):
         # must be a rustc warning, combine 2 lines into one warning
@@ -399,14 +434,31 @@
           prev_warning, flags, android_root, unique_warnings)
       prev_warning = ''
 
-    if use_rbe and rbe_warning_pattern.match(line):
-      cleaned_up_line = re.sub('.*/b/f/w/', '', line)
-      unique_warnings = add_normalized_line_to_warnings(
-          cleaned_up_line, flags, android_root, unique_warnings)
+    # re.match is slow, with several warning line patterns and
+    # long input lines like "TIMEOUT: ...".
+    # We save significant time by skipping non-warning lines.
+    # But do not skip the first 100 lines, because we want to
+    # catch build variables.
+    if line_counter > 100 and line.find('warning: ') < 0:
       continue
 
+    # A large clean build output can contain up to 90% of duplicated
+    # "warning:" lines. If we can skip them quickly, we can
+    # speed up this for-loop 3X to 5X.
+    if line in checked_warning_lines:
+      continue
+    checked_warning_lines[line] = True
+
+    # Clean up extra prefix that could be introduced when RBE was used.
+    if '/b/f/w/' in line:
+      result = bfw_warning_pattern.search(line)
+    else:
+      result = extra_warning_pattern.search(line)
+    if result is not None:
+      line = result.group(1)
+
     if warning_pattern.match(line):
-      if warning_without_file.match(line):
+      if line.startswith('warning: '):
         # save this line and combine it with the next line
         prev_warning = line
       else:
@@ -416,7 +468,6 @@
 
     if line_counter < 100:
       # save a little bit of time by only doing this for the first few lines
-      line_counter += 1
       result = re.search('(?<=^PLATFORM_VERSION=).*', line)
       if result is not None:
         platform_version = result.group(0)
@@ -433,13 +484,6 @@
       if result is not None:
         build_id = result.group(0)
         continue
-      result = re.search('(?<=^TOP=).*', line)
-      if result is not None:
-        android_root = result.group(1)
-        continue
-      if re.search('USE_RBE=', line) is not None:
-        use_rbe = True
-        continue
 
   if android_root:
     new_unique_warnings = dict()