Merge "OverridableModuleBase shouldn't embed ModuleBase."
diff --git a/Android.bp b/Android.bp
index 614e71f..fff17ef 100644
--- a/Android.bp
+++ b/Android.bp
@@ -296,6 +296,7 @@
         "java/jdeps_test.go",
         "java/kotlin_test.go",
         "java/plugin_test.go",
+        "java/robolectric_test.go",
         "java/sdk_test.go",
     ],
     pluginFor: ["soong_build"],
diff --git a/OWNERS b/OWNERS
index 85c70df..4ae045d 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,4 +1,5 @@
-per-file * = asmundak@google.com,ccross@android.com,dwillemsen@google.com,jungjw@google.com
+per-file * = asmundak@google.com,ccross@android.com,dwillemsen@google.com,jungjw@google.com,paulduffin@google.com
+
 per-file ndk_*.go, *gen_stub_libs.py = danalbert@google.com
 per-file clang.go,global.go = srhines@google.com, chh@google.com, pirama@google.com, yikong@google.com
 per-file tidy.go = srhines@google.com, chh@google.com
diff --git a/android/arch.go b/android/arch.go
index f4a3c06..46e582c 100644
--- a/android/arch.go
+++ b/android/arch.go
@@ -713,7 +713,7 @@
 // If host is supported for the module, the Host and HostCross OsClasses are selected.  If device is supported
 // for the module, the Device OsClass is selected.
 // Within each selected OsClass, the multilib selection is determined by:
-//    - The compile_multilib property if it set (which may be overriden by target.android.compile_multlib or
+//    - The compile_multilib property if it set (which may be overridden by target.android.compile_multilib or
 //      target.host.compile_multilib).
 //    - The default multilib passed to InitAndroidArchModule if compile_multilib was not set.
 // Valid multilib values include:
diff --git a/android/module.go b/android/module.go
index 03993e5..87e2ca7 100644
--- a/android/module.go
+++ b/android/module.go
@@ -966,10 +966,12 @@
 
 func (m *moduleContext) ninjaError(params BuildParams, err error) (PackageContext, BuildParams) {
 	return pctx, BuildParams{
-		Rule:        ErrorRule,
-		Description: params.Description,
-		Output:      params.Output,
-		Outputs:     params.Outputs,
+		Rule:            ErrorRule,
+		Description:     params.Description,
+		Output:          params.Output,
+		Outputs:         params.Outputs,
+		ImplicitOutput:  params.ImplicitOutput,
+		ImplicitOutputs: params.ImplicitOutputs,
 		Args: map[string]string{
 			"error": err.Error(),
 		},
diff --git a/android/namespace.go b/android/namespace.go
index 78d7f3c..27ec163 100644
--- a/android/namespace.go
+++ b/android/namespace.go
@@ -222,6 +222,11 @@
 }
 
 func (r *NameResolver) getNamespacesToSearchForModule(sourceNamespace *Namespace) (searchOrder []*Namespace) {
+	if sourceNamespace.visibleNamespaces == nil {
+		// When handling dependencies before namespaceMutator, assume they are non-Soong Blueprint modules and give
+		// access to all namespaces.
+		return r.sortedNamespaces.sortedItems()
+	}
 	return sourceNamespace.visibleNamespaces
 }
 
diff --git a/android/namespace_test.go b/android/namespace_test.go
index 51a0af2..20241fe 100644
--- a/android/namespace_test.go
+++ b/android/namespace_test.go
@@ -91,6 +91,28 @@
 	// setupTest will report any errors
 }
 
+func TestDependingOnBlueprintModuleInRootNamespace(t *testing.T) {
+	_ = setupTest(t,
+		map[string]string{
+			".": `
+			blueprint_test_module {
+				name: "a",
+			}
+			`,
+			"dir1": `
+			soong_namespace {
+			}
+			blueprint_test_module {
+				name: "b",
+				deps: ["a"],
+			}
+			`,
+		},
+	)
+
+	// setupTest will report any errors
+}
+
 func TestDependingOnModuleInImportedNamespace(t *testing.T) {
 	ctx := setupTest(t,
 		map[string]string{
@@ -617,6 +639,7 @@
 	ctx.MockFileSystem(bps)
 	ctx.RegisterModuleType("test_module", ModuleFactoryAdaptor(newTestModule))
 	ctx.RegisterModuleType("soong_namespace", ModuleFactoryAdaptor(NamespaceFactory))
+	ctx.RegisterModuleType("blueprint_test_module", newBlueprintTestModule)
 	ctx.PreArchMutators(RegisterNamespaceMutator)
 	ctx.PreDepsMutators(func(ctx RegisterMutatorsContext) {
 		ctx.BottomUp("rename", renameMutator)
@@ -641,6 +664,7 @@
 }
 
 func setupTest(t *testing.T, bps map[string]string) (ctx *TestContext) {
+	t.Helper()
 	ctx, errs := setupTestExpectErrs(bps)
 	FailIfErrored(t, errs)
 	return ctx
@@ -718,3 +742,22 @@
 	InitAndroidModule(m)
 	return m
 }
+
+type blueprintTestModule struct {
+	blueprint.SimpleName
+	properties struct {
+		Deps []string
+	}
+}
+
+func (b *blueprintTestModule) DynamicDependencies(ctx blueprint.DynamicDependerModuleContext) []string {
+	return b.properties.Deps
+}
+
+func (b *blueprintTestModule) GenerateBuildActions(blueprint.ModuleContext) {
+}
+
+func newBlueprintTestModule() (blueprint.Module, []interface{}) {
+	m := &blueprintTestModule{}
+	return m, []interface{}{&m.properties, &m.SimpleName.Properties}
+}
diff --git a/android/neverallow.go b/android/neverallow.go
index ee3bf4a..f35d1fe 100644
--- a/android/neverallow.go
+++ b/android/neverallow.go
@@ -100,13 +100,12 @@
 		"development",
 	}
 
-	// Core library constraints. The no_standard_libs can only be used in core
-	// library projects. Access to core library targets is restricted using
-	// visibility rules.
+	// Core library constraints. The sdk_version: "none" can only be used in core library projects.
+	// Access to core library targets is restricted using visibility rules.
 	rules := []*rule{
 		neverallow().
 			notIn(coreLibraryProjects...).
-			with("no_standard_libs", "true"),
+			with("sdk_version", "none"),
 	}
 
 	return rules
diff --git a/android/neverallow_test.go b/android/neverallow_test.go
index 40ccf14..ee3c94f 100644
--- a/android/neverallow_test.go
+++ b/android/neverallow_test.go
@@ -148,33 +148,33 @@
 	},
 	// Libcore rule tests
 	{
-		name: "no_standard_libs: true inside core libraries",
+		name: "sdk_version: \"none\" inside core libraries",
 		fs: map[string][]byte{
 			"libcore/Blueprints": []byte(`
 				java_library {
 					name: "inside_core_libraries",
-					no_standard_libs: true,
+					sdk_version: "none",
 				}`),
 		},
 	},
 	{
-		name: "no_standard_libs: true outside core libraries",
+		name: "sdk_version: \"none\" outside core libraries",
 		fs: map[string][]byte{
 			"Blueprints": []byte(`
 				java_library {
 					name: "outside_core_libraries",
-					no_standard_libs: true,
+					sdk_version: "none",
 				}`),
 		},
 		expectedError: "module \"outside_core_libraries\": violates neverallow",
 	},
 	{
-		name: "no_standard_libs: false",
+		name: "sdk_version: \"current\"",
 		fs: map[string][]byte{
 			"Blueprints": []byte(`
 				java_library {
 					name: "outside_core_libraries",
-					no_standard_libs: false,
+					sdk_version: "current",
 				}`),
 		},
 	},
@@ -200,6 +200,7 @@
 	ctx := NewTestContext()
 	ctx.RegisterModuleType("cc_library", ModuleFactoryAdaptor(newMockCcLibraryModule))
 	ctx.RegisterModuleType("java_library", ModuleFactoryAdaptor(newMockJavaLibraryModule))
+	ctx.RegisterModuleType("java_library_host", ModuleFactoryAdaptor(newMockJavaLibraryModule))
 	ctx.RegisterModuleType("java_device_for_host", ModuleFactoryAdaptor(newMockJavaLibraryModule))
 	ctx.PostDepsMutators(registerNeverallowMutator)
 	ctx.Register()
@@ -251,8 +252,8 @@
 }
 
 type mockJavaLibraryProperties struct {
-	Libs             []string
-	No_standard_libs *bool
+	Libs        []string
+	Sdk_version *string
 }
 
 type mockJavaLibraryModule struct {
diff --git a/androidmk/cmd/androidmk/android.go b/androidmk/cmd/androidmk/android.go
index 62aa3dc..af81e43 100644
--- a/androidmk/cmd/androidmk/android.go
+++ b/androidmk/cmd/androidmk/android.go
@@ -185,7 +185,6 @@
 			"LOCAL_NO_CRT":                     "nocrt",
 			"LOCAL_ALLOW_UNDEFINED_SYMBOLS":    "allow_undefined_symbols",
 			"LOCAL_RTTI_FLAG":                  "rtti",
-			"LOCAL_NO_STANDARD_LIBRARIES":      "no_standard_libs",
 			"LOCAL_PACK_MODULE_RELOCATIONS":    "pack_relocations",
 			"LOCAL_TIDY":                       "tidy",
 			"LOCAL_USE_CLANG_LLD":              "use_clang_lld",
diff --git a/apex/apex.go b/apex/apex.go
index 11b433c..84e5497 100644
--- a/apex/apex.go
+++ b/apex/apex.go
@@ -61,7 +61,7 @@
 			`--key ${key} ${opt_flags} ${image_dir} ${out} `,
 		CommandDeps: []string{"${apexer}", "${avbtool}", "${e2fsdroid}", "${merge_zips}",
 			"${mke2fs}", "${resize2fs}", "${sefcontext_compile}",
-			"${soong_zip}", "${zipalign}", "${aapt2}"},
+			"${soong_zip}", "${zipalign}", "${aapt2}", "prebuilts/sdk/current/public/android.jar"},
 		Description: "APEX ${image_dir} => ${out}",
 	}, "tool_path", "image_dir", "copy_commands", "manifest", "file_contexts", "canned_fs_config", "key", "opt_flags")
 
@@ -1062,6 +1062,10 @@
 		Description: "signapk",
 		Output:      a.outputFiles[apexType],
 		Input:       unsignedOutputFile,
+		Implicits: []android.Path{
+			a.container_certificate_file,
+			a.container_private_key_file,
+		},
 		Args: map[string]string{
 			"certificates": a.container_certificate_file.String() + " " + a.container_private_key_file.String(),
 			"flags":        "-a 4096", //alignment
diff --git a/cc/builder.go b/cc/builder.go
index 3a8afc0..1e12361 100644
--- a/cc/builder.go
+++ b/cc/builder.go
@@ -22,7 +22,6 @@
 	"fmt"
 	"path/filepath"
 	"runtime"
-	"strconv"
 	"strings"
 
 	"github.com/google/blueprint"
@@ -93,20 +92,6 @@
 		},
 		"arCmd", "arFlags")
 
-	darwinAr = pctx.AndroidStaticRule("darwinAr",
-		blueprint.RuleParams{
-			Command:     "rm -f ${out} && ${config.MacArPath} $arFlags $out $in",
-			CommandDeps: []string{"${config.MacArPath}"},
-		},
-		"arFlags")
-
-	darwinAppendAr = pctx.AndroidStaticRule("darwinAppendAr",
-		blueprint.RuleParams{
-			Command:     "cp -f ${inAr} ${out}.tmp && ${config.MacArPath} $arFlags ${out}.tmp $in && mv ${out}.tmp ${out}",
-			CommandDeps: []string{"${config.MacArPath}", "${inAr}"},
-		},
-		"arFlags", "inAr")
-
 	darwinStrip = pctx.AndroidStaticRule("darwinStrip",
 		blueprint.RuleParams{
 			Command:     "${config.MacStripPath} -u -r -o $out $in",
@@ -515,11 +500,6 @@
 func TransformObjToStaticLib(ctx android.ModuleContext, objFiles android.Paths,
 	flags builderFlags, outputFile android.ModuleOutPath, deps android.Paths) {
 
-	if ctx.Darwin() {
-		transformDarwinObjToStaticLib(ctx, objFiles, flags, outputFile, deps)
-		return
-	}
-
 	arCmd := "${config.ClangBin}/llvm-ar"
 	arFlags := "crsD"
 	if !ctx.Darwin() {
@@ -542,82 +522,6 @@
 	})
 }
 
-// Generate a rule for compiling multiple .o files to a static library (.a) on
-// darwin.  The darwin ar tool doesn't support @file for list files, and has a
-// very small command line length limit, so we have to split the ar into multiple
-// steps, each appending to the previous one.
-func transformDarwinObjToStaticLib(ctx android.ModuleContext, objFiles android.Paths,
-	flags builderFlags, outputFile android.ModuleOutPath, deps android.Paths) {
-
-	arFlags := "cqs"
-
-	if len(objFiles) == 0 {
-		dummy := android.PathForModuleOut(ctx, "dummy"+objectExtension)
-		dummyAr := android.PathForModuleOut(ctx, "dummy"+staticLibraryExtension)
-
-		ctx.Build(pctx, android.BuildParams{
-			Rule:        emptyFile,
-			Description: "empty object file",
-			Output:      dummy,
-			Implicits:   deps,
-		})
-
-		ctx.Build(pctx, android.BuildParams{
-			Rule:        darwinAr,
-			Description: "empty static archive",
-			Output:      dummyAr,
-			Input:       dummy,
-			Args: map[string]string{
-				"arFlags": arFlags,
-			},
-		})
-
-		ctx.Build(pctx, android.BuildParams{
-			Rule:        darwinAppendAr,
-			Description: "static link " + outputFile.Base(),
-			Output:      outputFile,
-			Input:       dummy,
-			Args: map[string]string{
-				"arFlags": "d",
-				"inAr":    dummyAr.String(),
-			},
-		})
-
-		return
-	}
-
-	// ARG_MAX on darwin is 262144, use half that to be safe
-	objFilesLists, err := splitListForSize(objFiles, 131072)
-	if err != nil {
-		ctx.ModuleErrorf("%s", err.Error())
-	}
-
-	var in, out android.WritablePath
-	for i, l := range objFilesLists {
-		in = out
-		out = outputFile
-		if i != len(objFilesLists)-1 {
-			out = android.PathForModuleOut(ctx, outputFile.Base()+strconv.Itoa(i))
-		}
-
-		build := android.BuildParams{
-			Rule:        darwinAr,
-			Description: "static link " + out.Base(),
-			Output:      out,
-			Inputs:      l,
-			Implicits:   deps,
-			Args: map[string]string{
-				"arFlags": arFlags,
-			},
-		}
-		if i != 0 {
-			build.Rule = darwinAppendAr
-			build.Args["inAr"] = in.String()
-		}
-		ctx.Build(pctx, build)
-	}
-}
-
 // Generate a rule for compiling multiple .o files, plus static libraries, whole static libraries,
 // and shared libraries, to a shared library (.so) or dynamic executable
 func TransformObjToDynamicBinary(ctx android.ModuleContext,
diff --git a/cc/config/global.go b/cc/config/global.go
index 24075d9..a27246e 100644
--- a/cc/config/global.go
+++ b/cc/config/global.go
@@ -122,8 +122,8 @@
 
 	// prebuilts/clang default settings.
 	ClangDefaultBase         = "prebuilts/clang/host"
-	ClangDefaultVersion      = "clang-r353983c"
-	ClangDefaultShortVersion = "9.0.3"
+	ClangDefaultVersion      = "clang-r353983d"
+	ClangDefaultShortVersion = "9.0.4"
 
 	// Directories with warnings from Android.bp files.
 	WarningAllowedProjects = []string{
diff --git a/cc/gen.go b/cc/gen.go
index 82669ac..1d30dab 100644
--- a/cc/gen.go
+++ b/cc/gen.go
@@ -39,11 +39,11 @@
 
 	sysprop = pctx.AndroidStaticRule("sysprop",
 		blueprint.RuleParams{
-			Command: "$syspropCmd --header-dir=$headerOutDir --system-header-dir=$systemOutDir " +
+			Command: "$syspropCmd --header-dir=$headerOutDir --public-header-dir=$publicOutDir " +
 				"--source-dir=$srcOutDir --include-name=$includeName $in",
 			CommandDeps: []string{"$syspropCmd"},
 		},
-		"headerOutDir", "systemOutDir", "srcOutDir", "includeName")
+		"headerOutDir", "publicOutDir", "srcOutDir", "includeName")
 
 	windmc = pctx.AndroidStaticRule("windmc",
 		blueprint.RuleParams{
@@ -150,7 +150,7 @@
 
 func genSysprop(ctx android.ModuleContext, syspropFile android.Path) (android.Path, android.Path) {
 	headerFile := android.PathForModuleGen(ctx, "sysprop", "include", syspropFile.Rel()+".h")
-	systemHeaderFile := android.PathForModuleGen(ctx, "sysprop/system", "include", syspropFile.Rel()+".h")
+	publicHeaderFile := android.PathForModuleGen(ctx, "sysprop/public", "include", syspropFile.Rel()+".h")
 	cppFile := android.PathForModuleGen(ctx, "sysprop", syspropFile.Rel()+".cpp")
 
 	ctx.Build(pctx, android.BuildParams{
@@ -161,7 +161,7 @@
 		Input:          syspropFile,
 		Args: map[string]string{
 			"headerOutDir": filepath.Dir(headerFile.String()),
-			"systemOutDir": filepath.Dir(systemHeaderFile.String()),
+			"publicOutDir": filepath.Dir(publicHeaderFile.String()),
 			"srcOutDir":    filepath.Dir(cppFile.String()),
 			"includeName":  syspropFile.Rel() + ".h",
 		},
diff --git a/cc/library.go b/cc/library.go
index fab5837..f98cd36 100644
--- a/cc/library.go
+++ b/cc/library.go
@@ -882,10 +882,10 @@
 			isVendor := ctx.useVndk()
 			isOwnerPlatform := Bool(library.Properties.Sysprop.Platform)
 
-			useSystem := isProduct || (isOwnerPlatform == isVendor)
+			usePublic := isProduct || (isOwnerPlatform == isVendor)
 
-			if useSystem {
-				dir = android.PathForModuleGen(ctx, "sysprop/system", "include").String()
+			if usePublic {
+				dir = android.PathForModuleGen(ctx, "sysprop/public", "include").String()
 			}
 		}
 
diff --git a/cc/pgo.go b/cc/pgo.go
index 7334ea2..4e915ff 100644
--- a/cc/pgo.go
+++ b/cc/pgo.go
@@ -27,10 +27,9 @@
 
 var (
 	// Add flags to ignore warnings that profiles are old or missing for
-	// some functions, and turn on the experimental new pass manager.
+	// some functions.
 	profileUseOtherFlags = []string{
 		"-Wno-backend-plugin",
-		"-fexperimental-new-pass-manager",
 	}
 
 	globalPgoProfileProjects = []string{
diff --git a/cc/rs.go b/cc/rs.go
index 5421b92..fbc6bfb 100644
--- a/cc/rs.go
+++ b/cc/rs.go
@@ -72,11 +72,12 @@
 	stampFile := android.PathForModuleGen(ctx, "rs", "rs.stamp")
 	depFiles := make(android.WritablePaths, 0, len(rsFiles))
 	genFiles := make(android.WritablePaths, 0, 2*len(rsFiles))
+	headers := make(android.Paths, 0, len(rsFiles))
 	for _, rsFile := range rsFiles {
 		depFiles = append(depFiles, rsGeneratedDepFile(ctx, rsFile))
-		genFiles = append(genFiles,
-			rsGeneratedCppFile(ctx, rsFile),
-			rsGeneratedHFile(ctx, rsFile))
+		headerFile := rsGeneratedHFile(ctx, rsFile)
+		genFiles = append(genFiles, rsGeneratedCppFile(ctx, rsFile), headerFile)
+		headers = append(headers, headerFile)
 	}
 
 	ctx.Build(pctx, android.BuildParams{
@@ -92,7 +93,7 @@
 		},
 	})
 
-	return android.Paths{stampFile}
+	return headers
 }
 
 func rsFlags(ctx ModuleContext, flags Flags, properties *BaseCompilerProperties) Flags {
diff --git a/cc/sanitize.go b/cc/sanitize.go
index fdda7be..0af0659 100644
--- a/cc/sanitize.go
+++ b/cc/sanitize.go
@@ -40,7 +40,8 @@
 	hwasanCflags = []string{"-fno-omit-frame-pointer", "-Wno-frame-larger-than=",
 		"-mllvm", "-hwasan-create-frame-descriptions=0",
 		"-mllvm", "-hwasan-allow-ifunc",
-		"-fsanitize-hwaddress-abi=platform"}
+		"-fsanitize-hwaddress-abi=platform",
+		"-fno-experimental-new-pass-manager"}
 
 	cfiCflags = []string{"-flto", "-fsanitize-cfi-cross-dso",
 		"-fsanitize-blacklist=external/compiler-rt/lib/cfi/cfi_blacklist.txt"}
diff --git a/cc/test.go b/cc/test.go
index dae2a37..c735fd9 100644
--- a/cc/test.go
+++ b/cc/test.go
@@ -64,6 +64,10 @@
 
 	// Test options.
 	Test_options TestOptions
+
+	// Add RootTargetPreparer to auto generated test config. This guarantees the test to run
+	// with root permission.
+	Require_root *bool
 }
 
 func init() {
@@ -273,18 +277,19 @@
 
 func (test *testBinary) install(ctx ModuleContext, file android.Path) {
 	test.data = android.PathsForModuleSrc(ctx, test.Properties.Data)
-	optionsMap := map[string]string{}
-	if Bool(test.testDecorator.Properties.Isolated) {
-		optionsMap["not-shardable"] = "true"
+	var configs []tradefed.Config
+	if Bool(test.Properties.Require_root) {
+		configs = append(configs, tradefed.Preparer{"com.android.tradefed.targetprep.RootTargetPreparer"})
 	}
-
+	if Bool(test.testDecorator.Properties.Isolated) {
+		configs = append(configs, tradefed.Option{"not-shardable", "true"})
+	}
 	if test.Properties.Test_options.Run_test_as != nil {
-		optionsMap["run-test-as"] = String(test.Properties.Test_options.Run_test_as)
+		configs = append(configs, tradefed.Option{"run-test-as", String(test.Properties.Test_options.Run_test_as)})
 	}
 
 	test.testConfig = tradefed.AutoGenNativeTestConfig(ctx, test.Properties.Test_config,
-		test.Properties.Test_config_template,
-		test.Properties.Test_suites, optionsMap)
+		test.Properties.Test_config_template, test.Properties.Test_suites, configs)
 
 	test.binaryDecorator.baseInstaller.dir = "nativetest"
 	test.binaryDecorator.baseInstaller.dir64 = "nativetest64"
@@ -371,6 +376,10 @@
 	// the name of the test configuration template (for example "AndroidTestTemplate.xml") that
 	// should be installed with the module.
 	Test_config_template *string `android:"path,arch_variant"`
+
+	// Add RootTargetPreparer to auto generated test config. This guarantees the test to run
+	// with root permission.
+	Require_root *bool
 }
 
 type benchmarkDecorator struct {
@@ -403,8 +412,12 @@
 
 func (benchmark *benchmarkDecorator) install(ctx ModuleContext, file android.Path) {
 	benchmark.data = android.PathsForModuleSrc(ctx, benchmark.Properties.Data)
+	var configs []tradefed.Config
+	if Bool(benchmark.Properties.Require_root) {
+		configs = append(configs, tradefed.Preparer{"com.android.tradefed.targetprep.RootTargetPreparer"})
+	}
 	benchmark.testConfig = tradefed.AutoGenNativeBenchmarkTestConfig(ctx, benchmark.Properties.Test_config,
-		benchmark.Properties.Test_config_template, benchmark.Properties.Test_suites)
+		benchmark.Properties.Test_config_template, benchmark.Properties.Test_suites, configs)
 
 	benchmark.binaryDecorator.baseInstaller.dir = filepath.Join("benchmarktest", ctx.ModuleName())
 	benchmark.binaryDecorator.baseInstaller.dir64 = filepath.Join("benchmarktest64", ctx.ModuleName())
diff --git a/cmd/multiproduct_kati/main.go b/cmd/multiproduct_kati/main.go
index 330c5dd..1171a65 100644
--- a/cmd/multiproduct_kati/main.go
+++ b/cmd/multiproduct_kati/main.go
@@ -156,10 +156,12 @@
 }
 
 func main() {
-	writer := terminal.NewWriter(terminal.StdioImpl{})
-	defer writer.Finish()
+	stdio := terminal.StdioImpl{}
 
-	log := logger.New(writer)
+	output := terminal.NewStatusOutput(stdio.Stdout(), "",
+		build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))
+
+	log := logger.New(output)
 	defer log.Cleanup()
 
 	flag.Parse()
@@ -172,8 +174,7 @@
 
 	stat := &status.Status{}
 	defer stat.Finish()
-	stat.AddOutput(terminal.NewStatusOutput(writer, "",
-		build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD")))
+	stat.AddOutput(output)
 
 	var failures failureCount
 	stat.AddOutput(&failures)
@@ -188,7 +189,7 @@
 		Context: ctx,
 		Logger:  log,
 		Tracer:  trace,
-		Writer:  writer,
+		Writer:  output,
 		Status:  stat,
 	}}
 
@@ -341,7 +342,7 @@
 	} else if failures > 1 {
 		log.Fatalf("%d failures", failures)
 	} else {
-		writer.Print("Success")
+		fmt.Fprintln(output, "Success")
 	}
 }
 
@@ -386,7 +387,7 @@
 		Context: mpctx.Context,
 		Logger:  log,
 		Tracer:  mpctx.Tracer,
-		Writer:  terminal.NewWriter(terminal.NewCustomStdio(nil, f, f)),
+		Writer:  f,
 		Thread:  mpctx.Tracer.NewThread(product),
 		Status:  &status.Status{},
 	}}
@@ -466,3 +467,8 @@
 }
 
 func (f *failureCount) Flush() {}
+
+func (f *failureCount) Write(p []byte) (int, error) {
+	// discard writes
+	return len(p), nil
+}
diff --git a/cmd/soong_ui/main.go b/cmd/soong_ui/main.go
index 5f9bd01..b909779 100644
--- a/cmd/soong_ui/main.go
+++ b/cmd/soong_ui/main.go
@@ -61,10 +61,8 @@
 		config: func(ctx build.Context, args ...string) build.Config {
 			return build.NewConfig(ctx, args...)
 		},
-		stdio: func() terminal.StdioInterface {
-			return terminal.StdioImpl{}
-		},
-		run: make,
+		stdio: stdio,
+		run:   make,
 	}, {
 		flag:        "--dumpvar-mode",
 		description: "print the value of the legacy make variable VAR to stdout",
@@ -77,6 +75,12 @@
 		config:      dumpVarConfig,
 		stdio:       customStdio,
 		run:         dumpVars,
+	}, {
+		flag:        "--build-mode",
+		description: "build modules based on the specified build action",
+		config:      buildActionConfig,
+		stdio:       stdio,
+		run:         make,
 	},
 }
 
@@ -109,10 +113,10 @@
 		os.Exit(1)
 	}
 
-	writer := terminal.NewWriter(c.stdio())
-	defer writer.Finish()
+	output := terminal.NewStatusOutput(c.stdio().Stdout(), os.Getenv("NINJA_STATUS"),
+		build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))
 
-	log := logger.New(writer)
+	log := logger.New(output)
 	defer log.Cleanup()
 
 	ctx, cancel := context.WithCancel(context.Background())
@@ -125,8 +129,7 @@
 
 	stat := &status.Status{}
 	defer stat.Finish()
-	stat.AddOutput(terminal.NewStatusOutput(writer, os.Getenv("NINJA_STATUS"),
-		build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD")))
+	stat.AddOutput(output)
 	stat.AddOutput(trace.StatusTracer())
 
 	build.SetupSignals(log, cancel, func() {
@@ -140,7 +143,7 @@
 		Logger:  log,
 		Metrics: met,
 		Tracer:  trace,
-		Writer:  writer,
+		Writer:  output,
 		Status:  stat,
 	}}
 
@@ -159,7 +162,7 @@
 	stat.AddOutput(status.NewVerboseLog(log, filepath.Join(logsDir, "verbose.log")))
 	stat.AddOutput(status.NewErrorLog(log, filepath.Join(logsDir, "error.log")))
 
-	defer met.Dump(filepath.Join(logsDir, "build_metrics"))
+	defer met.Dump(filepath.Join(logsDir, "soong_metrics"))
 
 	if start, ok := os.LookupEnv("TRACE_BEGIN_SOONG"); ok {
 		if !strings.HasSuffix(start, "N") {
@@ -185,13 +188,13 @@
 func dumpVar(ctx build.Context, config build.Config, args []string, _ string) {
 	flags := flag.NewFlagSet("dumpvar", flag.ExitOnError)
 	flags.Usage = func() {
-		fmt.Fprintf(os.Stderr, "usage: %s --dumpvar-mode [--abs] <VAR>\n\n", os.Args[0])
-		fmt.Fprintln(os.Stderr, "In dumpvar mode, print the value of the legacy make variable VAR to stdout")
-		fmt.Fprintln(os.Stderr, "")
+		fmt.Fprintf(ctx.Writer, "usage: %s --dumpvar-mode [--abs] <VAR>\n\n", os.Args[0])
+		fmt.Fprintln(ctx.Writer, "In dumpvar mode, print the value of the legacy make variable VAR to stdout")
+		fmt.Fprintln(ctx.Writer, "")
 
-		fmt.Fprintln(os.Stderr, "'report_config' is a special case that prints the human-readable config banner")
-		fmt.Fprintln(os.Stderr, "from the beginning of the build.")
-		fmt.Fprintln(os.Stderr, "")
+		fmt.Fprintln(ctx.Writer, "'report_config' is a special case that prints the human-readable config banner")
+		fmt.Fprintln(ctx.Writer, "from the beginning of the build.")
+		fmt.Fprintln(ctx.Writer, "")
 		flags.PrintDefaults()
 	}
 	abs := flags.Bool("abs", false, "Print the absolute path of the value")
@@ -235,15 +238,15 @@
 func dumpVars(ctx build.Context, config build.Config, args []string, _ string) {
 	flags := flag.NewFlagSet("dumpvars", flag.ExitOnError)
 	flags.Usage = func() {
-		fmt.Fprintf(os.Stderr, "usage: %s --dumpvars-mode [--vars=\"VAR VAR ...\"]\n\n", os.Args[0])
-		fmt.Fprintln(os.Stderr, "In dumpvars mode, dump the values of one or more legacy make variables, in")
-		fmt.Fprintln(os.Stderr, "shell syntax. The resulting output may be sourced directly into a shell to")
-		fmt.Fprintln(os.Stderr, "set corresponding shell variables.")
-		fmt.Fprintln(os.Stderr, "")
+		fmt.Fprintf(ctx.Writer, "usage: %s --dumpvars-mode [--vars=\"VAR VAR ...\"]\n\n", os.Args[0])
+		fmt.Fprintln(ctx.Writer, "In dumpvars mode, dump the values of one or more legacy make variables, in")
+		fmt.Fprintln(ctx.Writer, "shell syntax. The resulting output may be sourced directly into a shell to")
+		fmt.Fprintln(ctx.Writer, "set corresponding shell variables.")
+		fmt.Fprintln(ctx.Writer, "")
 
-		fmt.Fprintln(os.Stderr, "'report_config' is a special case that dumps a variable containing the")
-		fmt.Fprintln(os.Stderr, "human-readable config banner from the beginning of the build.")
-		fmt.Fprintln(os.Stderr, "")
+		fmt.Fprintln(ctx.Writer, "'report_config' is a special case that dumps a variable containing the")
+		fmt.Fprintln(ctx.Writer, "human-readable config banner from the beginning of the build.")
+		fmt.Fprintln(ctx.Writer, "")
 		flags.PrintDefaults()
 	}
 
@@ -300,6 +303,10 @@
 	}
 }
 
+func stdio() terminal.StdioInterface {
+	return terminal.StdioImpl{}
+}
+
 func customStdio() terminal.StdioInterface {
 	return terminal.NewCustomStdio(os.Stdin, os.Stderr, os.Stderr)
 }
@@ -309,16 +316,98 @@
 	return build.NewConfig(ctx)
 }
 
+func buildActionConfig(ctx build.Context, args ...string) build.Config {
+	flags := flag.NewFlagSet("build-mode", flag.ContinueOnError)
+	flags.Usage = func() {
+		fmt.Fprintf(ctx.Writer, "usage: %s --build-mode --dir=<path> <build action> [<build arg 1> <build arg 2> ...]\n\n", os.Args[0])
+		fmt.Fprintln(ctx.Writer, "In build mode, build the set of modules based on the specified build")
+		fmt.Fprintln(ctx.Writer, "action. The --dir flag is required to determine what is needed to")
+		fmt.Fprintln(ctx.Writer, "build in the source tree based on the build action. See below for")
+		fmt.Fprintln(ctx.Writer, "the list of acceptable build action flags.")
+		fmt.Fprintln(ctx.Writer, "")
+		flags.PrintDefaults()
+	}
+
+	buildActionFlags := []struct {
+		name              string
+		description       string
+		action            build.BuildAction
+		buildDependencies bool
+		set               bool
+	}{{
+		name:              "all-modules",
+		description:       "Build action: build from the top of the source tree.",
+		action:            build.BUILD_MODULES_IN_A_DIRECTORY,
+		buildDependencies: true,
+	}, {
+		name:              "modules-in-a-dir-no-deps",
+		description:       "Build action: builds all of the modules in the current directory without their dependencies.",
+		action:            build.BUILD_MODULES_IN_A_DIRECTORY,
+		buildDependencies: false,
+	}, {
+		name:              "modules-in-dirs-no-deps",
+		description:       "Build action: builds all of the modules in the supplied directories without their dependencies.",
+		action:            build.BUILD_MODULES_IN_DIRECTORIES,
+		buildDependencies: false,
+	}, {
+		name:              "modules-in-a-dir",
+		description:       "Build action: builds all of the modules in the current directory and their dependencies.",
+		action:            build.BUILD_MODULES_IN_A_DIRECTORY,
+		buildDependencies: true,
+	}, {
+		name:              "modules-in-dirs",
+		description:       "Build action: builds all of the modules in the supplied directories and their dependencies.",
+		action:            build.BUILD_MODULES_IN_DIRECTORIES,
+		buildDependencies: true,
+	}}
+	for i, flag := range buildActionFlags {
+		flags.BoolVar(&buildActionFlags[i].set, flag.name, false, flag.description)
+	}
+	dir := flags.String("dir", "", "Directory of the executed build command.")
+
+	// Only interested in the first two args which defines the build action and the directory.
+	// The remaining arguments are passed down to the config.
+	const numBuildActionFlags = 2
+	if len(args) < numBuildActionFlags {
+		flags.Usage()
+		ctx.Fatalln("Improper build action arguments.")
+	}
+	flags.Parse(args[0:numBuildActionFlags])
+
+	// The next block of code is to validate that exactly one build action is set and the dir flag
+	// is specified.
+	buildActionCount := 0
+	var buildAction build.BuildAction
+	buildDependency := false
+	for _, flag := range buildActionFlags {
+		if flag.set {
+			buildActionCount++
+			buildAction = flag.action
+			buildDependency = flag.buildDependencies
+		}
+	}
+	if buildActionCount != 1 {
+		ctx.Fatalln("Build action not defined.")
+	}
+	if *dir == "" {
+		ctx.Fatalln("-dir not specified.")
+	}
+
+	// Remove the build action flags from the args as they are not recognized by the config.
+	args = args[numBuildActionFlags:]
+	return build.NewBuildActionConfig(buildAction, *dir, buildDependency, ctx, args...)
+}
+
 func make(ctx build.Context, config build.Config, _ []string, logsDir string) {
 	if config.IsVerbose() {
 		writer := ctx.Writer
-		writer.Print("! The argument `showcommands` is no longer supported.")
-		writer.Print("! Instead, the verbose log is always written to a compressed file in the output dir:")
-		writer.Print("!")
-		writer.Print(fmt.Sprintf("!   gzip -cd %s/verbose.log.gz | less -R", logsDir))
-		writer.Print("!")
-		writer.Print("! Older versions are saved in verbose.log.#.gz files")
-		writer.Print("")
+		fmt.Fprintln(writer, "! The argument `showcommands` is no longer supported.")
+		fmt.Fprintln(writer, "! Instead, the verbose log is always written to a compressed file in the output dir:")
+		fmt.Fprintln(writer, "!")
+		fmt.Fprintf(writer, "!   gzip -cd %s/verbose.log.gz | less -R\n", logsDir)
+		fmt.Fprintln(writer, "!")
+		fmt.Fprintln(writer, "! Older versions are saved in verbose.log.#.gz files")
+		fmt.Fprintln(writer, "")
 		time.Sleep(5 * time.Second)
 	}
 
diff --git a/dexpreopt/config.go b/dexpreopt/config.go
index 5a1bd74..a2f1af4 100644
--- a/dexpreopt/config.go
+++ b/dexpreopt/config.go
@@ -121,8 +121,9 @@
 	UsesLibraries                []string
 	LibraryPaths                 map[string]android.Path
 
-	Archs           []android.ArchType
-	DexPreoptImages []android.Path
+	Archs               []android.ArchType
+	DexPreoptImages     []android.Path
+	DexPreoptImagesDeps []android.Paths
 
 	PreoptBootClassPathDexFiles     android.Paths // file paths of boot class path files
 	PreoptBootClassPathDexLocations []string      // virtual locations of boot class path files
@@ -257,6 +258,9 @@
 	config.ModuleConfig.StripInputPath = constructPath(ctx, config.StripInputPath)
 	config.ModuleConfig.StripOutputPath = constructWritablePath(ctx, config.StripOutputPath)
 
+	// This needs to exist, but dependencies are already handled in Make, so we don't need to pass them through JSON.
+	config.ModuleConfig.DexPreoptImagesDeps = make([]android.Paths, len(config.ModuleConfig.DexPreoptImages))
+
 	return config.ModuleConfig, nil
 }
 
diff --git a/dexpreopt/dexpreopt.go b/dexpreopt/dexpreopt.go
index 0be37d0..e02e60f 100644
--- a/dexpreopt/dexpreopt.go
+++ b/dexpreopt/dexpreopt.go
@@ -125,7 +125,8 @@
 
 			for i, arch := range module.Archs {
 				image := module.DexPreoptImages[i]
-				dexpreoptCommand(ctx, global, module, rule, arch, profile, image, appImage, generateDM)
+				imageDeps := module.DexPreoptImagesDeps[i]
+				dexpreoptCommand(ctx, global, module, rule, arch, profile, image, imageDeps, appImage, generateDM)
 			}
 		}
 	}
@@ -190,7 +191,7 @@
 }
 
 func dexpreoptCommand(ctx android.PathContext, global GlobalConfig, module ModuleConfig, rule *android.RuleBuilder,
-	arch android.ArchType, profile, bootImage android.Path, appImage, generateDM bool) {
+	arch android.ArchType, profile, bootImage android.Path, bootImageDeps android.Paths, appImage, generateDM bool) {
 
 	// HACK: make soname in Soong-generated .odex files match Make.
 	base := filepath.Base(module.DexLocation)
@@ -353,7 +354,7 @@
 		Flag("--runtime-arg").FlagWithList("-Xbootclasspath-locations:", module.PreoptBootClassPathDexLocations, ":").
 		Flag("${class_loader_context_arg}").
 		Flag("${stored_class_loader_context_arg}").
-		FlagWithArg("--boot-image=", bootImageLocation).Implicit(bootImage).
+		FlagWithArg("--boot-image=", bootImageLocation).Implicits(bootImageDeps).
 		FlagWithInput("--dex-file=", module.DexPath).
 		FlagWithArg("--dex-location=", dexLocationArg).
 		FlagWithOutput("--oat-file=", odexPath).ImplicitOutput(vdexPath).
diff --git a/dexpreopt/dexpreopt_test.go b/dexpreopt/dexpreopt_test.go
index 0402f87..7f1fe42 100644
--- a/dexpreopt/dexpreopt_test.go
+++ b/dexpreopt/dexpreopt_test.go
@@ -38,6 +38,7 @@
 		LibraryPaths:                    nil,
 		Archs:                           []android.ArchType{android.Arm},
 		DexPreoptImages:                 android.Paths{android.PathForTesting("system/framework/arm/boot.art")},
+		DexPreoptImagesDeps:             []android.Paths{android.Paths{}},
 		PreoptBootClassPathDexFiles:     nil,
 		PreoptBootClassPathDexLocations: nil,
 		PreoptExtractedApk:              false,
diff --git a/jar/Android.bp b/jar/Android.bp
index 6c2e60e..2563474 100644
--- a/jar/Android.bp
+++ b/jar/Android.bp
@@ -18,8 +18,10 @@
     srcs: [
         "jar.go",
     ],
+    testSrcs: [
+        "jar_test.go",
+    ],
     deps: [
         "android-archive-zip",
     ],
 }
-
diff --git a/jar/jar.go b/jar/jar.go
index fa0e693..a8f06a4 100644
--- a/jar/jar.go
+++ b/jar/jar.go
@@ -17,9 +17,12 @@
 import (
 	"bytes"
 	"fmt"
+	"io"
 	"os"
 	"strings"
+	"text/scanner"
 	"time"
+	"unicode"
 
 	"android/soong/third_party/zip"
 )
@@ -112,3 +115,111 @@
 
 	return finalBytes, nil
 }
+
+var javaIgnorableIdentifier = &unicode.RangeTable{
+	R16: []unicode.Range16{
+		{0x00, 0x08, 1},
+		{0x0e, 0x1b, 1},
+		{0x7f, 0x9f, 1},
+	},
+	LatinOffset: 3,
+}
+
+func javaIdentRune(ch rune, i int) bool {
+	if unicode.IsLetter(ch) {
+		return true
+	}
+	if unicode.IsDigit(ch) && i > 0 {
+		return true
+	}
+
+	if unicode.In(ch,
+		unicode.Nl, // letter number
+		unicode.Sc, // currency symbol
+		unicode.Pc, // connecting punctuation
+	) {
+		return true
+	}
+
+	if unicode.In(ch,
+		unicode.Cf, // format
+		unicode.Mc, // combining mark
+		unicode.Mn, // non-spacing mark
+		javaIgnorableIdentifier,
+	) && i > 0 {
+		return true
+	}
+
+	return false
+}
+
+// JavaPackage parses the package out of a java source file by looking for the package statement, or the first valid
+// non-package statement, in which case it returns an empty string for the package.
+func JavaPackage(r io.Reader, src string) (string, error) {
+	var s scanner.Scanner
+	var sErr error
+
+	s.Init(r)
+	s.Filename = src
+	s.Error = func(s *scanner.Scanner, msg string) {
+		sErr = fmt.Errorf("error parsing %q: %s", src, msg)
+	}
+	s.IsIdentRune = javaIdentRune
+
+	tok := s.Scan()
+	if sErr != nil {
+		return "", sErr
+	}
+	if tok == scanner.Ident {
+		switch s.TokenText() {
+		case "package":
+		// Nothing
+		case "import":
+			// File has no package statement, first keyword is an import
+			return "", nil
+		case "class", "enum", "interface":
+			// File has no package statement, first keyword is a type declaration
+			return "", nil
+		case "public", "protected", "private", "abstract", "static", "final", "strictfp":
+			// File has no package statement, first keyword is a modifier
+			return "", nil
+		case "module", "open":
+			// File has no package statement, first keyword is a module declaration
+			return "", nil
+		default:
+			return "", fmt.Errorf(`expected first token of java file to be "package", got %q`, s.TokenText())
+		}
+	} else if tok == '@' {
+		// File has no package statement, first token is an annotation
+		return "", nil
+	} else if tok == scanner.EOF {
+		// File no package statement, it has no non-whitespace non-comment tokens
+		return "", nil
+	} else {
+		return "", fmt.Errorf(`expected first token of java file to be "package", got %q`, s.TokenText())
+	}
+
+	var pkg string
+	for {
+		tok = s.Scan()
+		if sErr != nil {
+			return "", sErr
+		}
+		if tok != scanner.Ident {
+			return "", fmt.Errorf(`expected "package <package>;", got "package %s%s"`, pkg, s.TokenText())
+		}
+		pkg += s.TokenText()
+
+		tok = s.Scan()
+		if sErr != nil {
+			return "", sErr
+		}
+		if tok == ';' {
+			return pkg, nil
+		} else if tok == '.' {
+			pkg += "."
+		} else {
+			return "", fmt.Errorf(`expected "package <package>;", got "package %s%s"`, pkg, s.TokenText())
+		}
+	}
+}
diff --git a/jar/jar_test.go b/jar/jar_test.go
new file mode 100644
index 0000000..c92011e
--- /dev/null
+++ b/jar/jar_test.go
@@ -0,0 +1,182 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package jar
+
+import (
+	"bytes"
+	"io"
+	"testing"
+)
+
+func TestGetJavaPackage(t *testing.T) {
+	type args struct {
+		r   io.Reader
+		src string
+	}
+	tests := []struct {
+		name    string
+		in      string
+		want    string
+		wantErr bool
+	}{
+		{
+			name: "simple",
+			in:   "package foo.bar;",
+			want: "foo.bar",
+		},
+		{
+			name: "comment",
+			in:   "/* test */\npackage foo.bar;",
+			want: "foo.bar",
+		},
+		{
+			name: "no package",
+			in:   "import foo.bar;",
+			want: "",
+		},
+		{
+			name:    "missing semicolon error",
+			in:      "package foo.bar",
+			wantErr: true,
+		},
+		{
+			name:    "parser error",
+			in:      "/*",
+			wantErr: true,
+		},
+		{
+			name:    "parser ident error",
+			in:      "package 0foo.bar;",
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			buf := bytes.NewBufferString(tt.in)
+			got, err := JavaPackage(buf, "<test>")
+			if (err != nil) != tt.wantErr {
+				t.Errorf("JavaPackage() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("JavaPackage() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_javaIdentRune(t *testing.T) {
+	// runes that should be valid anywhere in an identifier
+	validAnywhere := []rune{
+		// letters, $, _
+		'a',
+		'A',
+		'$',
+		'_',
+
+		// assorted unicode
+		'𐐀',
+		'𐐨',
+		'Dž',
+		'ῼ',
+		'ʰ',
+		'゚',
+		'ƻ',
+		'㡢',
+		'₩',
+		'_',
+		'Ⅰ',
+		'𐍊',
+	}
+
+	// runes that should be invalid as the first rune in an identifier, but valid anywhere else
+	validAfterFirst := []rune{
+		// digits
+		'0',
+
+		// assorted unicode
+		'᥍',
+		'𝟎',
+		'ྂ',
+		'𝆀',
+
+		// control characters
+		'\x00',
+		'\b',
+		'\u000e',
+		'\u001b',
+		'\u007f',
+		'\u009f',
+		'\u00ad',
+		0xE007F,
+
+		// zero width space
+		'\u200b',
+	}
+
+	// runes that should never be valid in an identifier
+	invalid := []rune{
+		';',
+		0x110000,
+	}
+
+	validFirst := validAnywhere
+	invalidFirst := append(validAfterFirst, invalid...)
+	validPart := append(validAnywhere, validAfterFirst...)
+	invalidPart := invalid
+
+	check := func(t *testing.T, ch rune, i int, want bool) {
+		t.Helper()
+		if got := javaIdentRune(ch, i); got != want {
+			t.Errorf("javaIdentRune() = %v, want %v", got, want)
+		}
+	}
+
+	t.Run("first", func(t *testing.T) {
+		t.Run("valid", func(t *testing.T) {
+			for _, ch := range validFirst {
+				t.Run(string(ch), func(t *testing.T) {
+					check(t, ch, 0, true)
+				})
+			}
+		})
+
+		t.Run("invalid", func(t *testing.T) {
+			for _, ch := range invalidFirst {
+				t.Run(string(ch), func(t *testing.T) {
+					check(t, ch, 0, false)
+				})
+			}
+		})
+	})
+
+	t.Run("part", func(t *testing.T) {
+		t.Run("valid", func(t *testing.T) {
+			for _, ch := range validPart {
+				t.Run(string(ch), func(t *testing.T) {
+					check(t, ch, 1, true)
+				})
+			}
+		})
+
+		t.Run("invalid", func(t *testing.T) {
+			for _, ch := range invalidPart {
+				t.Run(string(ch), func(t *testing.T) {
+					check(t, ch, 1, false)
+				})
+			}
+		})
+	})
+}
diff --git a/java/aar.go b/java/aar.go
index 1e8e6d8..47f6e5f 100644
--- a/java/aar.go
+++ b/java/aar.go
@@ -188,8 +188,7 @@
 	return linkFlags, linkDeps, resDirs, overlayDirs, rroDirs, resourceZips
 }
 
-func (a *aapt) deps(ctx android.BottomUpMutatorContext, sdkContext sdkContext) {
-	sdkDep := decodeSdkDep(ctx, sdkContext)
+func (a *aapt) deps(ctx android.BottomUpMutatorContext, sdkDep sdkDep) {
 	if sdkDep.frameworkResModule != "" {
 		ctx.AddVariationDependencies(nil, frameworkResTag, sdkDep.frameworkResModule)
 	}
@@ -401,8 +400,9 @@
 
 func (a *AndroidLibrary) DepsMutator(ctx android.BottomUpMutatorContext) {
 	a.Module.deps(ctx)
-	if !Bool(a.properties.No_framework_libs) && !Bool(a.properties.No_standard_libs) {
-		a.aapt.deps(ctx, sdkContext(a))
+	sdkDep := decodeSdkDep(ctx, sdkContext(a))
+	if sdkDep.hasFrameworkLibs() {
+		a.aapt.deps(ctx, sdkDep)
 	}
 }
 
@@ -513,6 +513,10 @@
 	return a.sdkVersion()
 }
 
+func (a *AARImport) noFrameworkLibs() bool {
+	return false
+}
+
 var _ AndroidLibraryDependency = (*AARImport)(nil)
 
 func (a *AARImport) ExportPackage() android.Path {
diff --git a/java/androidmk.go b/java/androidmk.go
index 5491b3e..39c2d13 100644
--- a/java/androidmk.go
+++ b/java/androidmk.go
@@ -97,14 +97,6 @@
 				if library.proguardDictionary != nil {
 					fmt.Fprintln(w, "LOCAL_SOONG_PROGUARD_DICT :=", library.proguardDictionary.String())
 				}
-
-				// Temporary hack: export sources used to compile framework.jar to Make
-				// to be used for droiddoc
-				// TODO(ccross): remove this once droiddoc is in soong
-				if (library.Name() == "framework") || (library.Name() == "framework-annotation-proc") {
-					fmt.Fprintln(w, "SOONG_FRAMEWORK_SRCS :=", strings.Join(library.compiledJavaSrcs.Strings(), " "))
-					fmt.Fprintln(w, "SOONG_FRAMEWORK_SRCJARS :=", strings.Join(library.compiledSrcJars.Strings(), " "))
-				}
 			},
 		},
 		Custom: func(w io.Writer, name, prefix, moduleDir string, data android.AndroidMkData) {
diff --git a/java/app.go b/java/app.go
index cf9354f..cab97de 100644
--- a/java/app.go
+++ b/java/app.go
@@ -159,8 +159,9 @@
 		ctx.PropertyErrorf("stl", "sdk_version must be set in order to use c++_shared")
 	}
 
-	if !Bool(a.properties.No_framework_libs) && !Bool(a.properties.No_standard_libs) {
-		a.aapt.deps(ctx, sdkContext(a))
+	sdkDep := decodeSdkDep(ctx, sdkContext(a))
+	if sdkDep.hasFrameworkLibs() {
+		a.aapt.deps(ctx, sdkDep)
 	}
 
 	embedJni := a.shouldEmbedJnis(ctx)
@@ -180,7 +181,7 @@
 		}
 	}
 
-	a.usesLibrary.deps(ctx, Bool(a.properties.No_framework_libs))
+	a.usesLibrary.deps(ctx, sdkDep.hasFrameworkLibs())
 }
 
 func (a *AndroidApp) OverridablePropertiesDepsMutator(ctx android.BottomUpMutatorContext) {
@@ -783,7 +784,7 @@
 		ctx.AddDependency(ctx.Module(), certificateTag, cert)
 	}
 
-	a.usesLibrary.deps(ctx, false)
+	a.usesLibrary.deps(ctx, true)
 }
 
 func (a *AndroidAppImport) uncompressEmbeddedJniLibs(
@@ -937,17 +938,22 @@
 	usesLibraryProperties UsesLibraryProperties
 }
 
-func (u *usesLibrary) deps(ctx android.BottomUpMutatorContext, noFrameworkLibs bool) {
-	ctx.AddVariationDependencies(nil, usesLibTag, u.usesLibraryProperties.Uses_libs...)
-	ctx.AddVariationDependencies(nil, usesLibTag, u.presentOptionalUsesLibs(ctx)...)
-	if !noFrameworkLibs {
-		// dexpreopt/dexpreopt.go needs the paths to the dex jars of these libraries in case construct_context.sh needs
-		// to pass them to dex2oat.  Add them as a dependency so we can determine the path to the dex jar of each
-		// library to dexpreopt.
-		ctx.AddVariationDependencies(nil, usesLibTag,
-			"org.apache.http.legacy",
-			"android.hidl.base-V1.0-java",
-			"android.hidl.manager-V1.0-java")
+func (u *usesLibrary) deps(ctx android.BottomUpMutatorContext, hasFrameworkLibs bool) {
+	if !ctx.Config().UnbundledBuild() {
+		ctx.AddVariationDependencies(nil, usesLibTag, u.usesLibraryProperties.Uses_libs...)
+		ctx.AddVariationDependencies(nil, usesLibTag, u.presentOptionalUsesLibs(ctx)...)
+		// Only add these extra dependencies if the module depends on framework libs. This avoids
+		// creating a cyclic dependency:
+		//     e.g. framework-res -> org.apache.http.legacy -> ... -> framework-res.
+		if hasFrameworkLibs {
+			// dexpreopt/dexpreopt.go needs the paths to the dex jars of these libraries in case construct_context.sh needs
+			// to pass them to dex2oat.  Add them as a dependency so we can determine the path to the dex jar of each
+			// library to dexpreopt.
+			ctx.AddVariationDependencies(nil, usesLibTag,
+				"org.apache.http.legacy",
+				"android.hidl.base-V1.0-java",
+				"android.hidl.manager-V1.0-java")
+		}
 	}
 }
 
diff --git a/java/app_test.go b/java/app_test.go
index bb39c16..27802cd 100644
--- a/java/app_test.go
+++ b/java/app_test.go
@@ -546,7 +546,7 @@
 	}
 }
 
-func TestJNIABI(t *testing.T) {
+func TestJNIABI_no_framework_libs_true(t *testing.T) {
 	ctx := testJava(t, cc.GatherRequiredDepsForTest(android.Android)+`
 		cc_library {
 			name: "libjni",
@@ -619,7 +619,80 @@
 	}
 }
 
-func TestJNIPackaging(t *testing.T) {
+func TestJNIABI(t *testing.T) {
+	ctx := testJava(t, cc.GatherRequiredDepsForTest(android.Android)+`
+		cc_library {
+			name: "libjni",
+			system_shared_libs: [],
+			stl: "none",
+		}
+
+		android_test {
+			name: "test",
+			sdk_version: "core_platform",
+			jni_libs: ["libjni"],
+		}
+
+		android_test {
+			name: "test_first",
+			sdk_version: "core_platform",
+			compile_multilib: "first",
+			jni_libs: ["libjni"],
+		}
+
+		android_test {
+			name: "test_both",
+			sdk_version: "core_platform",
+			compile_multilib: "both",
+			jni_libs: ["libjni"],
+		}
+
+		android_test {
+			name: "test_32",
+			sdk_version: "core_platform",
+			compile_multilib: "32",
+			jni_libs: ["libjni"],
+		}
+
+		android_test {
+			name: "test_64",
+			sdk_version: "core_platform",
+			compile_multilib: "64",
+			jni_libs: ["libjni"],
+		}
+		`)
+
+	testCases := []struct {
+		name string
+		abis []string
+	}{
+		{"test", []string{"arm64-v8a"}},
+		{"test_first", []string{"arm64-v8a"}},
+		{"test_both", []string{"arm64-v8a", "armeabi-v7a"}},
+		{"test_32", []string{"armeabi-v7a"}},
+		{"test_64", []string{"arm64-v8a"}},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			app := ctx.ModuleForTests(test.name, "android_common")
+			jniLibZip := app.Output("jnilibs.zip")
+			var abis []string
+			args := strings.Fields(jniLibZip.Args["jarArgs"])
+			for i := 0; i < len(args); i++ {
+				if args[i] == "-P" {
+					abis = append(abis, filepath.Base(args[i+1]))
+					i++
+				}
+			}
+			if !reflect.DeepEqual(abis, test.abis) {
+				t.Errorf("want abis %v, got %v", test.abis, abis)
+			}
+		})
+	}
+}
+
+func TestJNIPackaging_no_framework_libs_true(t *testing.T) {
 	ctx := testJava(t, cc.GatherRequiredDepsForTest(android.Android)+`
 		cc_library {
 			name: "libjni",
@@ -700,7 +773,89 @@
 			}
 		})
 	}
+}
 
+func TestJNIPackaging(t *testing.T) {
+	ctx := testJava(t, cc.GatherRequiredDepsForTest(android.Android)+`
+		cc_library {
+			name: "libjni",
+			system_shared_libs: [],
+			stl: "none",
+		}
+
+		android_app {
+			name: "app",
+			jni_libs: ["libjni"],
+		}
+
+		android_app {
+			name: "app_noembed",
+			jni_libs: ["libjni"],
+			use_embedded_native_libs: false,
+		}
+
+		android_app {
+			name: "app_embed",
+			jni_libs: ["libjni"],
+			use_embedded_native_libs: true,
+		}
+
+		android_test {
+			name: "test",
+			sdk_version: "core_platform",
+			jni_libs: ["libjni"],
+		}
+
+		android_test {
+			name: "test_noembed",
+			sdk_version: "core_platform",
+			jni_libs: ["libjni"],
+			use_embedded_native_libs: false,
+		}
+
+		android_test_helper_app {
+			name: "test_helper",
+			sdk_version: "core_platform",
+			jni_libs: ["libjni"],
+		}
+
+		android_test_helper_app {
+			name: "test_helper_noembed",
+			sdk_version: "core_platform",
+			jni_libs: ["libjni"],
+			use_embedded_native_libs: false,
+		}
+		`)
+
+	testCases := []struct {
+		name       string
+		packaged   bool
+		compressed bool
+	}{
+		{"app", false, false},
+		{"app_noembed", false, false},
+		{"app_embed", true, false},
+		{"test", true, false},
+		{"test_noembed", true, true},
+		{"test_helper", true, false},
+		{"test_helper_noembed", true, true},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			app := ctx.ModuleForTests(test.name, "android_common")
+			jniLibZip := app.MaybeOutput("jnilibs.zip")
+			if g, w := (jniLibZip.Rule != nil), test.packaged; g != w {
+				t.Errorf("expected jni packaged %v, got %v", w, g)
+			}
+
+			if jniLibZip.Rule != nil {
+				if g, w := !strings.Contains(jniLibZip.Args["jarArgs"], "-L 0"), test.compressed; g != w {
+					t.Errorf("expected jni compressed %v, got %v", w, g)
+				}
+			}
+		})
+	}
 }
 
 func TestCertificates(t *testing.T) {
diff --git a/java/builder.go b/java/builder.go
index e1a912b..a48e8b1 100644
--- a/java/builder.go
+++ b/java/builder.go
@@ -148,15 +148,16 @@
 }
 
 type javaBuilderFlags struct {
-	javacFlags    string
-	bootClasspath classpath
-	classpath     classpath
-	processorPath classpath
-	processor     string
-	systemModules classpath
-	aidlFlags     string
-	aidlDeps      android.Paths
-	javaVersion   string
+	javacFlags        string
+	bootClasspath     classpath
+	classpath         classpath
+	processorPath     classpath
+	processor         string
+	systemModules     classpath
+	systemModulesDeps android.Paths
+	aidlFlags         string
+	aidlDeps          android.Paths
+	javaVersion       string
 
 	errorProneExtraJavacFlags string
 	errorProneProcessorPath   classpath
@@ -248,7 +249,7 @@
 
 	var bootClasspath string
 	if flags.javaVersion == "1.9" {
-		deps = append(deps, flags.systemModules...)
+		deps = append(deps, flags.systemModulesDeps...)
 		bootClasspath = flags.systemModules.FormJavaSystemModulesPath("--system=", ctx.Device())
 	} else {
 		deps = append(deps, flags.bootClasspath...)
@@ -430,7 +431,7 @@
 	if len(*x) > 1 {
 		panic("more than one system module")
 	} else if len(*x) == 1 {
-		return optName + strings.TrimSuffix((*x)[0].String(), "lib/modules")
+		return optName + (*x)[0].String()
 	} else if forceEmpty {
 		return optName + "none"
 	} else {
diff --git a/java/config/kotlin.go b/java/config/kotlin.go
index 7cea042..fd8e3db 100644
--- a/java/config/kotlin.go
+++ b/java/config/kotlin.go
@@ -32,6 +32,7 @@
 	pctx.SourcePathVariable("KotlinScriptRuntimeJar", "external/kotlinc/lib/kotlin-script-runtime.jar")
 	pctx.SourcePathVariable("KotlinTrove4jJar", "external/kotlinc/lib/trove4j.jar")
 	pctx.SourcePathVariable("KotlinKaptJar", "external/kotlinc/lib/kotlin-annotation-processing.jar")
+	pctx.SourcePathVariable("KotlinAnnotationJar", "external/kotlinc/lib/annotations-13.0.jar")
 	pctx.SourcePathVariable("KotlinStdlibJar", KotlinStdlibJar)
 
 	// These flags silence "Illegal reflective access" warnings when running kotlinc in OpenJDK9
diff --git a/java/dexpreopt.go b/java/dexpreopt.go
index 23d2aa6..ed12fe6 100644
--- a/java/dexpreopt.go
+++ b/java/dexpreopt.go
@@ -132,8 +132,10 @@
 	}
 
 	var images android.Paths
+	var imagesDeps []android.Paths
 	for _, arch := range archs {
 		images = append(images, bootImage.images[arch])
+		imagesDeps = append(imagesDeps, bootImage.imagesDeps[arch])
 	}
 
 	dexLocation := android.InstallPathToOnDevicePath(ctx, d.installPath)
@@ -173,8 +175,9 @@
 		UsesLibraries:                d.usesLibs,
 		LibraryPaths:                 d.libraryPaths,
 
-		Archs:           archs,
-		DexPreoptImages: images,
+		Archs:               archs,
+		DexPreoptImages:     images,
+		DexPreoptImagesDeps: imagesDeps,
 
 		// We use the dex paths and dex locations of the default boot image, as it
 		// contains the full dexpreopt boot classpath. Other images may just contain a subset of
diff --git a/java/dexpreopt_bootjars.go b/java/dexpreopt_bootjars.go
index 2a1a901..eb735c1 100644
--- a/java/dexpreopt_bootjars.go
+++ b/java/dexpreopt_bootjars.go
@@ -58,9 +58,32 @@
 	symbolsDir   android.OutputPath
 	targets      []android.Target
 	images       map[android.ArchType]android.OutputPath
+	imagesDeps   map[android.ArchType]android.Paths
 	zip          android.WritablePath
 }
 
+func (image bootImageConfig) moduleFiles(ctx android.PathContext, dir android.OutputPath, exts ...string) []android.OutputPath {
+	ret := make([]android.OutputPath, 0, len(image.modules)*len(exts))
+
+	// dex preopt on the bootclasspath produces multiple files.  The first dex file
+	// is converted into to 'name'.art (to match the legacy assumption that 'name'.art
+	// exists), and the rest are converted to 'name'-<jar>.art.
+	// In addition, each .art file has an associated .oat and .vdex file, and an
+	// unstripped .oat file
+	for i, m := range image.modules {
+		name := image.name
+		if i != 0 {
+			name += "-" + m
+		}
+
+		for _, ext := range exts {
+			ret = append(ret, dir.Join(ctx, name+ext))
+		}
+	}
+
+	return ret
+}
+
 type bootImage struct {
 	bootImageConfig
 
@@ -302,49 +325,38 @@
 	installDir := filepath.Join("/system/framework", arch.String())
 	vdexInstallDir := filepath.Join("/system/framework")
 
-	var extraFiles android.WritablePaths
 	var vdexInstalls android.RuleBuilderInstalls
 	var unstrippedInstalls android.RuleBuilderInstalls
 
 	var zipFiles android.WritablePaths
 
-	// dex preopt on the bootclasspath produces multiple files.  The first dex file
-	// is converted into to 'name'.art (to match the legacy assumption that 'name'.art
-	// exists), and the rest are converted to 'name'-<jar>.art.
-	// In addition, each .art file has an associated .oat and .vdex file, and an
-	// unstripped .oat file
-	for i, m := range image.modules {
-		name := image.name
-		if i != 0 {
-			name += "-" + m
-		}
+	for _, artOrOat := range image.moduleFiles(ctx, outputDir, ".art", ".oat") {
+		cmd.ImplicitOutput(artOrOat)
+		zipFiles = append(zipFiles, artOrOat)
 
-		art := outputDir.Join(ctx, name+".art")
-		oat := outputDir.Join(ctx, name+".oat")
-		vdex := outputDir.Join(ctx, name+".vdex")
-		unstrippedOat := symbolsDir.Join(ctx, name+".oat")
+		// Install the .oat and .art files
+		rule.Install(artOrOat, filepath.Join(installDir, artOrOat.Base()))
+	}
 
-		extraFiles = append(extraFiles, art, oat, vdex, unstrippedOat)
-
-		zipFiles = append(zipFiles, art, oat, vdex)
-
-		// Install the .oat and .art files.
-		rule.Install(art, filepath.Join(installDir, art.Base()))
-		rule.Install(oat, filepath.Join(installDir, oat.Base()))
+	for _, vdex := range image.moduleFiles(ctx, outputDir, ".vdex") {
+		cmd.ImplicitOutput(vdex)
+		zipFiles = append(zipFiles, vdex)
 
 		// The vdex files are identical between architectures, install them to a shared location.  The Make rules will
 		// only use the install rules for one architecture, and will create symlinks into the architecture-specific
 		// directories.
 		vdexInstalls = append(vdexInstalls,
 			android.RuleBuilderInstall{vdex, filepath.Join(vdexInstallDir, vdex.Base())})
+	}
+
+	for _, unstrippedOat := range image.moduleFiles(ctx, symbolsDir, ".oat") {
+		cmd.ImplicitOutput(unstrippedOat)
 
 		// Install the unstripped oat files.  The Make rules will put these in $(TARGET_OUT_UNSTRIPPED)
 		unstrippedInstalls = append(unstrippedInstalls,
 			android.RuleBuilderInstall{unstrippedOat, filepath.Join(installDir, unstrippedOat.Base())})
 	}
 
-	cmd.ImplicitOutputs(extraFiles)
-
 	rule.Build(pctx, ctx, image.name+"JarsDexpreopt_"+arch.String(), "dexpreopt "+image.name+" jars "+arch.String())
 
 	// save output and installed files for makevars
@@ -496,6 +508,7 @@
 			for _, arch := range arches {
 				ctx.Strict("DEXPREOPT_IMAGE_VDEX_BUILT_INSTALLED_"+current.name+"_"+arch.String(), current.vdexInstalls[arch].String())
 				ctx.Strict("DEXPREOPT_IMAGE_"+current.name+"_"+arch.String(), current.images[arch].String())
+				ctx.Strict("DEXPREOPT_IMAGE_DEPS_"+current.name+"_"+arch.String(), strings.Join(current.imagesDeps[arch].Strings(), " "))
 				ctx.Strict("DEXPREOPT_IMAGE_BUILT_INSTALLED_"+current.name+"_"+arch.String(), current.installs[arch].String())
 				ctx.Strict("DEXPREOPT_IMAGE_UNSTRIPPED_BUILT_INSTALLED_"+current.name+"_"+arch.String(), current.unstrippedInstalls[arch].String())
 				if current.zip != nil {
diff --git a/java/dexpreopt_config.go b/java/dexpreopt_config.go
index d903f45..c396d3e 100644
--- a/java/dexpreopt_config.go
+++ b/java/dexpreopt_config.go
@@ -137,27 +137,35 @@
 
 		dir := android.PathForOutput(ctx, ctx.Config().DeviceName(), "dex_bootjars")
 		symbolsDir := android.PathForOutput(ctx, ctx.Config().DeviceName(), "dex_bootjars_unstripped")
-		images := make(map[android.ArchType]android.OutputPath)
 		zip := dir.Join(ctx, "boot.zip")
 
 		targets := dexpreoptTargets(ctx)
 
-		for _, target := range targets {
-			images[target.Arch.ArchType] = dir.Join(ctx,
-				"system/framework", target.Arch.ArchType.String()).Join(ctx, "boot.art")
-		}
-
-		return bootImageConfig{
+		imageConfig := bootImageConfig{
 			name:         "boot",
 			modules:      nonUpdatableBootModules,
 			dexLocations: nonUpdatableBootLocations,
 			dexPaths:     nonUpdatableBootDexPaths,
 			dir:          dir,
 			symbolsDir:   symbolsDir,
-			images:       images,
+			images:       make(map[android.ArchType]android.OutputPath),
+			imagesDeps:   make(map[android.ArchType]android.Paths),
 			targets:      targets,
 			zip:          zip,
 		}
+
+		for _, target := range targets {
+			imageDir := dir.Join(ctx, "system/framework", target.Arch.ArchType.String())
+			imageConfig.images[target.Arch.ArchType] = imageDir.Join(ctx, "boot.art")
+
+			imagesDeps := make([]android.Path, 0, len(imageConfig.modules)*3)
+			for _, dep := range imageConfig.moduleFiles(ctx, imageDir, ".art", ".oat", ".vdex") {
+				imagesDeps = append(imagesDeps, dep)
+			}
+			imageConfig.imagesDeps[target.Arch.ArchType] = imagesDeps
+		}
+
+		return imageConfig
 	}).(bootImageConfig)
 }
 
@@ -196,16 +204,10 @@
 
 		dir := android.PathForOutput(ctx, ctx.Config().DeviceName(), "dex_apexjars")
 		symbolsDir := android.PathForOutput(ctx, ctx.Config().DeviceName(), "dex_apexjars_unstripped")
-		images := make(map[android.ArchType]android.OutputPath)
 
 		targets := dexpreoptTargets(ctx)
 
-		for _, target := range targets {
-			images[target.Arch.ArchType] = dir.Join(ctx,
-				"system/framework", target.Arch.ArchType.String(), "apex.art")
-		}
-
-		return bootImageConfig{
+		imageConfig := bootImageConfig{
 			name:         "apex",
 			modules:      imageModules,
 			dexLocations: bootLocations,
@@ -213,8 +215,22 @@
 			dir:          dir,
 			symbolsDir:   symbolsDir,
 			targets:      targets,
-			images:       images,
+			images:       make(map[android.ArchType]android.OutputPath),
+			imagesDeps:   make(map[android.ArchType]android.Paths),
 		}
+
+		for _, target := range targets {
+			imageDir := dir.Join(ctx, "system/framework", target.Arch.ArchType.String())
+			imageConfig.images[target.Arch.ArchType] = imageDir.Join(ctx, "apex.art")
+
+			imagesDeps := make([]android.Path, 0, len(imageConfig.modules)*3)
+			for _, dep := range imageConfig.moduleFiles(ctx, imageDir, ".art", ".oat", ".vdex") {
+				imagesDeps = append(imagesDeps, dep)
+			}
+			imageConfig.imagesDeps[target.Arch.ArchType] = imagesDeps
+		}
+
+		return imageConfig
 	}).(bootImageConfig)
 }
 
diff --git a/java/droiddoc.go b/java/droiddoc.go
index 992c8b5..be1b281 100644
--- a/java/droiddoc.go
+++ b/java/droiddoc.go
@@ -171,10 +171,6 @@
 	// list of java libraries that will be in the classpath.
 	Libs []string `android:"arch_variant"`
 
-	// don't build against the default libraries (bootclasspath, ext, and framework for device
-	// targets)
-	No_standard_libs *bool
-
 	// don't build against the framework libraries (ext, and framework for device targets)
 	No_framework_libs *bool
 
@@ -538,16 +534,20 @@
 	return j.sdkVersion()
 }
 
+func (j *Javadoc) noFrameworkLibs() bool {
+	return Bool(j.properties.No_framework_libs)
+}
+
 func (j *Javadoc) addDeps(ctx android.BottomUpMutatorContext) {
 	if ctx.Device() {
-		if !Bool(j.properties.No_standard_libs) {
-			sdkDep := decodeSdkDep(ctx, sdkContext(j))
+		sdkDep := decodeSdkDep(ctx, sdkContext(j))
+		if sdkDep.hasStandardLibs() {
 			if sdkDep.useDefaultLibs {
 				ctx.AddVariationDependencies(nil, bootClasspathTag, config.DefaultBootclasspathLibraries...)
 				if ctx.Config().TargetOpenJDK9() {
 					ctx.AddVariationDependencies(nil, systemModulesTag, config.DefaultSystemModules)
 				}
-				if !Bool(j.properties.No_framework_libs) {
+				if sdkDep.hasFrameworkLibs() {
 					ctx.AddVariationDependencies(nil, libTag, config.DefaultLibraries...)
 				}
 			} else if sdkDep.useModule {
@@ -692,10 +692,11 @@
 				panic("Found two system module dependencies")
 			}
 			sm := module.(*SystemModules)
-			if sm.outputFile == nil {
+			if sm.outputDir == nil && len(sm.outputDeps) == 0 {
 				panic("Missing directory for system module dependency")
 			}
-			deps.systemModules = sm.outputFile
+			deps.systemModules = sm.outputDir
+			deps.systemModulesDeps = sm.outputDeps
 		}
 	})
 	// do not pass exclude_srcs directly when expanding srcFiles since exclude_srcs
@@ -776,6 +777,7 @@
 		if deps.systemModules != nil {
 			systemModules = append(systemModules, deps.systemModules)
 		}
+		implicits = append(implicits, deps.systemModulesDeps...)
 		bootClasspathArgs = systemModules.FormJavaSystemModulesPath("--system ", ctx.Device())
 		bootClasspathArgs = bootClasspathArgs + " --patch-module java.base=."
 	}
diff --git a/java/hiddenapi_singleton.go b/java/hiddenapi_singleton.go
index b1ddab4..cf9f492 100644
--- a/java/hiddenapi_singleton.go
+++ b/java/hiddenapi_singleton.go
@@ -15,11 +15,14 @@
 package java
 
 import (
+	"fmt"
+
 	"android/soong/android"
 )
 
 func init() {
 	android.RegisterSingletonType("hiddenapi", hiddenAPISingletonFactory)
+	android.RegisterModuleType("hiddenapi_flags", hiddenAPIFlagsFactory)
 }
 
 type hiddenAPISingletonPathsStruct struct {
@@ -307,3 +310,48 @@
 		Text("fi").
 		Text(")")
 }
+
+type hiddenAPIFlagsProperties struct {
+	// name of the file into which the flags will be copied.
+	Filename *string
+}
+
+type hiddenAPIFlags struct {
+	android.ModuleBase
+
+	properties hiddenAPIFlagsProperties
+
+	outputFilePath android.OutputPath
+}
+
+func (h *hiddenAPIFlags) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	filename := String(h.properties.Filename)
+
+	inputPath := hiddenAPISingletonPaths(ctx).flags
+	h.outputFilePath = android.PathForModuleOut(ctx, filename).OutputPath
+
+	// This ensures that outputFilePath has the correct name for others to
+	// use, as the source file may have a different name.
+	ctx.Build(pctx, android.BuildParams{
+		Rule:   android.Cp,
+		Output: h.outputFilePath,
+		Input:  inputPath,
+	})
+}
+
+func (h *hiddenAPIFlags) OutputFiles(tag string) (android.Paths, error) {
+	switch tag {
+	case "":
+		return android.Paths{h.outputFilePath}, nil
+	default:
+		return nil, fmt.Errorf("unsupported module reference tag %q", tag)
+	}
+}
+
+// hiddenapi-flags provides access to the hiddenapi-flags.csv file generated during the build.
+func hiddenAPIFlagsFactory() android.Module {
+	module := &hiddenAPIFlags{}
+	module.AddProperties(&module.properties)
+	android.InitAndroidArchModule(module, android.HostAndDeviceSupported, android.MultilibCommon)
+	return module
+}
diff --git a/java/java.go b/java/java.go
index a1addb3..a2e9ab0 100644
--- a/java/java.go
+++ b/java/java.go
@@ -82,10 +82,6 @@
 	// list of files that should be excluded from java_resources and java_resource_dirs
 	Exclude_java_resources []string `android:"path,arch_variant"`
 
-	// don't build against the default libraries (bootclasspath, ext, and framework for device
-	// targets)
-	No_standard_libs *bool
-
 	// don't build against the framework libraries (ext, and framework for device targets)
 	No_framework_libs *bool
 
@@ -440,6 +436,16 @@
 
 	jars android.Paths
 	aidl android.OptionalPath
+
+	noStandardLibs, noFrameworksLibs bool
+}
+
+func (s sdkDep) hasStandardLibs() bool {
+	return !s.noStandardLibs
+}
+
+func (s sdkDep) hasFrameworkLibs() bool {
+	return !s.noStandardLibs && !s.noFrameworksLibs
 }
 
 type jniLib struct {
@@ -476,14 +482,18 @@
 	return j.sdkVersion()
 }
 
+func (j *Module) noFrameworkLibs() bool {
+	return Bool(j.properties.No_framework_libs)
+}
+
 func (j *Module) deps(ctx android.BottomUpMutatorContext) {
 	if ctx.Device() {
-		if !Bool(j.properties.No_standard_libs) {
-			sdkDep := decodeSdkDep(ctx, sdkContext(j))
+		sdkDep := decodeSdkDep(ctx, sdkContext(j))
+		if sdkDep.hasStandardLibs() {
 			if sdkDep.useDefaultLibs {
 				ctx.AddVariationDependencies(nil, bootClasspathTag, config.DefaultBootclasspathLibraries...)
 				ctx.AddVariationDependencies(nil, systemModulesTag, config.DefaultSystemModules)
-				if !Bool(j.properties.No_framework_libs) {
+				if sdkDep.hasFrameworkLibs() {
 					ctx.AddVariationDependencies(nil, libTag, config.DefaultLibraries...)
 				}
 			} else if sdkDep.useModule {
@@ -495,8 +505,8 @@
 				}
 			}
 		} else if j.deviceProperties.System_modules == nil {
-			ctx.PropertyErrorf("no_standard_libs",
-				"system_modules is required to be set when no_standard_libs is true, did you mean no_framework_libs?")
+			ctx.PropertyErrorf("sdk_version",
+				`system_modules is required to be set when sdk_version is "none", did you mean no_framework_libs?`)
 		} else if *j.deviceProperties.System_modules != "none" {
 			ctx.AddVariationDependencies(nil, systemModulesTag, *j.deviceProperties.System_modules)
 		}
@@ -618,6 +628,7 @@
 	srcs               android.Paths
 	srcJars            android.Paths
 	systemModules      android.Path
+	systemModulesDeps  android.Paths
 	aidlPreprocess     android.OptionalPath
 	kotlinStdlib       android.Paths
 	kotlinAnnotations  android.Paths
@@ -664,7 +675,7 @@
 		return javaSdk, true
 	case ver == "current":
 		return javaSdk, false
-	case ver == "":
+	case ver == "" || ver == "none" || ver == "core_platform":
 		return javaPlatform, false
 	default:
 		if _, err := strconv.Atoi(ver); err != nil {
@@ -825,10 +836,11 @@
 					panic("Found two system module dependencies")
 				}
 				sm := module.(*SystemModules)
-				if sm.outputFile == nil {
+				if sm.outputDir == nil || len(sm.outputDeps) == 0 {
 					panic("Missing directory for system module dependency")
 				}
-				deps.systemModules = sm.outputFile
+				deps.systemModules = sm.outputDir
+				deps.systemModulesDeps = sm.outputDeps
 			}
 		}
 	})
@@ -842,7 +854,8 @@
 	var ret string
 	v := sdkContext.sdkVersion()
 	// For PDK builds, use the latest SDK version instead of "current"
-	if ctx.Config().IsPdkBuild() && (v == "" || v == "current") {
+	if ctx.Config().IsPdkBuild() &&
+		(v == "" || v == "none" || v == "core_platform" || v == "current") {
 		sdkVersions := ctx.Config().Get(sdkVersionsKey).([]int)
 		latestSdkVersion := 0
 		if len(sdkVersions) > 0 {
@@ -861,7 +874,11 @@
 		ret = "1.7"
 	} else if ctx.Device() && sdk <= 29 || !ctx.Config().TargetOpenJDK9() {
 		ret = "1.8"
-	} else if ctx.Device() && sdkContext.sdkVersion() != "" && sdk == android.FutureApiLevel {
+	} else if ctx.Device() &&
+		sdkContext.sdkVersion() != "" &&
+		sdkContext.sdkVersion() != "none" &&
+		sdkContext.sdkVersion() != "core_platform" &&
+		sdk == android.FutureApiLevel {
 		// TODO(ccross): once we generate stubs we should be able to use 1.9 for sdk_version: "current"
 		ret = "1.8"
 	} else {
@@ -913,7 +930,7 @@
 	flags.processor = strings.Join(deps.processorClasses, ",")
 
 	if len(flags.bootClasspath) == 0 && ctx.Host() && flags.javaVersion != "1.9" &&
-		!Bool(j.properties.No_standard_libs) &&
+		decodeSdkDep(ctx, sdkContext(j)).hasStandardLibs() &&
 		inList(flags.javaVersion, []string{"1.6", "1.7", "1.8"}) {
 		// Give host-side tools a version of OpenJDK's standard libraries
 		// close to what they're targeting. As of Dec 2017, AOSP is only
@@ -953,6 +970,7 @@
 	// systemModules
 	if deps.systemModules != nil {
 		flags.systemModules = append(flags.systemModules, deps.systemModules)
+		flags.systemModulesDeps = append(flags.systemModulesDeps, deps.systemModulesDeps...)
 	}
 
 	// aidl flags.
diff --git a/java/java_test.go b/java/java_test.go
index 4d161c5..22dec07 100644
--- a/java/java_test.go
+++ b/java/java_test.go
@@ -212,7 +212,7 @@
 	setDexpreoptTestGlobalConfig(config, dexpreopt.GlobalConfigForTests(pathCtx))
 
 	ctx.Register()
-	_, errs := ctx.ParseFileList(".", []string{"Android.bp", "prebuilts/sdk/Android.bp"})
+	_, errs := ctx.ParseBlueprintsFiles("Android.bp")
 	android.FailIfErrored(t, errs)
 	_, errs = ctx.PrepareBuildActions(config)
 	android.FailIfErrored(t, errs)
@@ -842,6 +842,19 @@
 	}
 }
 
+func TestJavaLibrary(t *testing.T) {
+	config := testConfig(nil)
+	ctx := testContext(config, "", map[string][]byte{
+		"libcore/Android.bp": []byte(`
+				java_library {
+						name: "core",
+						sdk_version: "none",
+						system_modules: "none",
+				}`),
+	})
+	run(t, ctx, config)
+}
+
 func TestJavaSdkLibrary(t *testing.T) {
 	ctx := testJava(t, `
 		droiddoc_template {
@@ -1000,7 +1013,7 @@
 		java_library {
 			name: "bar",
 			srcs: ["b.java"],
-			no_standard_libs: true,
+			sdk_version: "none",
 			system_modules: "none",
 			patch_module: "java.base",
 		}
diff --git a/java/kotlin.go b/java/kotlin.go
index 33167ba..8306907 100644
--- a/java/kotlin.go
+++ b/java/kotlin.go
@@ -44,6 +44,7 @@
 			"${config.KotlinScriptRuntimeJar}",
 			"${config.KotlinStdlibJar}",
 			"${config.KotlinTrove4jJar}",
+			"${config.KotlinAnnotationJar}",
 			"${config.GenKotlinBuildFileCmd}",
 			"${config.SoongZipCmd}",
 			"${config.ZipSyncCmd}",
diff --git a/java/robolectric.go b/java/robolectric.go
index b87ee0d..1de56a5 100644
--- a/java/robolectric.go
+++ b/java/robolectric.go
@@ -17,6 +17,7 @@
 import (
 	"fmt"
 	"io"
+	"strconv"
 	"strings"
 
 	"android/soong/android"
@@ -40,6 +41,9 @@
 	Test_options struct {
 		// Timeout in seconds when running the tests.
 		Timeout *int64
+
+		// Number of shards to use when running the tests.
+		Shards *int64
 	}
 }
 
@@ -48,7 +52,8 @@
 
 	robolectricProperties robolectricProperties
 
-	libs []string
+	libs  []string
+	tests []string
 }
 
 func (r *robolectricTest) DepsMutator(ctx android.BottomUpMutatorContext) {
@@ -69,6 +74,39 @@
 	for _, dep := range ctx.GetDirectDepsWithTag(libTag) {
 		r.libs = append(r.libs, ctx.OtherModuleName(dep))
 	}
+
+	// TODO: this could all be removed if tradefed was used as the test runner, it will find everything
+	// annotated as a test and run it.
+	for _, src := range r.compiledJavaSrcs {
+		s := src.Rel()
+		if !strings.HasSuffix(s, "Test.java") {
+			continue
+		} else if strings.HasSuffix(s, "/BaseRobolectricTest.java") {
+			continue
+		} else if strings.HasPrefix(s, "src/") {
+			s = strings.TrimPrefix(s, "src/")
+		}
+		r.tests = append(r.tests, s)
+	}
+}
+
+func shardTests(paths []string, shards int) [][]string {
+	if shards > len(paths) {
+		shards = len(paths)
+	}
+	if shards == 0 {
+		return nil
+	}
+	ret := make([][]string, 0, shards)
+	shardSize := (len(paths) + shards - 1) / shards
+	for len(paths) > shardSize {
+		ret = append(ret, paths[0:shardSize])
+		paths = paths[shardSize:]
+	}
+	if len(paths) > 0 {
+		ret = append(ret, paths)
+	}
+	return ret
 }
 
 func (r *robolectricTest) AndroidMk() android.AndroidMkData {
@@ -77,24 +115,50 @@
 	data.Custom = func(w io.Writer, name, prefix, moduleDir string, data android.AndroidMkData) {
 		android.WriteAndroidMkData(w, data)
 
-		fmt.Fprintln(w, "")
-		fmt.Fprintln(w, "include $(CLEAR_VARS)")
-		fmt.Fprintln(w, "LOCAL_MODULE := Run"+name)
-		fmt.Fprintln(w, "LOCAL_JAVA_LIBRARIES :=", name)
-		fmt.Fprintln(w, "LOCAL_JAVA_LIBRARIES += ", strings.Join(r.libs, " "))
-		fmt.Fprintln(w, "LOCAL_TEST_PACKAGE :=", String(r.robolectricProperties.Instrumentation_for))
-		if t := r.robolectricProperties.Test_options.Timeout; t != nil {
-			fmt.Fprintln(w, "LOCAL_ROBOTEST_TIMEOUT :=", *t)
+		if s := r.robolectricProperties.Test_options.Shards; s != nil && *s > 1 {
+			shards := shardTests(r.tests, int(*s))
+			for i, shard := range shards {
+				r.writeTestRunner(w, name, "Run"+name+strconv.Itoa(i), shard)
+			}
+
+			// TODO: add rules to dist the outputs of the individual tests, or combine them together?
+			fmt.Fprintln(w, "")
+			fmt.Fprintln(w, ".PHONY:", "Run"+name)
+			fmt.Fprintln(w, "Run"+name, ": \\")
+			for i := range shards {
+				fmt.Fprintln(w, "   ", "Run"+name+strconv.Itoa(i), "\\")
+			}
+			fmt.Fprintln(w, "")
+		} else {
+			r.writeTestRunner(w, name, "Run"+name, r.tests)
 		}
-		fmt.Fprintln(w, "-include external/robolectric-shadows/run_robotests.mk")
 	}
 
 	return data
 }
 
+func (r *robolectricTest) writeTestRunner(w io.Writer, module, name string, tests []string) {
+	fmt.Fprintln(w, "")
+	fmt.Fprintln(w, "include $(CLEAR_VARS)")
+	fmt.Fprintln(w, "LOCAL_MODULE :=", name)
+	fmt.Fprintln(w, "LOCAL_JAVA_LIBRARIES :=", module)
+	fmt.Fprintln(w, "LOCAL_JAVA_LIBRARIES += ", strings.Join(r.libs, " "))
+	fmt.Fprintln(w, "LOCAL_TEST_PACKAGE :=", String(r.robolectricProperties.Instrumentation_for))
+	fmt.Fprintln(w, "LOCAL_ROBOTEST_FILES :=", strings.Join(tests, " "))
+	if t := r.robolectricProperties.Test_options.Timeout; t != nil {
+		fmt.Fprintln(w, "LOCAL_ROBOTEST_TIMEOUT :=", *t)
+	}
+	fmt.Fprintln(w, "-include external/robolectric-shadows/run_robotests.mk")
+
+}
+
 // An android_robolectric_test module compiles tests against the Robolectric framework that can run on the local host
 // instead of on a device.  It also generates a rule with the name of the module prefixed with "Run" that can be
 // used to run the tests.  Running the tests with build rule will eventually be deprecated and replaced with atest.
+//
+// The test runner considers any file listed in srcs whose name ends with Test.java to be a test class, unless
+// it is named BaseRobolectricTest.java.  The path to the each source file must exactly match the package
+// name, or match the package name when the prefix "src/" is removed.
 func RobolectricTestFactory() android.Module {
 	module := &robolectricTest{}
 
diff --git a/java/robolectric_test.go b/java/robolectric_test.go
new file mode 100644
index 0000000..e89c6e7
--- /dev/null
+++ b/java/robolectric_test.go
@@ -0,0 +1,88 @@
+// Copyright 2019 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package java
+
+import (
+	"reflect"
+	"testing"
+)
+
+func Test_shardTests(t *testing.T) {
+	type args struct {
+		paths  []string
+		shards int
+	}
+	tests := []struct {
+		name string
+		args args
+		want [][]string
+	}{
+		{
+			name: "empty",
+			args: args{
+				paths:  nil,
+				shards: 1,
+			},
+			want: [][]string(nil),
+		},
+		{
+			name: "too many shards",
+			args: args{
+				paths:  []string{"a", "b"},
+				shards: 3,
+			},
+			want: [][]string{{"a"}, {"b"}},
+		},
+		{
+			name: "single shard",
+			args: args{
+				paths:  []string{"a", "b"},
+				shards: 1,
+			},
+			want: [][]string{{"a", "b"}},
+		},
+		{
+			name: "shard per input",
+			args: args{
+				paths:  []string{"a", "b", "c"},
+				shards: 3,
+			},
+			want: [][]string{{"a"}, {"b"}, {"c"}},
+		},
+		{
+			name: "balanced shards",
+			args: args{
+				paths:  []string{"a", "b", "c", "d"},
+				shards: 2,
+			},
+			want: [][]string{{"a", "b"}, {"c", "d"}},
+		},
+		{
+			name: "unbalanced shards",
+			args: args{
+				paths:  []string{"a", "b", "c"},
+				shards: 2,
+			},
+			want: [][]string{{"a", "b"}, {"c"}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := shardTests(tt.args.paths, tt.args.shards); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("shardTests() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/java/sdk.go b/java/sdk.go
index 90b8fac..6ffe399 100644
--- a/java/sdk.go
+++ b/java/sdk.go
@@ -38,17 +38,20 @@
 var apiFingerprintPathKey = android.NewOnceKey("apiFingerprintPathKey")
 
 type sdkContext interface {
-	// sdkVersion eturns the sdk_version property of the current module, or an empty string if it is not set.
+	// sdkVersion returns the sdk_version property of the current module, or an empty string if it is not set.
 	sdkVersion() string
 	// minSdkVersion returns the min_sdk_version property of the current module, or sdkVersion() if it is not set.
 	minSdkVersion() string
 	// targetSdkVersion returns the target_sdk_version property of the current module, or sdkVersion() if it is not set.
 	targetSdkVersion() string
+
+	// Temporarily provide access to the no_frameworks_libs property (where present).
+	noFrameworkLibs() bool
 }
 
 func sdkVersionOrDefault(ctx android.BaseModuleContext, v string) string {
 	switch v {
-	case "", "current", "system_current", "test_current", "core_current":
+	case "", "none", "current", "test_current", "system_current", "core_current", "core_platform":
 		return ctx.Config().DefaultAppTargetSdk()
 	default:
 		return v
@@ -59,7 +62,7 @@
 // it returns android.FutureApiLevel (10000).
 func sdkVersionToNumber(ctx android.BaseModuleContext, v string) (int, error) {
 	switch v {
-	case "", "current", "test_current", "system_current", "core_current":
+	case "", "none", "current", "test_current", "system_current", "core_current", "core_platform":
 		return ctx.Config().DefaultAppTargetSdkInt(), nil
 	default:
 		n := android.GetNumericSdkVersion(v)
@@ -138,6 +141,9 @@
 			useFiles: true,
 			jars:     android.Paths{jarPath.Path(), lambdaStubsPath},
 			aidl:     android.OptionalPathForPath(aidlPath.Path()),
+
+			// Pass value straight through for now to match previous behavior.
+			noFrameworksLibs: sdkContext.noFrameworkLibs(),
 		}
 	}
 
@@ -148,6 +154,9 @@
 			systemModules:      m + "_system_modules",
 			frameworkResModule: r,
 			aidl:               android.OptionalPathForPath(aidl),
+
+			// Pass value straight through for now to match previous behavior.
+			noFrameworksLibs: sdkContext.noFrameworkLibs(),
 		}
 
 		if m == "core.current.stubs" {
@@ -173,7 +182,8 @@
 		}
 	}
 
-	if ctx.Config().UnbundledBuildUsePrebuiltSdks() && v != "" {
+	if ctx.Config().UnbundledBuildUsePrebuiltSdks() &&
+		v != "" && v != "none" && v != "core_platform" {
 		return toPrebuilt(v)
 	}
 
@@ -182,6 +192,19 @@
 		return sdkDep{
 			useDefaultLibs:     true,
 			frameworkResModule: "framework-res",
+
+			// Pass value straight through for now to match previous behavior.
+			noFrameworksLibs: sdkContext.noFrameworkLibs(),
+		}
+	case "none":
+		return sdkDep{
+			noStandardLibs: true,
+		}
+	case "core_platform":
+		return sdkDep{
+			useDefaultLibs:     true,
+			frameworkResModule: "framework-res",
+			noFrameworksLibs:   true,
 		}
 	case "current":
 		return toModule("android_stubs_current", "framework-res", sdkFrameworkAidlPath(ctx))
diff --git a/java/sdk_library.go b/java/sdk_library.go
index e383533..b4a3f29 100644
--- a/java/sdk_library.go
+++ b/java/sdk_library.go
@@ -159,7 +159,8 @@
 	ctx.AddVariationDependencies(nil, publicApiStubsTag, module.stubsName(apiScopePublic))
 	ctx.AddVariationDependencies(nil, publicApiFileTag, module.docsName(apiScopePublic))
 
-	if !Bool(module.properties.No_standard_libs) {
+	sdkDep := decodeSdkDep(ctx, sdkContext(&module.Library))
+	if sdkDep.hasStandardLibs() {
 		ctx.AddVariationDependencies(nil, systemApiStubsTag, module.stubsName(apiScopeSystem))
 		ctx.AddVariationDependencies(nil, systemApiFileTag, module.docsName(apiScopeSystem))
 		ctx.AddVariationDependencies(nil, testApiFileTag, module.docsName(apiScopeTest))
@@ -384,7 +385,6 @@
 		Device_specific   *bool
 		Product_specific  *bool
 		Compile_dex       *bool
-		No_standard_libs  *bool
 		System_modules    *string
 		Java_version      *string
 		Product_variables struct {
@@ -401,17 +401,22 @@
 		}
 	}{}
 
+	sdkVersion := module.sdkVersion(apiScope)
+	sdkDep := decodeSdkDep(mctx, sdkContext(&module.Library))
+	if !sdkDep.hasStandardLibs() {
+		sdkVersion = "none"
+	}
+
 	props.Name = proptools.StringPtr(module.stubsName(apiScope))
 	// sources are generated from the droiddoc
 	props.Srcs = []string{":" + module.docsName(apiScope)}
-	props.Sdk_version = proptools.StringPtr(module.sdkVersion(apiScope))
+	props.Sdk_version = proptools.StringPtr(sdkVersion)
 	props.Libs = module.sdkLibraryProperties.Stub_only_libs
 	// Unbundled apps will use the prebult one from /prebuilts/sdk
 	if mctx.Config().UnbundledBuildUsePrebuiltSdks() {
 		props.Product_variables.Unbundled_build.Enabled = proptools.BoolPtr(false)
 	}
 	props.Product_variables.Pdk.Enabled = proptools.BoolPtr(false)
-	props.No_standard_libs = module.Library.Module.properties.No_standard_libs
 	props.System_modules = module.Library.Module.deviceProperties.System_modules
 	props.Openjdk9.Srcs = module.Library.Module.properties.Openjdk9.Srcs
 	props.Openjdk9.Javacflags = module.Library.Module.properties.Openjdk9.Javacflags
@@ -441,13 +446,13 @@
 		Srcs_lib                         *string
 		Srcs_lib_whitelist_dirs          []string
 		Srcs_lib_whitelist_pkgs          []string
+		Sdk_version                      *string
 		Libs                             []string
 		Arg_files                        []string
 		Args                             *string
 		Api_tag_name                     *string
 		Api_filename                     *string
 		Removed_api_filename             *string
-		No_standard_libs                 *bool
 		Java_version                     *string
 		Merge_annotations_dirs           []string
 		Merge_inclusion_annotations_dirs []string
@@ -462,9 +467,16 @@
 		}
 	}{}
 
+	sdkDep := decodeSdkDep(mctx, sdkContext(&module.Library))
+	sdkVersion := ""
+	if !sdkDep.hasStandardLibs() {
+		sdkVersion = "none"
+	}
+
 	props.Name = proptools.StringPtr(module.docsName(apiScope))
 	props.Srcs = append(props.Srcs, module.Library.Module.properties.Srcs...)
 	props.Srcs = append(props.Srcs, module.sdkLibraryProperties.Api_srcs...)
+	props.Sdk_version = proptools.StringPtr(sdkVersion)
 	props.Installable = proptools.BoolPtr(false)
 	// A droiddoc module has only one Libs property and doesn't distinguish between
 	// shared libs and static libs. So we need to add both of these libs to Libs property.
@@ -472,7 +484,6 @@
 	props.Libs = append(props.Libs, module.Library.Module.properties.Static_libs...)
 	props.Aidl.Include_dirs = module.Library.Module.deviceProperties.Aidl.Include_dirs
 	props.Aidl.Local_include_dirs = module.Library.Module.deviceProperties.Aidl.Local_include_dirs
-	props.No_standard_libs = module.Library.Module.properties.No_standard_libs
 	props.Java_version = module.Library.Module.properties.Java_version
 
 	props.Merge_annotations_dirs = module.sdkLibraryProperties.Merge_annotations_dirs
@@ -593,7 +604,7 @@
 
 func (module *SdkLibrary) PrebuiltJars(ctx android.BaseModuleContext, sdkVersion string) android.Paths {
 	var api, v string
-	if sdkVersion == "" {
+	if sdkVersion == "" || sdkVersion == "none" {
 		api = "system"
 		v = "current"
 	} else if strings.Contains(sdkVersion, "_") {
@@ -701,7 +712,8 @@
 	module.createStubsLibrary(mctx, apiScopePublic)
 	module.createDocs(mctx, apiScopePublic)
 
-	if !Bool(module.properties.No_standard_libs) {
+	sdkDep := decodeSdkDep(mctx, sdkContext(&module.Library))
+	if sdkDep.hasStandardLibs() {
 		// for system API stubs
 		module.createStubsLibrary(mctx, apiScopeSystem)
 		module.createDocs(mctx, apiScopeSystem)
diff --git a/java/sdk_test.go b/java/sdk_test.go
index cc4da2e..953c372 100644
--- a/java/sdk_test.go
+++ b/java/sdk_test.go
@@ -47,6 +47,22 @@
 			aidl:          "-Iframework/aidl",
 		},
 		{
+			name:          "no_framework_libs:true",
+			properties:    `no_framework_libs:true`,
+			bootclasspath: config.DefaultBootclasspathLibraries,
+			system:        config.DefaultSystemModules,
+			classpath:     []string{},
+			aidl:          "",
+		},
+		{
+			name:          `sdk_version:"core_platform"`,
+			properties:    `sdk_version:"core_platform"`,
+			bootclasspath: config.DefaultBootclasspathLibraries,
+			system:        config.DefaultSystemModules,
+			classpath:     []string{},
+			aidl:          "",
+		},
+		{
 			name:          "blank sdk version",
 			properties:    `sdk_version: "",`,
 			bootclasspath: config.DefaultBootclasspathLibraries,
@@ -106,7 +122,7 @@
 		{
 
 			name:          "nostdlib",
-			properties:    `no_standard_libs: true, system_modules: "none"`,
+			properties:    `sdk_version: "none", system_modules: "none"`,
 			system:        "none",
 			bootclasspath: []string{`""`},
 			classpath:     []string{},
@@ -114,7 +130,7 @@
 		{
 
 			name:          "nostdlib system_modules",
-			properties:    `no_standard_libs: true, system_modules: "core-platform-api-stubs-system-modules"`,
+			properties:    `sdk_version: "none", system_modules: "core-platform-api-stubs-system-modules"`,
 			system:        "core-platform-api-stubs-system-modules",
 			bootclasspath: []string{`""`},
 			classpath:     []string{},
@@ -129,13 +145,6 @@
 			classpath:     []string{},
 		},
 		{
-			name:       "host nostdlib",
-			moduleType: "java_library_host",
-			host:       android.Host,
-			properties: `no_standard_libs: true`,
-			classpath:  []string{},
-		},
-		{
 
 			name:          "host supported default",
 			host:          android.Host,
@@ -146,7 +155,7 @@
 		{
 			name:       "host supported nostdlib",
 			host:       android.Host,
-			properties: `host_supported: true, no_standard_libs: true, system_modules: "none"`,
+			properties: `host_supported: true, sdk_version: "none", system_modules: "none"`,
 			classpath:  []string{},
 		},
 		{
@@ -245,7 +254,7 @@
 			if testcase.system == "none" {
 				system = "--system=none"
 			} else if testcase.system != "" {
-				system = "--system=" + filepath.Join(buildDir, ".intermediates", testcase.system, "android_common", "system") + "/"
+				system = "--system=" + filepath.Join(buildDir, ".intermediates", testcase.system, "android_common", "system")
 			}
 
 			checkClasspath := func(t *testing.T, ctx *android.TestContext) {
diff --git a/java/system_modules.go b/java/system_modules.go
index 2ec3dfb..f71f452 100644
--- a/java/system_modules.go
+++ b/java/system_modules.go
@@ -61,7 +61,7 @@
 		"moduleName", "classpath", "outDir", "workDir")
 )
 
-func TransformJarsToSystemModules(ctx android.ModuleContext, moduleName string, jars android.Paths) android.WritablePath {
+func TransformJarsToSystemModules(ctx android.ModuleContext, moduleName string, jars android.Paths) (android.Path, android.Paths) {
 	outDir := android.PathForModuleOut(ctx, "system")
 	workDir := android.PathForModuleOut(ctx, "modules")
 	outputFile := android.PathForModuleOut(ctx, "system/lib/modules")
@@ -84,7 +84,7 @@
 		},
 	})
 
-	return outputFile
+	return outDir, outputs.Paths()
 }
 
 func SystemModulesFactory() android.Module {
@@ -101,18 +101,13 @@
 
 	properties SystemModulesProperties
 
-	outputFile android.Path
+	outputDir  android.Path
+	outputDeps android.Paths
 }
 
 type SystemModulesProperties struct {
 	// List of java library modules that should be included in the system modules
 	Libs []string
-
-	// List of prebuilt jars that should be included in the system modules
-	Jars []string
-
-	// Sdk version that should be included in the system modules
-	Sdk_version *string
 }
 
 func (system *SystemModules) GenerateAndroidBuildActions(ctx android.ModuleContext) {
@@ -123,9 +118,7 @@
 		jars = append(jars, dep.HeaderJars()...)
 	})
 
-	jars = append(jars, android.PathsForModuleSrc(ctx, system.properties.Jars)...)
-
-	system.outputFile = TransformJarsToSystemModules(ctx, "java.base", jars)
+	system.outputDir, system.outputDeps = TransformJarsToSystemModules(ctx, "java.base", jars)
 }
 
 func (system *SystemModules) DepsMutator(ctx android.BottomUpMutatorContext) {
@@ -135,16 +128,22 @@
 func (system *SystemModules) AndroidMk() android.AndroidMkData {
 	return android.AndroidMkData{
 		Custom: func(w io.Writer, name, prefix, moduleDir string, data android.AndroidMkData) {
-			makevar := "SOONG_SYSTEM_MODULES_" + name
 			fmt.Fprintln(w)
-			fmt.Fprintln(w, makevar, ":=", system.outputFile.String())
-			fmt.Fprintln(w, ".KATI_READONLY", ":=", makevar)
+
+			makevar := "SOONG_SYSTEM_MODULES_" + name
+			fmt.Fprintln(w, makevar, ":=$=", system.outputDir.String())
+			fmt.Fprintln(w)
+
+			makevar = "SOONG_SYSTEM_MODULES_LIBS_" + name
+			fmt.Fprintln(w, makevar, ":=$=", strings.Join(system.properties.Libs, " "))
+			fmt.Fprintln(w)
+
+			makevar = "SOONG_SYSTEM_MODULE_DEPS_" + name
+			fmt.Fprintln(w, makevar, ":=$=", strings.Join(system.outputDeps.Strings(), " "))
+			fmt.Fprintln(w)
+
 			fmt.Fprintln(w, name+":", "$("+makevar+")")
 			fmt.Fprintln(w, ".PHONY:", name)
-			fmt.Fprintln(w)
-			makevar = "SOONG_SYSTEM_MODULES_LIBS_" + name
-			fmt.Fprintln(w, makevar, ":=", strings.Join(system.properties.Libs, " "))
-			fmt.Fprintln(w, ".KATI_READONLY :=", makevar)
 		},
 	}
 }
diff --git a/java/testing.go b/java/testing.go
index a9d4670..5d116a7 100644
--- a/java/testing.go
+++ b/java/testing.go
@@ -54,7 +54,7 @@
 			java_library {
 				name: "%s",
 				srcs: ["a.java"],
-				no_standard_libs: true,
+				sdk_version: "none",
 				system_modules: "core-platform-api-stubs-system-modules",
 			}
 		`, extra)
@@ -64,7 +64,7 @@
 		java_library {
 			name: "framework",
 			srcs: ["a.java"],
-			no_standard_libs: true,
+			sdk_version: "none",
 			system_modules: "core-platform-api-stubs-system-modules",
 			aidl: {
 				export_include_dirs: ["framework/aidl"],
@@ -73,13 +73,13 @@
 
 		android_app {
 			name: "framework-res",
-			no_framework_libs: true,
+			sdk_version: "core_platform",
 		}
 
 		java_library {
 			name: "android.hidl.base-V1.0-java",
 			srcs: ["a.java"],
-			no_standard_libs: true,
+			sdk_version: "none",
 			system_modules: "core-platform-api-stubs-system-modules",
 			installable: true,
 		}
@@ -87,7 +87,7 @@
 		java_library {
 			name: "android.hidl.manager-V1.0-java",
 			srcs: ["a.java"],
-			no_standard_libs: true,
+			sdk_version: "none",
 			system_modules: "core-platform-api-stubs-system-modules",
 			installable: true,
 		}
@@ -95,7 +95,7 @@
 		java_library {
 			name: "org.apache.http.legacy",
 			srcs: ["a.java"],
-			no_standard_libs: true,
+			sdk_version: "none",
 			system_modules: "core-platform-api-stubs-system-modules",
 			installable: true,
 		}
diff --git a/sysprop/sysprop_test.go b/sysprop/sysprop_test.go
index b470ba5..0566036 100644
--- a/sysprop/sysprop_test.go
+++ b/sysprop/sysprop_test.go
@@ -313,13 +313,13 @@
 	vendorVariant := "android_arm64_armv8-a_vendor_static"
 
 	platformInternalPath := "libsysprop-platform/android_arm64_armv8-a_core_static/gen/sysprop/include"
-	platformSystemCorePath := "libsysprop-platform/android_arm64_armv8-a_core_static/gen/sysprop/system/include"
-	platformSystemVendorPath := "libsysprop-platform/android_arm64_armv8-a_vendor_static/gen/sysprop/system/include"
+	platformPublicCorePath := "libsysprop-platform/android_arm64_armv8-a_core_static/gen/sysprop/public/include"
+	platformPublicVendorPath := "libsysprop-platform/android_arm64_armv8-a_vendor_static/gen/sysprop/public/include"
 
-	platformOnProductPath := "libsysprop-platform-on-product/android_arm64_armv8-a_core_static/gen/sysprop/system/include"
+	platformOnProductPath := "libsysprop-platform-on-product/android_arm64_armv8-a_core_static/gen/sysprop/public/include"
 
 	vendorInternalPath := "libsysprop-vendor/android_arm64_armv8-a_vendor_static/gen/sysprop/include"
-	vendorSystemPath := "libsysprop-vendor/android_arm64_armv8-a_core_static/gen/sysprop/system/include"
+	vendorPublicPath := "libsysprop-vendor/android_arm64_armv8-a_core_static/gen/sysprop/public/include"
 
 	platformClient := ctx.ModuleForTests("cc-client-platform", coreVariant)
 	platformFlags := platformClient.Rule("cc").Args["cFlags"]
@@ -342,20 +342,20 @@
 	productClient := ctx.ModuleForTests("cc-client-product", coreVariant)
 	productFlags := productClient.Rule("cc").Args["cFlags"]
 
-	// Product should use platform's and vendor's system headers
+	// Product should use platform's and vendor's public headers
 	if !strings.Contains(productFlags, platformOnProductPath) ||
-		!strings.Contains(productFlags, vendorSystemPath) {
+		!strings.Contains(productFlags, vendorPublicPath) {
 		t.Errorf("flags for product must contain %#v and %#v, but was %#v.",
-			platformSystemCorePath, vendorSystemPath, productFlags)
+			platformPublicCorePath, vendorPublicPath, productFlags)
 	}
 
 	vendorClient := ctx.ModuleForTests("cc-client-vendor", vendorVariant)
 	vendorFlags := vendorClient.Rule("cc").Args["cFlags"]
 
-	// Vendor should use platform's system header and vendor's internal header
-	if !strings.Contains(vendorFlags, platformSystemVendorPath) ||
+	// Vendor should use platform's public header and vendor's internal header
+	if !strings.Contains(vendorFlags, platformPublicVendorPath) ||
 		!strings.Contains(vendorFlags, vendorInternalPath) {
 		t.Errorf("flags for vendor must contain %#v and %#v, but was %#v.",
-			platformSystemVendorPath, vendorInternalPath, vendorFlags)
+			platformPublicVendorPath, vendorInternalPath, vendorFlags)
 	}
 }
diff --git a/tradefed/autogen.go b/tradefed/autogen.go
index da5dabe..952b022 100644
--- a/tradefed/autogen.go
+++ b/tradefed/autogen.go
@@ -16,7 +16,6 @@
 
 import (
 	"fmt"
-	"sort"
 	"strings"
 
 	"github.com/google/blueprint"
@@ -39,9 +38,9 @@
 }
 
 var autogenTestConfig = pctx.StaticRule("autogenTestConfig", blueprint.RuleParams{
-	Command:     "sed 's&{MODULE}&${name}&g;s&{EXTRA_OPTIONS}&'${extraOptions}'&g' $template > $out",
+	Command:     "sed 's&{MODULE}&${name}&g;s&{EXTRA_CONFIGS}&'${extraConfigs}'&g' $template > $out",
 	CommandDeps: []string{"$template"},
-}, "name", "template", "extraOptions")
+}, "name", "template", "extraConfigs")
 
 func testConfigPath(ctx android.ModuleContext, prop *string, testSuites []string) (path android.Path, autogenPath android.WritablePath) {
 	if p := getTestConfig(ctx, prop); p != nil {
@@ -57,17 +56,38 @@
 	}
 }
 
-func autogenTemplate(ctx android.ModuleContext, output android.WritablePath, template string, optionsMap map[string]string) {
-	// If no test option found, delete {EXTRA_OPTIONS} line.
-	var options []string
-	for optionName, value := range optionsMap {
-		if value != "" {
-			options = append(options, fmt.Sprintf(`<option name="%s" value="%s" />`, optionName, value))
-		}
+type Config interface {
+	Config() string
+}
+
+type Option struct {
+	Name  string
+	Value string
+}
+
+var _ Config = Option{}
+
+func (o Option) Config() string {
+	return fmt.Sprintf(`<option name="%s" value="%s" />`, o.Name, o.Value)
+}
+
+type Preparer struct {
+	Class string
+}
+
+var _ Config = Preparer{}
+
+func (p Preparer) Config() string {
+	return fmt.Sprintf(`<target_preparer class="%s" />`, p.Class)
+}
+
+func autogenTemplate(ctx android.ModuleContext, output android.WritablePath, template string, configs []Config) {
+	var configStrings []string
+	for _, config := range configs {
+		configStrings = append(configStrings, config.Config())
 	}
-	sort.Strings(options)
-	extraOptions := strings.Join(options, "\n        ")
-	extraOptions = proptools.NinjaAndShellEscape(extraOptions)
+	extraConfigs := strings.Join(configStrings, "\n        ")
+	extraConfigs = proptools.NinjaAndShellEscape(extraConfigs)
 
 	ctx.Build(pctx, android.BuildParams{
 		Rule:        autogenTestConfig,
@@ -76,26 +96,23 @@
 		Args: map[string]string{
 			"name":         ctx.ModuleName(),
 			"template":     template,
-			"extraOptions": extraOptions,
+			"extraConfigs": extraConfigs,
 		},
 	})
 }
 
 func AutoGenNativeTestConfig(ctx android.ModuleContext, testConfigProp *string,
-	testConfigTemplateProp *string, testSuites []string,
-	optionsMap map[string]string) android.Path {
+	testConfigTemplateProp *string, testSuites []string, config []Config) android.Path {
 	path, autogenPath := testConfigPath(ctx, testConfigProp, testSuites)
 	if autogenPath != nil {
 		templatePath := getTestConfigTemplate(ctx, testConfigTemplateProp)
 		if templatePath.Valid() {
-			autogenTemplate(ctx, autogenPath, templatePath.String(), optionsMap)
+			autogenTemplate(ctx, autogenPath, templatePath.String(), config)
 		} else {
 			if ctx.Device() {
-				autogenTemplate(ctx, autogenPath, "${NativeTestConfigTemplate}",
-					optionsMap)
+				autogenTemplate(ctx, autogenPath, "${NativeTestConfigTemplate}", config)
 			} else {
-				autogenTemplate(ctx, autogenPath, "${NativeHostTestConfigTemplate}",
-					optionsMap)
+				autogenTemplate(ctx, autogenPath, "${NativeHostTestConfigTemplate}", config)
 			}
 		}
 		return autogenPath
@@ -104,14 +121,14 @@
 }
 
 func AutoGenNativeBenchmarkTestConfig(ctx android.ModuleContext, testConfigProp *string,
-	testConfigTemplateProp *string, testSuites []string) android.Path {
+	testConfigTemplateProp *string, testSuites []string, configs []Config) android.Path {
 	path, autogenPath := testConfigPath(ctx, testConfigProp, testSuites)
 	if autogenPath != nil {
 		templatePath := getTestConfigTemplate(ctx, testConfigTemplateProp)
 		if templatePath.Valid() {
-			autogenTemplate(ctx, autogenPath, templatePath.String(), nil)
+			autogenTemplate(ctx, autogenPath, templatePath.String(), configs)
 		} else {
-			autogenTemplate(ctx, autogenPath, "${NativeBenchmarkTestConfigTemplate}", nil)
+			autogenTemplate(ctx, autogenPath, "${NativeBenchmarkTestConfigTemplate}", configs)
 		}
 		return autogenPath
 	}
diff --git a/ui/build/config.go b/ui/build/config.go
index c298f00..6df9529 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -61,6 +61,28 @@
 
 const srcDirFileCheck = "build/soong/root.bp"
 
+type BuildAction uint
+
+const (
+	// Builds all of the modules and their dependencies of a specified directory, relative to the root
+	// directory of the source tree.
+	BUILD_MODULES_IN_A_DIRECTORY BuildAction = iota
+
+	// Builds all of the modules and their dependencies of a list of specified directories. All specified
+	// directories are relative to the root directory of the source tree.
+	BUILD_MODULES_IN_DIRECTORIES
+)
+
+// checkTopDir validates that the current directory is at the root directory of the source tree.
+func checkTopDir(ctx Context) {
+	if _, err := os.Stat(srcDirFileCheck); err != nil {
+		if os.IsNotExist(err) {
+			ctx.Fatalf("Current working directory must be the source tree. %q not found.", srcDirFileCheck)
+		}
+		ctx.Fatalln("Error verifying tree state:", err)
+	}
+}
+
 func NewConfig(ctx Context, args ...string) Config {
 	ret := &configImpl{
 		environ: OsEnvironment(),
@@ -154,12 +176,7 @@
 	ret.environ.Set("TMPDIR", absPath(ctx, ret.TempDir()))
 
 	// Precondition: the current directory is the top of the source tree
-	if _, err := os.Stat(srcDirFileCheck); err != nil {
-		if os.IsNotExist(err) {
-			log.Fatalf("Current working directory must be the source tree. %q not found", srcDirFileCheck)
-		}
-		log.Fatalln("Error verifying tree state:", err)
-	}
+	checkTopDir(ctx)
 
 	if srcDir := absPath(ctx, "."); strings.ContainsRune(srcDir, ' ') {
 		log.Println("You are building in a directory whose absolute path contains a space character:")
@@ -229,6 +246,203 @@
 	return Config{ret}
 }
 
+// NewBuildActionConfig returns a build configuration based on the build action. The arguments are
+// processed based on the build action and extracts any arguments that belongs to the build action.
+func NewBuildActionConfig(action BuildAction, dir string, buildDependencies bool, ctx Context, args ...string) Config {
+	return NewConfig(ctx, getConfigArgs(action, dir, buildDependencies, ctx, args)...)
+}
+
+// getConfigArgs processes the command arguments based on the build action and creates a set of new
+// arguments to be accepted by Config.
+func getConfigArgs(action BuildAction, dir string, buildDependencies bool, ctx Context, args []string) []string {
+	// The next block of code verifies that the current directory is the root directory of the source
+	// tree. It then finds the relative path of dir based on the root directory of the source tree
+	// and verify that dir is inside of the source tree.
+	checkTopDir(ctx)
+	topDir, err := os.Getwd()
+	if err != nil {
+		ctx.Fatalf("Error retrieving top directory: %v", err)
+	}
+	dir, err = filepath.Abs(dir)
+	if err != nil {
+		ctx.Fatalf("Unable to find absolute path %s: %v", dir, err)
+	}
+	relDir, err := filepath.Rel(topDir, dir)
+	if err != nil {
+		ctx.Fatalf("Unable to find relative path %s of %s: %v", relDir, topDir, err)
+	}
+	// If there are ".." in the path, it's not in the source tree.
+	if strings.Contains(relDir, "..") {
+		ctx.Fatalf("Directory %s is not under the source tree %s", dir, topDir)
+	}
+
+	configArgs := args[:]
+
+	// If the arguments contains GET-INSTALL-PATH, change the target name prefix from MODULES-IN- to
+	// GET-INSTALL-PATH-IN- to extract the installation path instead of building the modules.
+	targetNamePrefix := "MODULES-IN-"
+	if inList("GET-INSTALL-PATH", configArgs) {
+		targetNamePrefix = "GET-INSTALL-PATH-IN-"
+		configArgs = removeFromList("GET-INSTALL-PATH", configArgs)
+	}
+
+	var buildFiles []string
+	var targets []string
+
+	switch action {
+	case BUILD_MODULES_IN_A_DIRECTORY:
+		// If dir is the root source tree, all the modules are built of the source tree are built so
+		// no need to find the build file.
+		if topDir == dir {
+			break
+		}
+		// Find the build file from the directory where the build action was triggered by traversing up
+		// the source tree. If a blank build filename is returned, simply use the directory where the build
+		// action was invoked.
+		buildFile := findBuildFile(ctx, relDir)
+		if buildFile == "" {
+			buildFile = filepath.Join(relDir, "Android.mk")
+		}
+		buildFiles = []string{buildFile}
+		targets = []string{convertToTarget(filepath.Dir(buildFile), targetNamePrefix)}
+	case BUILD_MODULES_IN_DIRECTORIES:
+		newConfigArgs, dirs := splitArgs(configArgs)
+		configArgs = newConfigArgs
+		targets, buildFiles = getTargetsFromDirs(ctx, relDir, dirs, targetNamePrefix)
+	}
+
+	// This is to support building modules without building their dependencies. Soon, this will be
+	// deprecated.
+	if !buildDependencies && len(buildFiles) > 0 {
+		if err := os.Setenv("ONE_SHOT_MAKEFILE", strings.Join(buildFiles, " ")); err != nil {
+			ctx.Fatalf("Unable to set ONE_SHOT_MAKEFILE environment variable: %v", err)
+		}
+	}
+
+	// Tidy only override all other specified targets.
+	tidyOnly := os.Getenv("WITH_TIDY_ONLY")
+	if tidyOnly == "true" || tidyOnly == "1" {
+		configArgs = append(configArgs, "tidy_only")
+	} else {
+		configArgs = append(configArgs, targets...)
+	}
+
+	return configArgs
+}
+
+// convertToTarget replaces "/" to "-" in dir and pre-append the targetNamePrefix to the target name.
+func convertToTarget(dir string, targetNamePrefix string) string {
+	return targetNamePrefix + strings.ReplaceAll(dir, "/", "-")
+}
+
+// findBuildFile finds a build file (makefile or blueprint file) by looking at dir first. If not
+// found, go up one level and repeat again until one is found and the path of that build file
+// relative to the root directory of the source tree is returned. The returned filename of build
+// file is "Android.mk". If one was not found, a blank string is returned.
+func findBuildFile(ctx Context, dir string) string {
+	// If the string is empty, assume it is top directory of the source tree.
+	if dir == "" {
+		return ""
+	}
+
+	for ; dir != "."; dir = filepath.Dir(dir) {
+		for _, buildFile := range []string{"Android.bp", "Android.mk"} {
+			_, err := os.Stat(filepath.Join(dir, buildFile))
+			if err == nil {
+				// Returning the filename Android.mk as it might be used for ONE_SHOT_MAKEFILE variable.
+				return filepath.Join(dir, "Android.mk")
+			}
+			if !os.IsNotExist(err) {
+				ctx.Fatalf("Error retrieving the build file stats: %v", err)
+			}
+		}
+	}
+
+	return ""
+}
+
+// splitArgs iterates over the arguments list and splits into two lists: arguments and directories.
+func splitArgs(args []string) (newArgs []string, dirs []string) {
+	specialArgs := map[string]bool{
+		"showcommands": true,
+		"snod":         true,
+		"dist":         true,
+		"checkbuild":   true,
+	}
+
+	newArgs = []string{}
+	dirs = []string{}
+
+	for _, arg := range args {
+		// It's a dash argument if it starts with "-" or it's a key=value pair, it's not a directory.
+		if strings.IndexRune(arg, '-') == 0 || strings.IndexRune(arg, '=') != -1 {
+			newArgs = append(newArgs, arg)
+			continue
+		}
+
+		if _, ok := specialArgs[arg]; ok {
+			newArgs = append(newArgs, arg)
+			continue
+		}
+
+		dirs = append(dirs, arg)
+	}
+
+	return newArgs, dirs
+}
+
+// getTargetsFromDirs iterates over the dirs list and creates a list of targets to build. If a
+// directory from the dirs list does not exist, a fatal error is raised. relDir is related to the
+// source root tree where the build action command was invoked. Each directory is validated if the
+// build file can be found and follows the format "dir1:target1,target2,...". Target is optional.
+func getTargetsFromDirs(ctx Context, relDir string, dirs []string, targetNamePrefix string) (targets []string, buildFiles []string) {
+	for _, dir := range dirs {
+		// The directory may have specified specific modules to build. ":" is the separator to separate
+		// the directory and the list of modules.
+		s := strings.Split(dir, ":")
+		l := len(s)
+		if l > 2 { // more than one ":" was specified.
+			ctx.Fatalf("%s not in proper directory:target1,target2,... format (\":\" was specified more than once)", dir)
+		}
+
+		dir = filepath.Join(relDir, s[0])
+		if _, err := os.Stat(dir); err != nil {
+			ctx.Fatalf("couldn't find directory %s", dir)
+		}
+
+		// Verify that if there are any targets specified after ":". Each target is separated by ",".
+		var newTargets []string
+		if l == 2 && s[1] != "" {
+			newTargets = strings.Split(s[1], ",")
+			if inList("", newTargets) {
+				ctx.Fatalf("%s not in proper directory:target1,target2,... format", dir)
+			}
+		}
+
+		buildFile := findBuildFile(ctx, dir)
+		if buildFile == "" {
+			ctx.Fatalf("Build file not found for %s directory", dir)
+		}
+		buildFileDir := filepath.Dir(buildFile)
+
+		// If there are specified targets, find the build file in the directory. If dir does not
+		// contain the build file, bail out as it is required for one shot build. If there are no
+		// target specified, build all the modules in dir (or the closest one in the dir path).
+		if len(newTargets) > 0 {
+			if buildFileDir != dir {
+				ctx.Fatalf("Couldn't locate a build file from %s directory", dir)
+			}
+		} else {
+			newTargets = []string{convertToTarget(buildFileDir, targetNamePrefix)}
+		}
+
+		buildFiles = append(buildFiles, buildFile)
+		targets = append(targets, newTargets...)
+	}
+
+	return targets, buildFiles
+}
+
 func (c *configImpl) parseArgs(ctx Context, args []string) {
 	for i := 0; i < len(args); i++ {
 		arg := strings.TrimSpace(args[i])
diff --git a/ui/build/config_test.go b/ui/build/config_test.go
index 242e3af..1ef5456 100644
--- a/ui/build/config_test.go
+++ b/ui/build/config_test.go
@@ -17,19 +17,22 @@
 import (
 	"bytes"
 	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
 	"reflect"
 	"strings"
 	"testing"
 
 	"android/soong/ui/logger"
-	"android/soong/ui/terminal"
 )
 
 func testContext() Context {
 	return Context{&ContextImpl{
 		Context: context.Background(),
 		Logger:  logger.New(&bytes.Buffer{}),
-		Writer:  terminal.NewWriter(terminal.NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{})),
+		Writer:  &bytes.Buffer{},
 	}}
 }
 
@@ -173,3 +176,877 @@
 		})
 	}
 }
+
+func TestConfigCheckTopDir(t *testing.T) {
+	ctx := testContext()
+	buildRootDir := filepath.Dir(srcDirFileCheck)
+	expectedErrStr := fmt.Sprintf("Current working directory must be the source tree. %q not found.", srcDirFileCheck)
+
+	tests := []struct {
+		// ********* Setup *********
+		// Test description.
+		description string
+
+		// ********* Action *********
+		// If set to true, the build root file is created.
+		rootBuildFile bool
+
+		// The current path where Soong is being executed.
+		path string
+
+		// ********* Validation *********
+		// Expecting error and validate the error string against expectedErrStr.
+		wantErr bool
+	}{{
+		description:   "current directory is the root source tree",
+		rootBuildFile: true,
+		path:          ".",
+		wantErr:       false,
+	}, {
+		description:   "one level deep in the source tree",
+		rootBuildFile: true,
+		path:          "1",
+		wantErr:       true,
+	}, {
+		description:   "very deep in the source tree",
+		rootBuildFile: true,
+		path:          "1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/7",
+		wantErr:       true,
+	}, {
+		description:   "outside of source tree",
+		rootBuildFile: false,
+		path:          "1/2/3/4/5",
+		wantErr:       true,
+	}}
+
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			defer logger.Recover(func(err error) {
+				if !tt.wantErr {
+					t.Fatalf("Got unexpected error: %v", err)
+				}
+				if expectedErrStr != err.Error() {
+					t.Fatalf("expected %s, got %s", expectedErrStr, err.Error())
+				}
+			})
+
+			// Create the root source tree.
+			rootDir, err := ioutil.TempDir("", "")
+			if err != nil {
+				t.Fatal(err)
+			}
+			defer os.RemoveAll(rootDir)
+
+			// Create the build root file. This is to test if topDir returns an error if the build root
+			// file does not exist.
+			if tt.rootBuildFile {
+				dir := filepath.Join(rootDir, buildRootDir)
+				if err := os.MkdirAll(dir, 0755); err != nil {
+					t.Errorf("failed to create %s directory: %v", dir, err)
+				}
+				f := filepath.Join(rootDir, srcDirFileCheck)
+				if err := ioutil.WriteFile(f, []byte{}, 0644); err != nil {
+					t.Errorf("failed to create file %s: %v", f, err)
+				}
+			}
+
+			// Next block of code is to set the current directory.
+			dir := rootDir
+			if tt.path != "" {
+				dir = filepath.Join(dir, tt.path)
+				if err := os.MkdirAll(dir, 0755); err != nil {
+					t.Errorf("failed to create %s directory: %v", dir, err)
+				}
+			}
+			curDir, err := os.Getwd()
+			if err != nil {
+				t.Fatalf("failed to get the current directory: %v", err)
+			}
+			defer func() { os.Chdir(curDir) }()
+
+			if err := os.Chdir(dir); err != nil {
+				t.Fatalf("failed to change directory to %s: %v", dir, err)
+			}
+
+			checkTopDir(ctx)
+		})
+	}
+}
+
+func TestConfigConvertToTarget(t *testing.T) {
+	tests := []struct {
+		// ********* Setup *********
+		// Test description.
+		description string
+
+		// ********* Action *********
+		// The current directory where Soong is being executed.
+		dir string
+
+		// The current prefix string to be pre-appended to the target.
+		prefix string
+
+		// ********* Validation *********
+		// The expected target to be invoked in ninja.
+		expectedTarget string
+	}{{
+		description:    "one level directory in source tree",
+		dir:            "test1",
+		prefix:         "MODULES-IN-",
+		expectedTarget: "MODULES-IN-test1",
+	}, {
+		description:    "multiple level directories in source tree",
+		dir:            "test1/test2/test3/test4",
+		prefix:         "GET-INSTALL-PATH-IN-",
+		expectedTarget: "GET-INSTALL-PATH-IN-test1-test2-test3-test4",
+	}}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			target := convertToTarget(tt.dir, tt.prefix)
+			if target != tt.expectedTarget {
+				t.Errorf("expected %s, got %s for target", tt.expectedTarget, target)
+			}
+		})
+	}
+}
+
+func setTop(t *testing.T, dir string) func() {
+	curDir, err := os.Getwd()
+	if err != nil {
+		t.Fatalf("failed to get current directory: %v", err)
+	}
+	if err := os.Chdir(dir); err != nil {
+		t.Fatalf("failed to change directory to top dir %s: %v", dir, err)
+	}
+	return func() { os.Chdir(curDir) }
+}
+
+func createBuildFiles(t *testing.T, topDir string, buildFiles []string) {
+	for _, buildFile := range buildFiles {
+		buildFile = filepath.Join(topDir, buildFile)
+		if err := ioutil.WriteFile(buildFile, []byte{}, 0644); err != nil {
+			t.Errorf("failed to create file %s: %v", buildFile, err)
+		}
+	}
+}
+
+func createDirectories(t *testing.T, topDir string, dirs []string) {
+	for _, dir := range dirs {
+		dir = filepath.Join(topDir, dir)
+		if err := os.MkdirAll(dir, 0755); err != nil {
+			t.Errorf("failed to create %s directory: %v", dir, err)
+		}
+	}
+}
+
+func TestConfigGetTargets(t *testing.T) {
+	ctx := testContext()
+	tests := []struct {
+		// ********* Setup *********
+		// Test description.
+		description string
+
+		// Directories that exist in the source tree.
+		dirsInTrees []string
+
+		// Build files that exists in the source tree.
+		buildFiles []string
+
+		// ********* Action *********
+		// Directories passed in to soong_ui.
+		dirs []string
+
+		// Current directory that the user executed the build action command.
+		curDir string
+
+		// ********* Validation *********
+		// Expected targets from the function.
+		expectedTargets []string
+
+		// Expected build from the build system.
+		expectedBuildFiles []string
+
+		// Expecting error from running test case.
+		errStr string
+	}{{
+		description:        "one target dir specified",
+		dirsInTrees:        []string{"0/1/2/3"},
+		buildFiles:         []string{"0/1/2/3/Android.bp"},
+		dirs:               []string{"1/2/3"},
+		curDir:             "0",
+		expectedTargets:    []string{"MODULES-IN-0-1-2-3"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk"},
+	}, {
+		description: "one target dir specified, build file does not exist",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{},
+		dirs:        []string{"1/2/3"},
+		curDir:      "0",
+		errStr:      "Build file not found for 0/1/2/3 directory",
+	}, {
+		description: "one target dir specified, invalid targets specified",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{},
+		dirs:        []string{"1/2/3:t1:t2"},
+		curDir:      "0",
+		errStr:      "1/2/3:t1:t2 not in proper directory:target1,target2,... format (\":\" was specified more than once)",
+	}, {
+		description:        "one target dir specified, no targets specified but has colon",
+		dirsInTrees:        []string{"0/1/2/3"},
+		buildFiles:         []string{"0/1/2/3/Android.bp"},
+		dirs:               []string{"1/2/3:"},
+		curDir:             "0",
+		expectedTargets:    []string{"MODULES-IN-0-1-2-3"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk"},
+	}, {
+		description:        "one target dir specified, two targets specified",
+		dirsInTrees:        []string{"0/1/2/3"},
+		buildFiles:         []string{"0/1/2/3/Android.bp"},
+		dirs:               []string{"1/2/3:t1,t2"},
+		curDir:             "0",
+		expectedTargets:    []string{"t1", "t2"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk"},
+	}, {
+		description: "one target dir specified, no targets and has a comma",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{"0/1/2/3/Android.bp"},
+		dirs:        []string{"1/2/3:,"},
+		curDir:      "0",
+		errStr:      "0/1/2/3 not in proper directory:target1,target2,... format",
+	}, {
+		description: "one target dir specified, improper targets defined",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{"0/1/2/3/Android.bp"},
+		dirs:        []string{"1/2/3:,t1"},
+		curDir:      "0",
+		errStr:      "0/1/2/3 not in proper directory:target1,target2,... format",
+	}, {
+		description: "one target dir specified, blank target",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{"0/1/2/3/Android.bp"},
+		dirs:        []string{"1/2/3:t1,"},
+		curDir:      "0",
+		errStr:      "0/1/2/3 not in proper directory:target1,target2,... format",
+	}, {
+		description:        "one target dir specified, many targets specified",
+		dirsInTrees:        []string{"0/1/2/3"},
+		buildFiles:         []string{"0/1/2/3/Android.bp"},
+		dirs:               []string{"1/2/3:t1,t2,t3,t4,t5,t6,t7,t8,t9,t10"},
+		curDir:             "0",
+		expectedTargets:    []string{"t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8", "t9", "t10"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk"},
+	}, {
+		description: "one target dir specified, one target specified, build file does not exist",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{},
+		dirs:        []string{"1/2/3:t1"},
+		curDir:      "0",
+		errStr:      "Build file not found for 0/1/2/3 directory",
+	}, {
+		description: "one target dir specified, one target specified, build file not in target dir",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{"0/1/2/Android.mk"},
+		dirs:        []string{"1/2/3:t1"},
+		curDir:      "0",
+		errStr:      "Couldn't locate a build file from 0/1/2/3 directory",
+	}, {
+		description:        "one target dir specified, build file not in target dir",
+		dirsInTrees:        []string{"0/1/2/3"},
+		buildFiles:         []string{"0/1/2/Android.mk"},
+		dirs:               []string{"1/2/3"},
+		curDir:             "0",
+		expectedTargets:    []string{"MODULES-IN-0-1-2"},
+		expectedBuildFiles: []string{"0/1/2/Android.mk"},
+	}, {
+		description:        "multiple targets dir specified, targets specified",
+		dirsInTrees:        []string{"0/1/2/3", "0/3/4"},
+		buildFiles:         []string{"0/1/2/3/Android.bp", "0/3/4/Android.mk"},
+		dirs:               []string{"1/2/3:t1,t2", "3/4:t3,t4,t5"},
+		curDir:             "0",
+		expectedTargets:    []string{"t1", "t2", "t3", "t4", "t5"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk", "0/3/4/Android.mk"},
+	}, {
+		description:        "multiple targets dir specified, one directory has targets specified",
+		dirsInTrees:        []string{"0/1/2/3", "0/3/4"},
+		buildFiles:         []string{"0/1/2/3/Android.bp", "0/3/4/Android.mk"},
+		dirs:               []string{"1/2/3:t1,t2", "3/4"},
+		curDir:             "0",
+		expectedTargets:    []string{"t1", "t2", "MODULES-IN-0-3-4"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk", "0/3/4/Android.mk"},
+	}, {
+		description: "two dirs specified, only one dir exist",
+		dirsInTrees: []string{"0/1/2/3"},
+		buildFiles:  []string{"0/1/2/3/Android.mk"},
+		dirs:        []string{"1/2/3:t1", "3/4"},
+		curDir:      "0",
+		errStr:      "couldn't find directory 0/3/4",
+	}, {
+		description:        "multiple targets dirs specified at root source tree",
+		dirsInTrees:        []string{"0/1/2/3", "0/3/4"},
+		buildFiles:         []string{"0/1/2/3/Android.bp", "0/3/4/Android.mk"},
+		dirs:               []string{"0/1/2/3:t1,t2", "0/3/4"},
+		curDir:             ".",
+		expectedTargets:    []string{"t1", "t2", "MODULES-IN-0-3-4"},
+		expectedBuildFiles: []string{"0/1/2/3/Android.mk", "0/3/4/Android.mk"},
+	}, {
+		description: "no directories specified",
+		dirsInTrees: []string{"0/1/2/3", "0/3/4"},
+		buildFiles:  []string{"0/1/2/3/Android.bp", "0/3/4/Android.mk"},
+		dirs:        []string{},
+		curDir:      ".",
+	}}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			defer logger.Recover(func(err error) {
+				if tt.errStr == "" {
+					t.Fatalf("Got unexpected error: %v", err)
+				}
+				if tt.errStr != err.Error() {
+					t.Errorf("expected %s, got %s", tt.errStr, err.Error())
+				}
+			})
+
+			// Create the root source tree.
+			topDir, err := ioutil.TempDir("", "")
+			if err != nil {
+				t.Fatalf("failed to create temp dir: %v", err)
+			}
+			defer os.RemoveAll(topDir)
+
+			createDirectories(t, topDir, tt.dirsInTrees)
+			createBuildFiles(t, topDir, tt.buildFiles)
+			r := setTop(t, topDir)
+			defer r()
+
+			targets, buildFiles := getTargetsFromDirs(ctx, tt.curDir, tt.dirs, "MODULES-IN-")
+			if !reflect.DeepEqual(targets, tt.expectedTargets) {
+				t.Errorf("expected %v, got %v for targets", tt.expectedTargets, targets)
+			}
+			if !reflect.DeepEqual(buildFiles, tt.expectedBuildFiles) {
+				t.Errorf("expected %v, got %v for build files", tt.expectedBuildFiles, buildFiles)
+			}
+
+			// If the execution reached here and there was an expected error code, the unit test case failed.
+			if tt.errStr != "" {
+				t.Errorf("expecting error %s", tt.errStr)
+			}
+		})
+	}
+}
+
+func TestConfigFindBuildFile(t *testing.T) {
+	ctx := testContext()
+
+	tests := []struct {
+		// ********* Setup *********
+		// Test description.
+		description string
+
+		// Array of build files to create in dir.
+		buildFiles []string
+
+		// ********* Action *********
+		// Directory to create, also the base directory is where findBuildFile is invoked.
+		dir string
+
+		// ********* Validation *********
+		// Expected build file path to find.
+		expectedBuildFile string
+	}{{
+		description:       "build file exists at leaf directory",
+		buildFiles:        []string{"1/2/3/Android.bp"},
+		dir:               "1/2/3",
+		expectedBuildFile: "1/2/3/Android.mk",
+	}, {
+		description:       "build file exists in all directory paths",
+		buildFiles:        []string{"1/Android.mk", "1/2/Android.mk", "1/2/3/Android.mk"},
+		dir:               "1/2/3",
+		expectedBuildFile: "1/2/3/Android.mk",
+	}, {
+		description:       "build file does not exist in all directory paths",
+		buildFiles:        []string{},
+		dir:               "1/2/3",
+		expectedBuildFile: "",
+	}, {
+		description:       "build file exists only at top directory",
+		buildFiles:        []string{"Android.bp"},
+		dir:               "1/2/3",
+		expectedBuildFile: "",
+	}, {
+		description:       "build file exist in a subdirectory",
+		buildFiles:        []string{"1/2/Android.bp"},
+		dir:               "1/2/3",
+		expectedBuildFile: "1/2/Android.mk",
+	}, {
+		description:       "build file exists in a subdirectory",
+		buildFiles:        []string{"1/Android.mk"},
+		dir:               "1/2/3",
+		expectedBuildFile: "1/Android.mk",
+	}, {
+		description:       "top directory",
+		buildFiles:        []string{"Android.bp"},
+		dir:               ".",
+		expectedBuildFile: "",
+	}}
+
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			defer logger.Recover(func(err error) {
+				t.Fatalf("Got unexpected error: %v", err)
+			})
+
+			topDir, err := ioutil.TempDir("", "")
+			if err != nil {
+				t.Fatalf("failed to create temp dir: %v", err)
+			}
+			defer os.RemoveAll(topDir)
+
+			if tt.dir != "" {
+				createDirectories(t, topDir, []string{tt.dir})
+			}
+
+			createBuildFiles(t, topDir, tt.buildFiles)
+
+			curDir, err := os.Getwd()
+			if err != nil {
+				t.Fatalf("Could not get working directory: %v", err)
+			}
+			defer func() { os.Chdir(curDir) }()
+			if err := os.Chdir(topDir); err != nil {
+				t.Fatalf("Could not change top dir to %s: %v", topDir, err)
+			}
+
+			buildFile := findBuildFile(ctx, tt.dir)
+			if buildFile != tt.expectedBuildFile {
+				t.Errorf("expected %q, got %q for build file", tt.expectedBuildFile, buildFile)
+			}
+		})
+	}
+}
+
+func TestConfigSplitArgs(t *testing.T) {
+	tests := []struct {
+		// ********* Setup *********
+		// Test description.
+		description string
+
+		// ********* Action *********
+		// Arguments passed in to soong_ui.
+		args []string
+
+		// ********* Validation *********
+		// Expected newArgs list after extracting the directories.
+		expectedNewArgs []string
+
+		// Expected directories
+		expectedDirs []string
+	}{{
+		description:     "flags but no directories specified",
+		args:            []string{"showcommands", "-j", "-k"},
+		expectedNewArgs: []string{"showcommands", "-j", "-k"},
+		expectedDirs:    []string{},
+	}, {
+		description:     "flags and one directory specified",
+		args:            []string{"snod", "-j", "dir:target1,target2"},
+		expectedNewArgs: []string{"snod", "-j"},
+		expectedDirs:    []string{"dir:target1,target2"},
+	}, {
+		description:     "flags and directories specified",
+		args:            []string{"dist", "-k", "dir1", "dir2:target1,target2"},
+		expectedNewArgs: []string{"dist", "-k"},
+		expectedDirs:    []string{"dir1", "dir2:target1,target2"},
+	}, {
+		description:     "only directories specified",
+		args:            []string{"dir1", "dir2", "dir3:target1,target2"},
+		expectedNewArgs: []string{},
+		expectedDirs:    []string{"dir1", "dir2", "dir3:target1,target2"},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			args, dirs := splitArgs(tt.args)
+			if !reflect.DeepEqual(tt.expectedNewArgs, args) {
+				t.Errorf("expected %v, got %v for arguments", tt.expectedNewArgs, args)
+			}
+			if !reflect.DeepEqual(tt.expectedDirs, dirs) {
+				t.Errorf("expected %v, got %v for directories", tt.expectedDirs, dirs)
+			}
+		})
+	}
+}
+
+type envVar struct {
+	name  string
+	value string
+}
+
+type buildActionTestCase struct {
+	// ********* Setup *********
+	// Test description.
+	description string
+
+	// Directories that exist in the source tree.
+	dirsInTrees []string
+
+	// Build files that exists in the source tree.
+	buildFiles []string
+
+	// ********* Action *********
+	// Arguments passed in to soong_ui.
+	args []string
+
+	// Directory where the build action was invoked.
+	curDir string
+
+	// WITH_TIDY_ONLY environment variable specified.
+	tidyOnly string
+
+	// ********* Validation *********
+	// Expected arguments to be in Config instance.
+	expectedArgs []string
+
+	// Expected environment variables to be set.
+	expectedEnvVars []envVar
+}
+
+func testGetConfigArgs(t *testing.T, tt buildActionTestCase, action BuildAction, buildDependencies bool) {
+	ctx := testContext()
+
+	// Environment variables to set it to blank on every test case run.
+	resetEnvVars := []string{
+		"ONE_SHOT_MAKEFILE",
+		"WITH_TIDY_ONLY",
+	}
+
+	for _, name := range resetEnvVars {
+		if err := os.Unsetenv(name); err != nil {
+			t.Fatalf("failed to unset environment variable %s: %v", name, err)
+		}
+	}
+	if tt.tidyOnly != "" {
+		if err := os.Setenv("WITH_TIDY_ONLY", tt.tidyOnly); err != nil {
+			t.Errorf("failed to set WITH_TIDY_ONLY to %s: %v", tt.tidyOnly, err)
+		}
+	}
+
+	// Create the root source tree.
+	topDir, err := ioutil.TempDir("", "")
+	if err != nil {
+		t.Fatalf("failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(topDir)
+
+	createDirectories(t, topDir, tt.dirsInTrees)
+	createBuildFiles(t, topDir, tt.buildFiles)
+
+	r := setTop(t, topDir)
+	defer r()
+
+	// The next block is to create the root build file.
+	rootBuildFileDir := filepath.Dir(srcDirFileCheck)
+	if err := os.MkdirAll(rootBuildFileDir, 0755); err != nil {
+		t.Fatalf("Failed to create %s directory: %v", rootBuildFileDir, err)
+	}
+
+	if err := ioutil.WriteFile(srcDirFileCheck, []byte{}, 0644); err != nil {
+		t.Fatalf("failed to create %s file: %v", srcDirFileCheck, err)
+	}
+
+	args := getConfigArgs(action, tt.curDir, buildDependencies, ctx, tt.args)
+	if !reflect.DeepEqual(tt.expectedArgs, args) {
+		t.Fatalf("expected %v, got %v for config arguments", tt.expectedArgs, args)
+	}
+
+	for _, env := range tt.expectedEnvVars {
+		if val := os.Getenv(env.name); val != env.value {
+			t.Errorf("expecting %s, got %s for environment variable %s", env.value, val, env.name)
+		}
+	}
+}
+
+// TODO: Remove this test case once mm shell build command has been deprecated.
+func TestGetConfigArgsBuildModulesInDirecotoryNoDeps(t *testing.T) {
+	tests := []buildActionTestCase{{
+		description:  "normal execution in a directory",
+		dirsInTrees:  []string{"0/1/2"},
+		buildFiles:   []string{"0/1/2/Android.mk"},
+		args:         []string{"-j", "-k", "showcommands", "fake-module"},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"-j", "-k", "showcommands", "fake-module", "MODULES-IN-0-1-2"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/Android.mk"}},
+	}, {
+		description:  "makefile in parent directory",
+		dirsInTrees:  []string{"0/1/2"},
+		buildFiles:   []string{"0/1/Android.mk"},
+		args:         []string{},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"MODULES-IN-0-1"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/Android.mk"}},
+	}, {
+		description:  "build file not found",
+		dirsInTrees:  []string{"0/1/2"},
+		buildFiles:   []string{},
+		args:         []string{},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"MODULES-IN-0-1-2"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/Android.mk"}},
+	}, {
+		description:  "build action executed at root directory",
+		dirsInTrees:  []string{},
+		buildFiles:   []string{},
+		args:         []string{},
+		curDir:       ".",
+		tidyOnly:     "",
+		expectedArgs: []string{},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: ""}},
+	}, {
+		description:  "GET-INSTALL-PATH specified,",
+		dirsInTrees:  []string{"0/1/2"},
+		buildFiles:   []string{"0/1/Android.mk"},
+		args:         []string{"GET-INSTALL-PATH"},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"GET-INSTALL-PATH-IN-0-1"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/Android.mk"}},
+	}, {
+		description:  "tidy only environment variable specified,",
+		dirsInTrees:  []string{"0/1/2"},
+		buildFiles:   []string{"0/1/Android.mk"},
+		args:         []string{"GET-INSTALL-PATH"},
+		curDir:       "0/1/2",
+		tidyOnly:     "true",
+		expectedArgs: []string{"tidy_only"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/Android.mk"}},
+	}}
+	for _, tt := range tests {
+		t.Run("build action BUILD_MODULES_IN_DIR without their dependencies, "+tt.description, func(t *testing.T) {
+			testGetConfigArgs(t, tt, BUILD_MODULES_IN_A_DIRECTORY, false)
+		})
+	}
+}
+
+func TestGetConfigArgsBuildModulesInDirectory(t *testing.T) {
+	tests := []buildActionTestCase{{
+		description:     "normal execution in a directory",
+		dirsInTrees:     []string{"0/1/2"},
+		buildFiles:      []string{"0/1/2/Android.mk"},
+		args:            []string{"fake-module"},
+		curDir:          "0/1/2",
+		tidyOnly:        "",
+		expectedArgs:    []string{"fake-module", "MODULES-IN-0-1-2"},
+		expectedEnvVars: []envVar{},
+	}, {
+		description:     "build file in parent directory",
+		dirsInTrees:     []string{"0/1/2"},
+		buildFiles:      []string{"0/1/Android.mk"},
+		args:            []string{},
+		curDir:          "0/1/2",
+		tidyOnly:        "",
+		expectedArgs:    []string{"MODULES-IN-0-1"},
+		expectedEnvVars: []envVar{},
+	},
+		{
+			description:     "build file in parent directory, multiple module names passed in",
+			dirsInTrees:     []string{"0/1/2"},
+			buildFiles:      []string{"0/1/Android.mk"},
+			args:            []string{"fake-module1", "fake-module2", "fake-module3"},
+			curDir:          "0/1/2",
+			tidyOnly:        "",
+			expectedArgs:    []string{"fake-module1", "fake-module2", "fake-module3", "MODULES-IN-0-1"},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "build file in 2nd level parent directory",
+			dirsInTrees:     []string{"0/1/2"},
+			buildFiles:      []string{"0/Android.bp"},
+			args:            []string{},
+			curDir:          "0/1/2",
+			tidyOnly:        "",
+			expectedArgs:    []string{"MODULES-IN-0"},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "build action executed at root directory",
+			dirsInTrees:     []string{},
+			buildFiles:      []string{},
+			args:            []string{},
+			curDir:          ".",
+			tidyOnly:        "",
+			expectedArgs:    []string{},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "build file not found - no error is expected to return",
+			dirsInTrees:     []string{"0/1/2"},
+			buildFiles:      []string{},
+			args:            []string{},
+			curDir:          "0/1/2",
+			tidyOnly:        "",
+			expectedArgs:    []string{"MODULES-IN-0-1-2"},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "GET-INSTALL-PATH specified,",
+			dirsInTrees:     []string{"0/1/2"},
+			buildFiles:      []string{"0/1/Android.mk"},
+			args:            []string{"GET-INSTALL-PATH", "-j", "-k", "GET-INSTALL-PATH"},
+			curDir:          "0/1/2",
+			tidyOnly:        "",
+			expectedArgs:    []string{"-j", "-k", "GET-INSTALL-PATH-IN-0-1"},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "tidy only environment variable specified,",
+			dirsInTrees:     []string{"0/1/2"},
+			buildFiles:      []string{"0/1/Android.mk"},
+			args:            []string{"GET-INSTALL-PATH"},
+			curDir:          "0/1/2",
+			tidyOnly:        "true",
+			expectedArgs:    []string{"tidy_only"},
+			expectedEnvVars: []envVar{},
+		}, {
+			description:     "normal execution in root directory with args",
+			dirsInTrees:     []string{},
+			buildFiles:      []string{},
+			args:            []string{"-j", "-k", "fake_module"},
+			curDir:          "",
+			tidyOnly:        "",
+			expectedArgs:    []string{"-j", "-k", "fake_module"},
+			expectedEnvVars: []envVar{},
+		}}
+	for _, tt := range tests {
+		t.Run("build action BUILD_MODULES_IN_DIR, "+tt.description, func(t *testing.T) {
+			testGetConfigArgs(t, tt, BUILD_MODULES_IN_A_DIRECTORY, true)
+		})
+	}
+}
+
+// TODO: Remove this test case once mmm shell build command has been deprecated.
+func TestGetConfigArgsBuildModulesInDirectoriesNoDeps(t *testing.T) {
+	tests := []buildActionTestCase{{
+		description:  "normal execution in a directory",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"3.1/:t1,t2", "3.2/:t3,t4", "3.3/:t5,t6"},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"t1", "t2", "t3", "t4", "t5", "t6"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/3.1/Android.mk 0/1/2/3.2/Android.mk 0/1/2/3.3/Android.mk"}},
+	}, {
+		description:  "GET-INSTALL-PATH specified",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"GET-INSTALL-PATH", "3.1/", "3.2/", "3.3/:t6"},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"GET-INSTALL-PATH-IN-0-1-2-3.1", "GET-INSTALL-PATH-IN-0-1-2-3.2", "t6"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/3.1/Android.mk 0/1/2/3.2/Android.mk 0/1/2/3.3/Android.mk"}},
+	}, {
+		description:  "tidy only environment variable specified",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"GET-INSTALL-PATH", "3.1/", "3.2/", "3.3/:t6"},
+		curDir:       "0/1/2",
+		tidyOnly:     "1",
+		expectedArgs: []string{"tidy_only"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/3.1/Android.mk 0/1/2/3.2/Android.mk 0/1/2/3.3/Android.mk"}},
+	}, {
+		description:  "normal execution from top dir directory",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"0/1/2/3.1", "0/1/2/3.2/:t3,t4", "0/1/2/3.3/:t5,t6"},
+		curDir:       ".",
+		tidyOnly:     "",
+		expectedArgs: []string{"MODULES-IN-0-1-2-3.1", "t3", "t4", "t5", "t6"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: "0/1/2/3.1/Android.mk 0/1/2/3.2/Android.mk 0/1/2/3.3/Android.mk"}},
+	}}
+	for _, tt := range tests {
+		t.Run("build action BUILD_MODULES_IN_DIRS_NO_DEPS, "+tt.description, func(t *testing.T) {
+			testGetConfigArgs(t, tt, BUILD_MODULES_IN_DIRECTORIES, false)
+		})
+	}
+}
+
+func TestGetConfigArgsBuildModulesInDirectories(t *testing.T) {
+	tests := []buildActionTestCase{{
+		description:  "normal execution in a directory",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"3.1/", "3.2/", "3.3/"},
+		curDir:       "0/1/2",
+		tidyOnly:     "",
+		expectedArgs: []string{"MODULES-IN-0-1-2-3.1", "MODULES-IN-0-1-2-3.2", "MODULES-IN-0-1-2-3.3"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: ""}},
+	}, {
+		description:  "GET-INSTALL-PATH specified",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/Android.bp"},
+		args:         []string{"GET-INSTALL-PATH", "2/3.1/", "2/3.2", "3"},
+		curDir:       "0/1",
+		tidyOnly:     "",
+		expectedArgs: []string{"GET-INSTALL-PATH-IN-0-1-2-3.1", "GET-INSTALL-PATH-IN-0-1-2-3.2", "GET-INSTALL-PATH-IN-0-1"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: ""}},
+	}, {
+		description:  "tidy only environment variable specified",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/2/3.3"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/2/3.3/Android.bp"},
+		args:         []string{"GET-INSTALL-PATH", "3.1/", "3.2/", "3.3"},
+		curDir:       "0/1/2",
+		tidyOnly:     "1",
+		expectedArgs: []string{"tidy_only"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: ""}},
+	}, {
+		description:  "normal execution from top dir directory",
+		dirsInTrees:  []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/3", "0/2"},
+		buildFiles:   []string{"0/1/2/3.1/Android.bp", "0/1/2/3.2/Android.bp", "0/1/3/Android.bp", "0/2/Android.bp"},
+		args:         []string{"0/1/2/3.1", "0/1/2/3.2", "0/1/3", "0/2"},
+		curDir:       ".",
+		tidyOnly:     "",
+		expectedArgs: []string{"MODULES-IN-0-1-2-3.1", "MODULES-IN-0-1-2-3.2", "MODULES-IN-0-1-3", "MODULES-IN-0-2"},
+		expectedEnvVars: []envVar{
+			envVar{
+				name:  "ONE_SHOT_MAKEFILE",
+				value: ""}},
+	}}
+	for _, tt := range tests {
+		t.Run("build action BUILD_MODULES_IN_DIRS, "+tt.description, func(t *testing.T) {
+			testGetConfigArgs(t, tt, BUILD_MODULES_IN_DIRECTORIES, true)
+		})
+	}
+}
diff --git a/ui/build/context.go b/ui/build/context.go
index 249e898..3945ce0 100644
--- a/ui/build/context.go
+++ b/ui/build/context.go
@@ -16,12 +16,12 @@
 
 import (
 	"context"
+	"io"
 
 	"android/soong/ui/logger"
 	"android/soong/ui/metrics"
 	"android/soong/ui/metrics/metrics_proto"
 	"android/soong/ui/status"
-	"android/soong/ui/terminal"
 	"android/soong/ui/tracer"
 )
 
@@ -35,7 +35,7 @@
 
 	Metrics *metrics.Metrics
 
-	Writer terminal.Writer
+	Writer io.Writer
 	Status *status.Status
 
 	Thread tracer.Thread
@@ -70,7 +70,7 @@
 	if c.Metrics != nil {
 		realTime := end - begin
 		c.Metrics.SetTimeMetrics(
-			metrics_proto.PerfInfo{
+			soong_metrics_proto.PerfInfo{
 				Desc:      &desc,
 				Name:      &name,
 				StartTime: &begin,
diff --git a/ui/build/dumpvars.go b/ui/build/dumpvars.go
index 4335667..266130f 100644
--- a/ui/build/dumpvars.go
+++ b/ui/build/dumpvars.go
@@ -249,7 +249,7 @@
 	env := config.Environment()
 	// Print the banner like make does
 	if !env.IsEnvTrue("ANDROID_QUIET_BUILD") {
-		ctx.Writer.Print(Banner(make_vars))
+		fmt.Fprintln(ctx.Writer, Banner(make_vars))
 	}
 
 	// Populate the environment
diff --git a/ui/build/paths/config.go b/ui/build/paths/config.go
index a50a880..c9946e2 100644
--- a/ui/build/paths/config.go
+++ b/ui/build/paths/config.go
@@ -103,7 +103,6 @@
 	"rsync":    Allowed,
 	"sh":       Allowed,
 	"tar":      Allowed,
-	"timeout":  Allowed,
 	"tr":       Allowed,
 	"unzip":    Allowed,
 	"zip":      Allowed,
@@ -166,6 +165,7 @@
 	"stat":      LinuxOnlyPrebuilt,
 	"tail":      LinuxOnlyPrebuilt,
 	"tee":       LinuxOnlyPrebuilt,
+	"timeout":   LinuxOnlyPrebuilt,
 	"touch":     LinuxOnlyPrebuilt,
 	"true":      LinuxOnlyPrebuilt,
 	"uname":     LinuxOnlyPrebuilt,
diff --git a/ui/build/util.go b/ui/build/util.go
index 0676a86..75e6753 100644
--- a/ui/build/util.go
+++ b/ui/build/util.go
@@ -44,6 +44,17 @@
 	return indexList(s, list) != -1
 }
 
+// removeFromlist removes all occurrences of the string in list.
+func removeFromList(s string, list []string) []string {
+	filteredList := make([]string, 0, len(list))
+	for _, ls := range list {
+		if s != ls {
+			filteredList = append(filteredList, ls)
+		}
+	}
+	return filteredList
+}
+
 // ensureDirectoriesExist is a shortcut to os.MkdirAll, sending errors to the ctx logger.
 func ensureDirectoriesExist(ctx Context, dirs ...string) {
 	for _, dir := range dirs {
diff --git a/ui/metrics/metrics.go b/ui/metrics/metrics.go
index 790b67a..bc86f0a 100644
--- a/ui/metrics/metrics.go
+++ b/ui/metrics/metrics.go
@@ -33,19 +33,19 @@
 )
 
 type Metrics struct {
-	metrics    metrics_proto.MetricsBase
+	metrics    soong_metrics_proto.MetricsBase
 	TimeTracer TimeTracer
 }
 
 func New() (metrics *Metrics) {
 	m := &Metrics{
-		metrics:    metrics_proto.MetricsBase{},
+		metrics:    soong_metrics_proto.MetricsBase{},
 		TimeTracer: &timeTracerImpl{},
 	}
 	return m
 }
 
-func (m *Metrics) SetTimeMetrics(perf metrics_proto.PerfInfo) {
+func (m *Metrics) SetTimeMetrics(perf soong_metrics_proto.PerfInfo) {
 	switch perf.GetName() {
 	case RunKati:
 		m.metrics.KatiRuns = append(m.metrics.KatiRuns, &perf)
@@ -76,11 +76,11 @@
 		case "TARGET_BUILD_VARIANT":
 			switch v {
 			case "user":
-				m.metrics.TargetBuildVariant = metrics_proto.MetricsBase_USER.Enum()
+				m.metrics.TargetBuildVariant = soong_metrics_proto.MetricsBase_USER.Enum()
 			case "userdebug":
-				m.metrics.TargetBuildVariant = metrics_proto.MetricsBase_USERDEBUG.Enum()
+				m.metrics.TargetBuildVariant = soong_metrics_proto.MetricsBase_USERDEBUG.Enum()
 			case "eng":
-				m.metrics.TargetBuildVariant = metrics_proto.MetricsBase_ENG.Enum()
+				m.metrics.TargetBuildVariant = soong_metrics_proto.MetricsBase_ENG.Enum()
 			default:
 				// ignored
 			}
@@ -112,18 +112,18 @@
 	}
 }
 
-func (m *Metrics) getArch(arch string) *metrics_proto.MetricsBase_ARCH {
+func (m *Metrics) getArch(arch string) *soong_metrics_proto.MetricsBase_Arch {
 	switch arch {
 	case "arm":
-		return metrics_proto.MetricsBase_ARM.Enum()
+		return soong_metrics_proto.MetricsBase_ARM.Enum()
 	case "arm64":
-		return metrics_proto.MetricsBase_ARM64.Enum()
+		return soong_metrics_proto.MetricsBase_ARM64.Enum()
 	case "x86":
-		return metrics_proto.MetricsBase_X86.Enum()
+		return soong_metrics_proto.MetricsBase_X86.Enum()
 	case "x86_64":
-		return metrics_proto.MetricsBase_X86_64.Enum()
+		return soong_metrics_proto.MetricsBase_X86_64.Enum()
 	default:
-		return metrics_proto.MetricsBase_UNKNOWN.Enum()
+		return soong_metrics_proto.MetricsBase_UNKNOWN.Enum()
 	}
 }
 
@@ -148,7 +148,7 @@
 		return err
 	}
 	tempPath := outputPath + ".tmp"
-	err = ioutil.WriteFile(tempPath, []byte(data), 0777)
+	err = ioutil.WriteFile(tempPath, []byte(data), 0644)
 	if err != nil {
 		return err
 	}
diff --git a/ui/metrics/metrics_proto/metrics.pb.go b/ui/metrics/metrics_proto/metrics.pb.go
index feefc89..5486ec1 100644
--- a/ui/metrics/metrics_proto/metrics.pb.go
+++ b/ui/metrics/metrics_proto/metrics.pb.go
@@ -1,11 +1,13 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // source: metrics.proto
 
-package metrics_proto
+package soong_metrics_proto
 
-import proto "github.com/golang/protobuf/proto"
-import fmt "fmt"
-import math "math"
+import (
+	fmt "fmt"
+	proto "github.com/golang/protobuf/proto"
+	math "math"
+)
 
 // Reference imports to suppress errors if they are not otherwise used.
 var _ = proto.Marshal
@@ -16,65 +18,70 @@
 // is compatible with the proto package it is being compiled against.
 // A compilation error at this line likely means your copy of the
 // proto package needs to be updated.
-const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
+const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
 
-type MetricsBase_BUILDVARIANT int32
+type MetricsBase_BuildVariant int32
 
 const (
-	MetricsBase_USER      MetricsBase_BUILDVARIANT = 0
-	MetricsBase_USERDEBUG MetricsBase_BUILDVARIANT = 1
-	MetricsBase_ENG       MetricsBase_BUILDVARIANT = 2
+	MetricsBase_USER      MetricsBase_BuildVariant = 0
+	MetricsBase_USERDEBUG MetricsBase_BuildVariant = 1
+	MetricsBase_ENG       MetricsBase_BuildVariant = 2
 )
 
-var MetricsBase_BUILDVARIANT_name = map[int32]string{
+var MetricsBase_BuildVariant_name = map[int32]string{
 	0: "USER",
 	1: "USERDEBUG",
 	2: "ENG",
 }
-var MetricsBase_BUILDVARIANT_value = map[string]int32{
+
+var MetricsBase_BuildVariant_value = map[string]int32{
 	"USER":      0,
 	"USERDEBUG": 1,
 	"ENG":       2,
 }
 
-func (x MetricsBase_BUILDVARIANT) Enum() *MetricsBase_BUILDVARIANT {
-	p := new(MetricsBase_BUILDVARIANT)
+func (x MetricsBase_BuildVariant) Enum() *MetricsBase_BuildVariant {
+	p := new(MetricsBase_BuildVariant)
 	*p = x
 	return p
 }
-func (x MetricsBase_BUILDVARIANT) String() string {
-	return proto.EnumName(MetricsBase_BUILDVARIANT_name, int32(x))
+
+func (x MetricsBase_BuildVariant) String() string {
+	return proto.EnumName(MetricsBase_BuildVariant_name, int32(x))
 }
-func (x *MetricsBase_BUILDVARIANT) UnmarshalJSON(data []byte) error {
-	value, err := proto.UnmarshalJSONEnum(MetricsBase_BUILDVARIANT_value, data, "MetricsBase_BUILDVARIANT")
+
+func (x *MetricsBase_BuildVariant) UnmarshalJSON(data []byte) error {
+	value, err := proto.UnmarshalJSONEnum(MetricsBase_BuildVariant_value, data, "MetricsBase_BuildVariant")
 	if err != nil {
 		return err
 	}
-	*x = MetricsBase_BUILDVARIANT(value)
+	*x = MetricsBase_BuildVariant(value)
 	return nil
 }
-func (MetricsBase_BUILDVARIANT) EnumDescriptor() ([]byte, []int) {
-	return fileDescriptor_metrics_9e7b895801991242, []int{0, 0}
+
+func (MetricsBase_BuildVariant) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor_6039342a2ba47b72, []int{0, 0}
 }
 
-type MetricsBase_ARCH int32
+type MetricsBase_Arch int32
 
 const (
-	MetricsBase_UNKNOWN MetricsBase_ARCH = 0
-	MetricsBase_ARM     MetricsBase_ARCH = 1
-	MetricsBase_ARM64   MetricsBase_ARCH = 2
-	MetricsBase_X86     MetricsBase_ARCH = 3
-	MetricsBase_X86_64  MetricsBase_ARCH = 4
+	MetricsBase_UNKNOWN MetricsBase_Arch = 0
+	MetricsBase_ARM     MetricsBase_Arch = 1
+	MetricsBase_ARM64   MetricsBase_Arch = 2
+	MetricsBase_X86     MetricsBase_Arch = 3
+	MetricsBase_X86_64  MetricsBase_Arch = 4
 )
 
-var MetricsBase_ARCH_name = map[int32]string{
+var MetricsBase_Arch_name = map[int32]string{
 	0: "UNKNOWN",
 	1: "ARM",
 	2: "ARM64",
 	3: "X86",
 	4: "X86_64",
 }
-var MetricsBase_ARCH_value = map[string]int32{
+
+var MetricsBase_Arch_value = map[string]int32{
 	"UNKNOWN": 0,
 	"ARM":     1,
 	"ARM64":   2,
@@ -82,63 +89,70 @@
 	"X86_64":  4,
 }
 
-func (x MetricsBase_ARCH) Enum() *MetricsBase_ARCH {
-	p := new(MetricsBase_ARCH)
+func (x MetricsBase_Arch) Enum() *MetricsBase_Arch {
+	p := new(MetricsBase_Arch)
 	*p = x
 	return p
 }
-func (x MetricsBase_ARCH) String() string {
-	return proto.EnumName(MetricsBase_ARCH_name, int32(x))
+
+func (x MetricsBase_Arch) String() string {
+	return proto.EnumName(MetricsBase_Arch_name, int32(x))
 }
-func (x *MetricsBase_ARCH) UnmarshalJSON(data []byte) error {
-	value, err := proto.UnmarshalJSONEnum(MetricsBase_ARCH_value, data, "MetricsBase_ARCH")
+
+func (x *MetricsBase_Arch) UnmarshalJSON(data []byte) error {
+	value, err := proto.UnmarshalJSONEnum(MetricsBase_Arch_value, data, "MetricsBase_Arch")
 	if err != nil {
 		return err
 	}
-	*x = MetricsBase_ARCH(value)
+	*x = MetricsBase_Arch(value)
 	return nil
 }
-func (MetricsBase_ARCH) EnumDescriptor() ([]byte, []int) {
-	return fileDescriptor_metrics_9e7b895801991242, []int{0, 1}
+
+func (MetricsBase_Arch) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor_6039342a2ba47b72, []int{0, 1}
 }
 
-type ModuleTypeInfo_BUILDSYSTEM int32
+type ModuleTypeInfo_BuildSystem int32
 
 const (
-	ModuleTypeInfo_UNKNOWN ModuleTypeInfo_BUILDSYSTEM = 0
-	ModuleTypeInfo_SOONG   ModuleTypeInfo_BUILDSYSTEM = 1
-	ModuleTypeInfo_MAKE    ModuleTypeInfo_BUILDSYSTEM = 2
+	ModuleTypeInfo_UNKNOWN ModuleTypeInfo_BuildSystem = 0
+	ModuleTypeInfo_SOONG   ModuleTypeInfo_BuildSystem = 1
+	ModuleTypeInfo_MAKE    ModuleTypeInfo_BuildSystem = 2
 )
 
-var ModuleTypeInfo_BUILDSYSTEM_name = map[int32]string{
+var ModuleTypeInfo_BuildSystem_name = map[int32]string{
 	0: "UNKNOWN",
 	1: "SOONG",
 	2: "MAKE",
 }
-var ModuleTypeInfo_BUILDSYSTEM_value = map[string]int32{
+
+var ModuleTypeInfo_BuildSystem_value = map[string]int32{
 	"UNKNOWN": 0,
 	"SOONG":   1,
 	"MAKE":    2,
 }
 
-func (x ModuleTypeInfo_BUILDSYSTEM) Enum() *ModuleTypeInfo_BUILDSYSTEM {
-	p := new(ModuleTypeInfo_BUILDSYSTEM)
+func (x ModuleTypeInfo_BuildSystem) Enum() *ModuleTypeInfo_BuildSystem {
+	p := new(ModuleTypeInfo_BuildSystem)
 	*p = x
 	return p
 }
-func (x ModuleTypeInfo_BUILDSYSTEM) String() string {
-	return proto.EnumName(ModuleTypeInfo_BUILDSYSTEM_name, int32(x))
+
+func (x ModuleTypeInfo_BuildSystem) String() string {
+	return proto.EnumName(ModuleTypeInfo_BuildSystem_name, int32(x))
 }
-func (x *ModuleTypeInfo_BUILDSYSTEM) UnmarshalJSON(data []byte) error {
-	value, err := proto.UnmarshalJSONEnum(ModuleTypeInfo_BUILDSYSTEM_value, data, "ModuleTypeInfo_BUILDSYSTEM")
+
+func (x *ModuleTypeInfo_BuildSystem) UnmarshalJSON(data []byte) error {
+	value, err := proto.UnmarshalJSONEnum(ModuleTypeInfo_BuildSystem_value, data, "ModuleTypeInfo_BuildSystem")
 	if err != nil {
 		return err
 	}
-	*x = ModuleTypeInfo_BUILDSYSTEM(value)
+	*x = ModuleTypeInfo_BuildSystem(value)
 	return nil
 }
-func (ModuleTypeInfo_BUILDSYSTEM) EnumDescriptor() ([]byte, []int) {
-	return fileDescriptor_metrics_9e7b895801991242, []int{2, 0}
+
+func (ModuleTypeInfo_BuildSystem) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor_6039342a2ba47b72, []int{2, 0}
 }
 
 type MetricsBase struct {
@@ -151,17 +165,17 @@
 	// The target product information, eg. aosp_arm.
 	TargetProduct *string `protobuf:"bytes,4,opt,name=target_product,json=targetProduct" json:"target_product,omitempty"`
 	// The target build variant information, eg. eng.
-	TargetBuildVariant *MetricsBase_BUILDVARIANT `protobuf:"varint,5,opt,name=target_build_variant,json=targetBuildVariant,enum=build_metrics.MetricsBase_BUILDVARIANT,def=2" json:"target_build_variant,omitempty"`
+	TargetBuildVariant *MetricsBase_BuildVariant `protobuf:"varint,5,opt,name=target_build_variant,json=targetBuildVariant,enum=soong_build_metrics.MetricsBase_BuildVariant,def=2" json:"target_build_variant,omitempty"`
 	// The target arch information, eg. arm.
-	TargetArch *MetricsBase_ARCH `protobuf:"varint,6,opt,name=target_arch,json=targetArch,enum=build_metrics.MetricsBase_ARCH,def=0" json:"target_arch,omitempty"`
+	TargetArch *MetricsBase_Arch `protobuf:"varint,6,opt,name=target_arch,json=targetArch,enum=soong_build_metrics.MetricsBase_Arch,def=0" json:"target_arch,omitempty"`
 	// The target arch variant information, eg. armv7-a-neon.
 	TargetArchVariant *string `protobuf:"bytes,7,opt,name=target_arch_variant,json=targetArchVariant" json:"target_arch_variant,omitempty"`
 	// The target cpu variant information, eg. generic.
 	TargetCpuVariant *string `protobuf:"bytes,8,opt,name=target_cpu_variant,json=targetCpuVariant" json:"target_cpu_variant,omitempty"`
 	// The host arch information, eg. x86_64.
-	HostArch *MetricsBase_ARCH `protobuf:"varint,9,opt,name=host_arch,json=hostArch,enum=build_metrics.MetricsBase_ARCH,def=0" json:"host_arch,omitempty"`
+	HostArch *MetricsBase_Arch `protobuf:"varint,9,opt,name=host_arch,json=hostArch,enum=soong_build_metrics.MetricsBase_Arch,def=0" json:"host_arch,omitempty"`
 	// The host 2nd arch information, eg. x86.
-	Host_2NdArch *MetricsBase_ARCH `protobuf:"varint,10,opt,name=host_2nd_arch,json=host2ndArch,enum=build_metrics.MetricsBase_ARCH,def=0" json:"host_2nd_arch,omitempty"`
+	Host_2NdArch *MetricsBase_Arch `protobuf:"varint,10,opt,name=host_2nd_arch,json=host2ndArch,enum=soong_build_metrics.MetricsBase_Arch,def=0" json:"host_2nd_arch,omitempty"`
 	// The host os information, eg. linux.
 	HostOs *string `protobuf:"bytes,11,opt,name=host_os,json=hostOs" json:"host_os,omitempty"`
 	// The host os extra information, eg. Linux-4.17.0-3rodete2-amd64-x86_64-Debian-GNU.
@@ -191,16 +205,17 @@
 func (m *MetricsBase) String() string { return proto.CompactTextString(m) }
 func (*MetricsBase) ProtoMessage()    {}
 func (*MetricsBase) Descriptor() ([]byte, []int) {
-	return fileDescriptor_metrics_9e7b895801991242, []int{0}
+	return fileDescriptor_6039342a2ba47b72, []int{0}
 }
+
 func (m *MetricsBase) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_MetricsBase.Unmarshal(m, b)
 }
 func (m *MetricsBase) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
 	return xxx_messageInfo_MetricsBase.Marshal(b, m, deterministic)
 }
-func (dst *MetricsBase) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_MetricsBase.Merge(dst, src)
+func (m *MetricsBase) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_MetricsBase.Merge(m, src)
 }
 func (m *MetricsBase) XXX_Size() int {
 	return xxx_messageInfo_MetricsBase.Size(m)
@@ -211,10 +226,10 @@
 
 var xxx_messageInfo_MetricsBase proto.InternalMessageInfo
 
-const Default_MetricsBase_TargetBuildVariant MetricsBase_BUILDVARIANT = MetricsBase_ENG
-const Default_MetricsBase_TargetArch MetricsBase_ARCH = MetricsBase_UNKNOWN
-const Default_MetricsBase_HostArch MetricsBase_ARCH = MetricsBase_UNKNOWN
-const Default_MetricsBase_Host_2NdArch MetricsBase_ARCH = MetricsBase_UNKNOWN
+const Default_MetricsBase_TargetBuildVariant MetricsBase_BuildVariant = MetricsBase_ENG
+const Default_MetricsBase_TargetArch MetricsBase_Arch = MetricsBase_UNKNOWN
+const Default_MetricsBase_HostArch MetricsBase_Arch = MetricsBase_UNKNOWN
+const Default_MetricsBase_Host_2NdArch MetricsBase_Arch = MetricsBase_UNKNOWN
 
 func (m *MetricsBase) GetBuildDateTimestamp() int64 {
 	if m != nil && m.BuildDateTimestamp != nil {
@@ -244,14 +259,14 @@
 	return ""
 }
 
-func (m *MetricsBase) GetTargetBuildVariant() MetricsBase_BUILDVARIANT {
+func (m *MetricsBase) GetTargetBuildVariant() MetricsBase_BuildVariant {
 	if m != nil && m.TargetBuildVariant != nil {
 		return *m.TargetBuildVariant
 	}
 	return Default_MetricsBase_TargetBuildVariant
 }
 
-func (m *MetricsBase) GetTargetArch() MetricsBase_ARCH {
+func (m *MetricsBase) GetTargetArch() MetricsBase_Arch {
 	if m != nil && m.TargetArch != nil {
 		return *m.TargetArch
 	}
@@ -272,14 +287,14 @@
 	return ""
 }
 
-func (m *MetricsBase) GetHostArch() MetricsBase_ARCH {
+func (m *MetricsBase) GetHostArch() MetricsBase_Arch {
 	if m != nil && m.HostArch != nil {
 		return *m.HostArch
 	}
 	return Default_MetricsBase_HostArch
 }
 
-func (m *MetricsBase) GetHost_2NdArch() MetricsBase_ARCH {
+func (m *MetricsBase) GetHost_2NdArch() MetricsBase_Arch {
 	if m != nil && m.Host_2NdArch != nil {
 		return *m.Host_2NdArch
 	}
@@ -378,16 +393,17 @@
 func (m *PerfInfo) String() string { return proto.CompactTextString(m) }
 func (*PerfInfo) ProtoMessage()    {}
 func (*PerfInfo) Descriptor() ([]byte, []int) {
-	return fileDescriptor_metrics_9e7b895801991242, []int{1}
+	return fileDescriptor_6039342a2ba47b72, []int{1}
 }
+
 func (m *PerfInfo) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_PerfInfo.Unmarshal(m, b)
 }
 func (m *PerfInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
 	return xxx_messageInfo_PerfInfo.Marshal(b, m, deterministic)
 }
-func (dst *PerfInfo) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_PerfInfo.Merge(dst, src)
+func (m *PerfInfo) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_PerfInfo.Merge(m, src)
 }
 func (m *PerfInfo) XXX_Size() int {
 	return xxx_messageInfo_PerfInfo.Size(m)
@@ -435,7 +451,7 @@
 
 type ModuleTypeInfo struct {
 	// The build system, eg. Soong or Make.
-	BuildSystem *ModuleTypeInfo_BUILDSYSTEM `protobuf:"varint,1,opt,name=build_system,json=buildSystem,enum=build_metrics.ModuleTypeInfo_BUILDSYSTEM,def=0" json:"build_system,omitempty"`
+	BuildSystem *ModuleTypeInfo_BuildSystem `protobuf:"varint,1,opt,name=build_system,json=buildSystem,enum=soong_build_metrics.ModuleTypeInfo_BuildSystem,def=0" json:"build_system,omitempty"`
 	// The module type, eg. java_library, cc_binary, and etc.
 	ModuleType *string `protobuf:"bytes,2,opt,name=module_type,json=moduleType" json:"module_type,omitempty"`
 	// The number of logical modules.
@@ -449,16 +465,17 @@
 func (m *ModuleTypeInfo) String() string { return proto.CompactTextString(m) }
 func (*ModuleTypeInfo) ProtoMessage()    {}
 func (*ModuleTypeInfo) Descriptor() ([]byte, []int) {
-	return fileDescriptor_metrics_9e7b895801991242, []int{2}
+	return fileDescriptor_6039342a2ba47b72, []int{2}
 }
+
 func (m *ModuleTypeInfo) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_ModuleTypeInfo.Unmarshal(m, b)
 }
 func (m *ModuleTypeInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
 	return xxx_messageInfo_ModuleTypeInfo.Marshal(b, m, deterministic)
 }
-func (dst *ModuleTypeInfo) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_ModuleTypeInfo.Merge(dst, src)
+func (m *ModuleTypeInfo) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ModuleTypeInfo.Merge(m, src)
 }
 func (m *ModuleTypeInfo) XXX_Size() int {
 	return xxx_messageInfo_ModuleTypeInfo.Size(m)
@@ -469,9 +486,9 @@
 
 var xxx_messageInfo_ModuleTypeInfo proto.InternalMessageInfo
 
-const Default_ModuleTypeInfo_BuildSystem ModuleTypeInfo_BUILDSYSTEM = ModuleTypeInfo_UNKNOWN
+const Default_ModuleTypeInfo_BuildSystem ModuleTypeInfo_BuildSystem = ModuleTypeInfo_UNKNOWN
 
-func (m *ModuleTypeInfo) GetBuildSystem() ModuleTypeInfo_BUILDSYSTEM {
+func (m *ModuleTypeInfo) GetBuildSystem() ModuleTypeInfo_BuildSystem {
 	if m != nil && m.BuildSystem != nil {
 		return *m.BuildSystem
 	}
@@ -493,65 +510,65 @@
 }
 
 func init() {
-	proto.RegisterType((*MetricsBase)(nil), "build_metrics.MetricsBase")
-	proto.RegisterType((*PerfInfo)(nil), "build_metrics.PerfInfo")
-	proto.RegisterType((*ModuleTypeInfo)(nil), "build_metrics.ModuleTypeInfo")
-	proto.RegisterEnum("build_metrics.MetricsBase_BUILDVARIANT", MetricsBase_BUILDVARIANT_name, MetricsBase_BUILDVARIANT_value)
-	proto.RegisterEnum("build_metrics.MetricsBase_ARCH", MetricsBase_ARCH_name, MetricsBase_ARCH_value)
-	proto.RegisterEnum("build_metrics.ModuleTypeInfo_BUILDSYSTEM", ModuleTypeInfo_BUILDSYSTEM_name, ModuleTypeInfo_BUILDSYSTEM_value)
+	proto.RegisterEnum("soong_build_metrics.MetricsBase_BuildVariant", MetricsBase_BuildVariant_name, MetricsBase_BuildVariant_value)
+	proto.RegisterEnum("soong_build_metrics.MetricsBase_Arch", MetricsBase_Arch_name, MetricsBase_Arch_value)
+	proto.RegisterEnum("soong_build_metrics.ModuleTypeInfo_BuildSystem", ModuleTypeInfo_BuildSystem_name, ModuleTypeInfo_BuildSystem_value)
+	proto.RegisterType((*MetricsBase)(nil), "soong_build_metrics.MetricsBase")
+	proto.RegisterType((*PerfInfo)(nil), "soong_build_metrics.PerfInfo")
+	proto.RegisterType((*ModuleTypeInfo)(nil), "soong_build_metrics.ModuleTypeInfo")
 }
 
-func init() { proto.RegisterFile("metrics.proto", fileDescriptor_metrics_9e7b895801991242) }
+func init() { proto.RegisterFile("metrics.proto", fileDescriptor_6039342a2ba47b72) }
 
-var fileDescriptor_metrics_9e7b895801991242 = []byte{
-	// 783 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x55, 0xdd, 0x6e, 0xdb, 0x36,
-	0x14, 0xae, 0x62, 0x25, 0x96, 0x8e, 0x62, 0x57, 0x61, 0x02, 0x44, 0xc5, 0x50, 0x34, 0x30, 0xf6,
-	0x93, 0x01, 0x9b, 0x57, 0x18, 0x81, 0x11, 0x04, 0xbb, 0xb1, 0x13, 0xa3, 0x35, 0x5a, 0xdb, 0x85,
-	0x6c, 0x67, 0xdd, 0x2e, 0x46, 0x68, 0x12, 0xdd, 0x68, 0xb3, 0x44, 0x81, 0xa4, 0x8a, 0xf9, 0x21,
-	0xf6, 0x8c, 0x7b, 0x91, 0x5d, 0x0c, 0x3c, 0xb4, 0x5c, 0xa5, 0x17, 0x29, 0x72, 0x47, 0x9d, 0xef,
-	0x87, 0xdf, 0x91, 0xc8, 0x23, 0x68, 0x65, 0x4c, 0x89, 0x34, 0x96, 0xdd, 0x42, 0x70, 0xc5, 0x49,
-	0xeb, 0x8f, 0x32, 0x5d, 0x27, 0x74, 0x5b, 0xec, 0xfc, 0xe7, 0x80, 0x37, 0x31, 0xeb, 0x61, 0x24,
-	0x19, 0x79, 0x09, 0x27, 0x86, 0x90, 0x44, 0x8a, 0x51, 0x95, 0x66, 0x4c, 0xaa, 0x28, 0x2b, 0x02,
-	0xeb, 0xcc, 0x3a, 0x6f, 0x84, 0x04, 0xb1, 0x9b, 0x48, 0xb1, 0x45, 0x85, 0x90, 0x67, 0xe0, 0x18,
-	0x45, 0x9a, 0x04, 0x7b, 0x67, 0xd6, 0xb9, 0x1b, 0x36, 0xf1, 0x79, 0x9c, 0x90, 0x2b, 0x78, 0x56,
-	0xac, 0x23, 0xb5, 0xe2, 0x22, 0xa3, 0x1f, 0x99, 0x90, 0x29, 0xcf, 0x69, 0xcc, 0x13, 0x96, 0x47,
-	0x19, 0x0b, 0x1a, 0xc8, 0x3d, 0xad, 0x08, 0xb7, 0x06, 0xbf, 0xde, 0xc2, 0xe4, 0x1b, 0x68, 0xab,
-	0x48, 0x7c, 0x60, 0x8a, 0x16, 0x82, 0x27, 0x65, 0xac, 0x02, 0x1b, 0x05, 0x2d, 0x53, 0x7d, 0x67,
-	0x8a, 0xe4, 0x77, 0x38, 0xd9, 0xd2, 0x4c, 0x88, 0x8f, 0x91, 0x48, 0xa3, 0x5c, 0x05, 0xfb, 0x67,
-	0xd6, 0x79, 0xbb, 0xf7, 0x5d, 0xf7, 0x5e, 0xb7, 0xdd, 0x5a, 0xa7, 0xdd, 0xe1, 0x72, 0xfc, 0xf6,
-	0xe6, 0x76, 0x10, 0x8e, 0x07, 0xd3, 0xc5, 0x55, 0x63, 0x34, 0x7d, 0x15, 0x12, 0xe3, 0x34, 0xd4,
-	0x92, 0x5b, 0xe3, 0x43, 0xc6, 0xe0, 0x6d, 0xfd, 0x23, 0x11, 0xdf, 0x05, 0x07, 0x68, 0xfb, 0xe2,
-	0x01, 0xdb, 0x41, 0x78, 0xfd, 0xfa, 0xaa, 0xb9, 0x9c, 0xbe, 0x99, 0xce, 0x7e, 0x99, 0x86, 0x60,
-	0xc4, 0x03, 0x11, 0xdf, 0x91, 0x2e, 0x1c, 0xd7, 0xac, 0x76, 0x49, 0x9b, 0xd8, 0xd6, 0xd1, 0x27,
-	0x62, 0xb5, 0xf5, 0x0f, 0xb0, 0x0d, 0x44, 0xe3, 0xa2, 0xdc, 0xd1, 0x1d, 0xa4, 0xfb, 0x06, 0xb9,
-	0x2e, 0xca, 0x8a, 0x3d, 0x02, 0xf7, 0x8e, 0xcb, 0x6d, 0x4c, 0xf7, 0x91, 0x31, 0x1d, 0x2d, 0xc5,
-	0x90, 0x6f, 0xa1, 0x85, 0x36, 0xbd, 0x3c, 0x31, 0x56, 0xf0, 0x48, 0x2b, 0x4f, 0xcb, 0x7b, 0x79,
-	0x82, 0x6e, 0xa7, 0xd0, 0x44, 0x37, 0x2e, 0x03, 0x0f, 0x73, 0x1f, 0xe8, 0xc7, 0x99, 0x24, 0x9d,
-	0xed, 0x36, 0x5c, 0x52, 0xf6, 0xb7, 0x12, 0x51, 0x70, 0x88, 0xb0, 0x67, 0xe0, 0x91, 0x2e, 0xed,
-	0x38, 0xb1, 0xe0, 0x52, 0x6a, 0x8b, 0xd6, 0x27, 0xce, 0xb5, 0xae, 0xcd, 0x24, 0xf9, 0x16, 0x9e,
-	0xd6, 0x38, 0x18, 0xb8, 0x6d, 0x8e, 0xc9, 0x8e, 0x85, 0x41, 0x7e, 0x84, 0xe3, 0x1a, 0x6f, 0xd7,
-	0xdc, 0x53, 0xf3, 0x32, 0x77, 0xdc, 0x5a, 0x6e, 0x5e, 0x2a, 0x9a, 0xa4, 0x22, 0xf0, 0x4d, 0x6e,
-	0x5e, 0xaa, 0x9b, 0x54, 0x90, 0x4b, 0xf0, 0x24, 0x53, 0x65, 0x41, 0x15, 0xe7, 0x6b, 0x19, 0x1c,
-	0x9d, 0x35, 0xce, 0xbd, 0xde, 0xe9, 0x67, 0x2f, 0xe7, 0x1d, 0x13, 0xab, 0x71, 0xbe, 0xe2, 0x21,
-	0x20, 0x77, 0xa1, 0xa9, 0xe4, 0x02, 0xdc, 0xbf, 0x22, 0x95, 0x52, 0x51, 0xe6, 0x32, 0x20, 0x0f,
-	0xeb, 0x1c, 0xcd, 0x0c, 0xcb, 0x5c, 0x92, 0x3e, 0x80, 0xe4, 0x3c, 0xff, 0x60, 0x64, 0xc7, 0x0f,
-	0xcb, 0x5c, 0xa4, 0x56, 0xba, 0x3c, 0xcd, 0xff, 0x8c, 0x8c, 0xee, 0xe4, 0x0b, 0x3a, 0xa4, 0x6a,
-	0x5d, 0xe7, 0x25, 0x1c, 0xd6, 0xef, 0x05, 0x71, 0xc0, 0x5e, 0xce, 0x47, 0xa1, 0xff, 0x84, 0xb4,
-	0xc0, 0xd5, 0xab, 0x9b, 0xd1, 0x70, 0xf9, 0xca, 0xb7, 0x48, 0x13, 0xf4, 0x95, 0xf1, 0xf7, 0x3a,
-	0x3f, 0x83, 0xad, 0x0f, 0x00, 0xf1, 0xa0, 0x3a, 0x02, 0xfe, 0x13, 0x8d, 0x0e, 0xc2, 0x89, 0x6f,
-	0x11, 0x17, 0xf6, 0x07, 0xe1, 0xa4, 0x7f, 0xe1, 0xef, 0xe9, 0xda, 0xfb, 0xcb, 0xbe, 0xdf, 0x20,
-	0x00, 0x07, 0xef, 0x2f, 0xfb, 0xb4, 0x7f, 0xe1, 0xdb, 0x9d, 0x7f, 0x2c, 0x70, 0xaa, 0x1c, 0x84,
-	0x80, 0x9d, 0x30, 0x19, 0xe3, 0xac, 0x71, 0x43, 0x5c, 0xeb, 0x1a, 0x4e, 0x0b, 0x33, 0x59, 0x70,
-	0x4d, 0x9e, 0x03, 0x48, 0x15, 0x09, 0x85, 0xe3, 0x09, 0xe7, 0x88, 0x1d, 0xba, 0x58, 0xd1, 0x53,
-	0x89, 0x7c, 0x05, 0xae, 0x60, 0xd1, 0xda, 0xa0, 0x36, 0xa2, 0x8e, 0x2e, 0x20, 0xf8, 0x1c, 0x20,
-	0x63, 0x19, 0x17, 0x1b, 0x5a, 0x4a, 0x86, 0x53, 0xc2, 0x0e, 0x5d, 0x53, 0x59, 0x4a, 0xd6, 0xf9,
-	0xd7, 0x82, 0xf6, 0x84, 0x27, 0xe5, 0x9a, 0x2d, 0x36, 0x05, 0xc3, 0x54, 0x4b, 0x38, 0x34, 0xef,
-	0x4d, 0x6e, 0xa4, 0x62, 0x19, 0xa6, 0x6b, 0xf7, 0xbe, 0xff, 0xfc, 0x42, 0xdc, 0x13, 0x99, 0xe1,
-	0x32, 0xff, 0x75, 0xbe, 0x18, 0x4d, 0x6a, 0x57, 0x03, 0x25, 0x73, 0xb4, 0x21, 0x2f, 0xc0, 0xcb,
-	0x50, 0x43, 0xd5, 0xa6, 0xa8, 0xfa, 0x83, 0x6c, 0x67, 0x43, 0xbe, 0x86, 0x76, 0x5e, 0x66, 0x94,
-	0xaf, 0xa8, 0x29, 0x4a, 0xec, 0xb4, 0x15, 0x1e, 0xe6, 0x65, 0x36, 0x5b, 0x99, 0xfd, 0x64, 0xe7,
-	0x27, 0xf0, 0x6a, 0x7b, 0xdd, 0xff, 0x0a, 0x2e, 0xec, 0xcf, 0x67, 0xb3, 0xa9, 0xfe, 0x5c, 0x0e,
-	0xd8, 0x93, 0xc1, 0x9b, 0x91, 0xbf, 0x37, 0x3c, 0x7a, 0xdd, 0xf8, 0xad, 0xfa, 0x25, 0x50, 0xfc,
-	0x25, 0xfc, 0x1f, 0x00, 0x00, 0xff, 0xff, 0xd4, 0x8d, 0x19, 0x89, 0x22, 0x06, 0x00, 0x00,
+var fileDescriptor_6039342a2ba47b72 = []byte{
+	// 769 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x55, 0x6f, 0x6b, 0xdb, 0x46,
+	0x18, 0xaf, 0x62, 0x25, 0x96, 0x1e, 0xc5, 0xae, 0x7a, 0xc9, 0xa8, 0xca, 0x08, 0x33, 0x66, 0x1d,
+	0x7e, 0xb1, 0xba, 0xc5, 0x14, 0x53, 0x4c, 0x19, 0xd8, 0x89, 0x29, 0x25, 0xd8, 0x2e, 0x4a, 0xdc,
+	0x95, 0xed, 0xc5, 0xa1, 0x4a, 0xe7, 0x46, 0x9b, 0xa5, 0x13, 0x77, 0xa7, 0x32, 0x7f, 0x88, 0x7d,
+	0x93, 0x7d, 0xad, 0x7d, 0x8f, 0x71, 0xcf, 0x49, 0x8e, 0x02, 0x81, 0x85, 0xbe, 0x3b, 0x3d, 0xbf,
+	0x3f, 0xf7, 0x7b, 0x4e, 0xba, 0x47, 0xd0, 0xc9, 0x98, 0x12, 0x69, 0x2c, 0x87, 0x85, 0xe0, 0x8a,
+	0x93, 0x13, 0xc9, 0x79, 0xfe, 0x85, 0x7e, 0x2e, 0xd3, 0x6d, 0x42, 0x2b, 0xa8, 0xff, 0x8f, 0x0b,
+	0xde, 0xc2, 0xac, 0x67, 0x91, 0x64, 0xe4, 0x15, 0x9c, 0x1a, 0x42, 0x12, 0x29, 0x46, 0x55, 0x9a,
+	0x31, 0xa9, 0xa2, 0xac, 0x08, 0xac, 0x9e, 0x35, 0x68, 0x85, 0x04, 0xb1, 0x8b, 0x48, 0xb1, 0xeb,
+	0x1a, 0x21, 0xcf, 0xc0, 0x31, 0x8a, 0x34, 0x09, 0x0e, 0x7a, 0xd6, 0xc0, 0x0d, 0xdb, 0xf8, 0xfc,
+	0x3e, 0x21, 0x13, 0x78, 0x56, 0x6c, 0x23, 0xb5, 0xe1, 0x22, 0xa3, 0x5f, 0x99, 0x90, 0x29, 0xcf,
+	0x69, 0xcc, 0x13, 0x96, 0x47, 0x19, 0x0b, 0x5a, 0xc8, 0x7d, 0x5a, 0x13, 0x3e, 0x1a, 0xfc, 0xbc,
+	0x82, 0xc9, 0x73, 0xe8, 0xaa, 0x48, 0x7c, 0x61, 0x8a, 0x16, 0x82, 0x27, 0x65, 0xac, 0x02, 0x1b,
+	0x05, 0x1d, 0x53, 0xfd, 0x60, 0x8a, 0x24, 0x81, 0xd3, 0x8a, 0x66, 0x42, 0x7c, 0x8d, 0x44, 0x1a,
+	0xe5, 0x2a, 0x38, 0xec, 0x59, 0x83, 0xee, 0xe8, 0xc5, 0xf0, 0x9e, 0x9e, 0x87, 0x8d, 0x7e, 0x87,
+	0x33, 0x8d, 0x7c, 0x34, 0xa2, 0x49, 0x6b, 0xbe, 0x7c, 0x17, 0x12, 0xe3, 0xd7, 0x04, 0xc8, 0x0a,
+	0xbc, 0x6a, 0x97, 0x48, 0xc4, 0x37, 0xc1, 0x11, 0x9a, 0x3f, 0xff, 0x5f, 0xf3, 0xa9, 0x88, 0x6f,
+	0x26, 0xed, 0xf5, 0xf2, 0x72, 0xb9, 0xfa, 0x75, 0x19, 0x82, 0xb1, 0xd0, 0x45, 0x32, 0x84, 0x93,
+	0x86, 0xe1, 0x3e, 0x75, 0x1b, 0x5b, 0x7c, 0x72, 0x4b, 0xac, 0x03, 0xfc, 0x0c, 0x55, 0x2c, 0x1a,
+	0x17, 0xe5, 0x9e, 0xee, 0x20, 0xdd, 0x37, 0xc8, 0x79, 0x51, 0xd6, 0xec, 0x4b, 0x70, 0x6f, 0xb8,
+	0xac, 0xc2, 0xba, 0xdf, 0x14, 0xd6, 0xd1, 0x06, 0x18, 0x35, 0x84, 0x0e, 0x9a, 0x8d, 0xf2, 0xc4,
+	0x18, 0xc2, 0x37, 0x19, 0x7a, 0xda, 0x64, 0x94, 0x27, 0xe8, 0xf9, 0x14, 0xda, 0xe8, 0xc9, 0x65,
+	0xe0, 0x61, 0x0f, 0x47, 0xfa, 0x71, 0x25, 0x49, 0xbf, 0xda, 0x8c, 0x4b, 0xca, 0xfe, 0x52, 0x22,
+	0x0a, 0x8e, 0x11, 0xf6, 0x0c, 0x3c, 0xd7, 0xa5, 0x3d, 0x27, 0x16, 0x5c, 0x4a, 0x6d, 0xd1, 0xb9,
+	0xe5, 0x9c, 0xeb, 0xda, 0x4a, 0x92, 0x9f, 0xe0, 0x71, 0x83, 0x83, 0xb1, 0xbb, 0xe6, 0xf3, 0xd9,
+	0xb3, 0x30, 0xc8, 0x0b, 0x38, 0x69, 0xf0, 0xf6, 0x2d, 0x3e, 0x36, 0x07, 0xbb, 0xe7, 0x36, 0x72,
+	0xf3, 0x52, 0xd1, 0x24, 0x15, 0x81, 0x6f, 0x72, 0xf3, 0x52, 0x5d, 0xa4, 0x82, 0xfc, 0x02, 0x9e,
+	0x64, 0xaa, 0x2c, 0xa8, 0xe2, 0x7c, 0x2b, 0x83, 0x27, 0xbd, 0xd6, 0xc0, 0x1b, 0x9d, 0xdd, 0x7b,
+	0x44, 0x1f, 0x98, 0xd8, 0xbc, 0xcf, 0x37, 0x3c, 0x04, 0x54, 0x5c, 0x6b, 0x01, 0x99, 0x80, 0xfb,
+	0x67, 0xa4, 0x52, 0x2a, 0xca, 0x5c, 0x06, 0xe4, 0x21, 0x6a, 0x47, 0xf3, 0xc3, 0x32, 0x97, 0xe4,
+	0x2d, 0x80, 0x61, 0xa2, 0xf8, 0xe4, 0x21, 0x62, 0x17, 0xd1, 0x5a, 0x9d, 0xa7, 0xf9, 0x1f, 0x91,
+	0x51, 0x9f, 0x3e, 0x48, 0x8d, 0x02, 0xad, 0xee, 0xbf, 0x82, 0xe3, 0x3b, 0x17, 0xc5, 0x01, 0x7b,
+	0x7d, 0x35, 0x0f, 0xfd, 0x47, 0xa4, 0x03, 0xae, 0x5e, 0x5d, 0xcc, 0x67, 0xeb, 0x77, 0xbe, 0x45,
+	0xda, 0xa0, 0x2f, 0x97, 0x7f, 0xd0, 0x7f, 0x0b, 0x36, 0x1e, 0xa5, 0x07, 0xf5, 0xa7, 0xe1, 0x3f,
+	0xd2, 0xe8, 0x34, 0x5c, 0xf8, 0x16, 0x71, 0xe1, 0x70, 0x1a, 0x2e, 0xc6, 0xaf, 0xfd, 0x03, 0x5d,
+	0xfb, 0xf4, 0x66, 0xec, 0xb7, 0x08, 0xc0, 0xd1, 0xa7, 0x37, 0x63, 0x3a, 0x7e, 0xed, 0xdb, 0xfd,
+	0xbf, 0x2d, 0x70, 0xea, 0x1c, 0x84, 0x80, 0x9d, 0x30, 0x19, 0xe3, 0x6c, 0x72, 0x43, 0x5c, 0xeb,
+	0x1a, 0x4e, 0x17, 0x33, 0x89, 0x70, 0x4d, 0xce, 0x00, 0xa4, 0x8a, 0x84, 0xc2, 0x71, 0x86, 0x73,
+	0xc7, 0x0e, 0x5d, 0xac, 0xe8, 0x29, 0x46, 0xbe, 0x07, 0x57, 0xb0, 0x68, 0x6b, 0x50, 0x1b, 0x51,
+	0x47, 0x17, 0x10, 0x3c, 0x03, 0xc8, 0x58, 0xc6, 0xc5, 0x8e, 0x96, 0x92, 0xe1, 0x54, 0xb1, 0x43,
+	0xd7, 0x54, 0xd6, 0x92, 0xf5, 0xff, 0xb5, 0xa0, 0xbb, 0xe0, 0x49, 0xb9, 0x65, 0xd7, 0xbb, 0x82,
+	0x61, 0xaa, 0xdf, 0xe1, 0xd8, 0x9c, 0x9b, 0xdc, 0x49, 0xc5, 0x32, 0x4c, 0xd7, 0x1d, 0xbd, 0xbc,
+	0xff, 0xba, 0xdc, 0x91, 0x9a, 0x61, 0x74, 0x85, 0xb2, 0xc6, 0xc5, 0xf9, 0x7c, 0x5b, 0x25, 0x3f,
+	0x80, 0x97, 0xa1, 0x86, 0xaa, 0x5d, 0x51, 0x77, 0x09, 0xd9, 0xde, 0x86, 0xfc, 0x08, 0xdd, 0xbc,
+	0xcc, 0x28, 0xdf, 0x50, 0x53, 0x94, 0xd8, 0x6f, 0x27, 0x3c, 0xce, 0xcb, 0x6c, 0xb5, 0x31, 0xfb,
+	0xc9, 0xfe, 0x4b, 0xf0, 0x1a, 0x7b, 0xdd, 0x7d, 0x17, 0x2e, 0x1c, 0x5e, 0xad, 0x56, 0x4b, 0xfd,
+	0xd2, 0x1c, 0xb0, 0x17, 0xd3, 0xcb, 0xb9, 0x7f, 0x30, 0xfb, 0xee, 0xb7, 0xea, 0xef, 0x51, 0x25,
+	0xa7, 0xf8, 0x4b, 0xf9, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x81, 0xd0, 0x84, 0x23, 0x62, 0x06, 0x00,
+	0x00,
 }
diff --git a/ui/metrics/metrics_proto/metrics.proto b/ui/metrics/metrics_proto/metrics.proto
index b3de2f4..93034eb 100644
--- a/ui/metrics/metrics_proto/metrics.proto
+++ b/ui/metrics/metrics_proto/metrics.proto
@@ -14,10 +14,8 @@
 
 syntax = "proto2";
 
-option optimize_for = LITE_RUNTIME;
-
-package build_metrics;
-option go_package = "metrics_proto";
+package soong_build_metrics;
+option go_package = "soong_metrics_proto";
 
 message MetricsBase {
   // Timestamp generated when the build starts.
@@ -32,15 +30,15 @@
   // The target product information, eg. aosp_arm.
   optional string target_product = 4;
 
-  enum BUILDVARIANT {
+  enum BuildVariant {
     USER = 0;
     USERDEBUG = 1;
     ENG = 2;
   }
   // The target build variant information, eg. eng.
-  optional BUILDVARIANT target_build_variant = 5 [default = ENG];
+  optional BuildVariant target_build_variant = 5 [default = ENG];
 
-  enum ARCH {
+  enum Arch {
     UNKNOWN = 0;
     ARM = 1;
     ARM64 = 2;
@@ -48,7 +46,7 @@
     X86_64 = 4;
   }
   // The target arch information, eg. arm.
-  optional ARCH target_arch = 6 [default = UNKNOWN];
+  optional Arch target_arch = 6 [default = UNKNOWN];
 
   // The target arch variant information, eg. armv7-a-neon.
   optional string target_arch_variant = 7;
@@ -57,10 +55,10 @@
   optional string target_cpu_variant = 8;
 
   // The host arch information, eg. x86_64.
-  optional ARCH host_arch = 9 [default = UNKNOWN];
+  optional Arch host_arch = 9 [default = UNKNOWN];
 
   // The host 2nd arch information, eg. x86.
-  optional ARCH host_2nd_arch = 10 [default = UNKNOWN];
+  optional Arch host_2nd_arch = 10 [default = UNKNOWN];
 
   // The host os information, eg. linux.
   optional string host_os = 11;
@@ -113,13 +111,13 @@
 }
 
 message ModuleTypeInfo {
-  enum BUILDSYSTEM {
+  enum BuildSystem {
     UNKNOWN = 0;
     SOONG = 1;
     MAKE = 2;
   }
   // The build system, eg. Soong or Make.
-  optional BUILDSYSTEM build_system = 1 [default = UNKNOWN];
+  optional BuildSystem build_system = 1 [default = UNKNOWN];
 
   // The module type, eg. java_library, cc_binary, and etc.
   optional string module_type = 2;
diff --git a/ui/metrics/time.go b/ui/metrics/time.go
index 7e8801a..b8baf16 100644
--- a/ui/metrics/time.go
+++ b/ui/metrics/time.go
@@ -30,7 +30,7 @@
 
 type TimeTracer interface {
 	Begin(name, desc string, thread tracer.Thread)
-	End(thread tracer.Thread) metrics_proto.PerfInfo
+	End(thread tracer.Thread) soong_metrics_proto.PerfInfo
 }
 
 type timeTracerImpl struct {
@@ -51,11 +51,11 @@
 	t.activeEvents = append(t.activeEvents, timeEvent{name: name, desc: desc, atNanos: atNanos})
 }
 
-func (t *timeTracerImpl) End(thread tracer.Thread) metrics_proto.PerfInfo {
+func (t *timeTracerImpl) End(thread tracer.Thread) soong_metrics_proto.PerfInfo {
 	return t.endAt(t.now())
 }
 
-func (t *timeTracerImpl) endAt(atNanos uint64) metrics_proto.PerfInfo {
+func (t *timeTracerImpl) endAt(atNanos uint64) soong_metrics_proto.PerfInfo {
 	if len(t.activeEvents) < 1 {
 		panic("Internal error: No pending events for endAt to end!")
 	}
@@ -63,7 +63,7 @@
 	t.activeEvents = t.activeEvents[:len(t.activeEvents)-1]
 	realTime := atNanos - lastEvent.atNanos
 
-	return metrics_proto.PerfInfo{
+	return soong_metrics_proto.PerfInfo{
 		Desc:      &lastEvent.desc,
 		Name:      &lastEvent.name,
 		StartTime: &lastEvent.atNanos,
diff --git a/ui/status/log.go b/ui/status/log.go
index 921aa44..7badac7 100644
--- a/ui/status/log.go
+++ b/ui/status/log.go
@@ -71,6 +71,11 @@
 	fmt.Fprintf(v.w, "%s%s\n", level.Prefix(), message)
 }
 
+func (v *verboseLog) Write(p []byte) (int, error) {
+	fmt.Fprint(v.w, string(p))
+	return len(p), nil
+}
+
 type errorLog struct {
 	w io.WriteCloser
 
@@ -134,3 +139,8 @@
 
 	fmt.Fprintf(e.w, "error: %s\n", message)
 }
+
+func (e *errorLog) Write(p []byte) (int, error) {
+	fmt.Fprint(e.w, string(p))
+	return len(p), nil
+}
diff --git a/ui/status/status.go b/ui/status/status.go
index 46ec72e..3d8cd7a 100644
--- a/ui/status/status.go
+++ b/ui/status/status.go
@@ -173,6 +173,9 @@
 	// Flush is called when your outputs should be flushed / closed. No
 	// output is expected after this call.
 	Flush()
+
+	// Write lets StatusOutput implement io.Writer
+	Write(p []byte) (n int, err error)
 }
 
 // Status is the multiplexer / accumulator between ToolStatus instances (via
diff --git a/ui/status/status_test.go b/ui/status/status_test.go
index e62785f..9494582 100644
--- a/ui/status/status_test.go
+++ b/ui/status/status_test.go
@@ -27,6 +27,11 @@
 func (c counterOutput) Message(level MsgLevel, msg string) {}
 func (c counterOutput) Flush()                             {}
 
+func (c counterOutput) Write(p []byte) (int, error) {
+	// Discard writes
+	return len(p), nil
+}
+
 func (c counterOutput) Expect(t *testing.T, counts Counts) {
 	if Counts(c) == counts {
 		return
diff --git a/ui/terminal/Android.bp b/ui/terminal/Android.bp
index 7104a50..b533b0d 100644
--- a/ui/terminal/Android.bp
+++ b/ui/terminal/Android.bp
@@ -17,11 +17,15 @@
     pkgPath: "android/soong/ui/terminal",
     deps: ["soong-ui-status"],
     srcs: [
+        "dumb_status.go",
+        "format.go",
+        "smart_status.go",
         "status.go",
-        "writer.go",
+        "stdio.go",
         "util.go",
     ],
     testSrcs: [
+        "status_test.go",
         "util_test.go",
     ],
     darwin: {
diff --git a/ui/terminal/dumb_status.go b/ui/terminal/dumb_status.go
new file mode 100644
index 0000000..201770f
--- /dev/null
+++ b/ui/terminal/dumb_status.go
@@ -0,0 +1,71 @@
+// Copyright 2019 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminal
+
+import (
+	"fmt"
+	"io"
+
+	"android/soong/ui/status"
+)
+
+type dumbStatusOutput struct {
+	writer    io.Writer
+	formatter formatter
+}
+
+// NewDumbStatusOutput returns a StatusOutput that represents the
+// current build status similarly to Ninja's built-in terminal
+// output.
+func NewDumbStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
+	return &dumbStatusOutput{
+		writer:    w,
+		formatter: formatter,
+	}
+}
+
+func (s *dumbStatusOutput) Message(level status.MsgLevel, message string) {
+	if level >= status.StatusLvl {
+		fmt.Fprintln(s.writer, s.formatter.message(level, message))
+	}
+}
+
+func (s *dumbStatusOutput) StartAction(action *status.Action, counts status.Counts) {
+}
+
+func (s *dumbStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
+	str := result.Description
+	if str == "" {
+		str = result.Command
+	}
+
+	progress := s.formatter.progress(counts) + str
+
+	output := s.formatter.result(result)
+	output = string(stripAnsiEscapes([]byte(output)))
+
+	if output != "" {
+		fmt.Fprint(s.writer, progress, "\n", output)
+	} else {
+		fmt.Fprintln(s.writer, progress)
+	}
+}
+
+func (s *dumbStatusOutput) Flush() {}
+
+func (s *dumbStatusOutput) Write(p []byte) (int, error) {
+	fmt.Fprint(s.writer, string(p))
+	return len(p), nil
+}
diff --git a/ui/terminal/format.go b/ui/terminal/format.go
new file mode 100644
index 0000000..4205bdc
--- /dev/null
+++ b/ui/terminal/format.go
@@ -0,0 +1,123 @@
+// Copyright 2019 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminal
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"android/soong/ui/status"
+)
+
+type formatter struct {
+	format string
+	quiet  bool
+	start  time.Time
+}
+
+// newFormatter returns a formatter for formatting output to
+// the terminal in a format similar to Ninja.
+// format takes nearly all the same options as NINJA_STATUS.
+// %c is currently unsupported.
+func newFormatter(format string, quiet bool) formatter {
+	return formatter{
+		format: format,
+		quiet:  quiet,
+		start:  time.Now(),
+	}
+}
+
+func (s formatter) message(level status.MsgLevel, message string) string {
+	if level >= status.ErrorLvl {
+		return fmt.Sprintf("FAILED: %s", message)
+	} else if level > status.StatusLvl {
+		return fmt.Sprintf("%s%s", level.Prefix(), message)
+	} else if level == status.StatusLvl {
+		return message
+	}
+	return ""
+}
+
+func (s formatter) progress(counts status.Counts) string {
+	if s.format == "" {
+		return fmt.Sprintf("[%3d%% %d/%d] ", 100*counts.FinishedActions/counts.TotalActions, counts.FinishedActions, counts.TotalActions)
+	}
+
+	buf := &strings.Builder{}
+	for i := 0; i < len(s.format); i++ {
+		c := s.format[i]
+		if c != '%' {
+			buf.WriteByte(c)
+			continue
+		}
+
+		i = i + 1
+		if i == len(s.format) {
+			buf.WriteByte(c)
+			break
+		}
+
+		c = s.format[i]
+		switch c {
+		case '%':
+			buf.WriteByte(c)
+		case 's':
+			fmt.Fprintf(buf, "%d", counts.StartedActions)
+		case 't':
+			fmt.Fprintf(buf, "%d", counts.TotalActions)
+		case 'r':
+			fmt.Fprintf(buf, "%d", counts.RunningActions)
+		case 'u':
+			fmt.Fprintf(buf, "%d", counts.TotalActions-counts.StartedActions)
+		case 'f':
+			fmt.Fprintf(buf, "%d", counts.FinishedActions)
+		case 'o':
+			fmt.Fprintf(buf, "%.1f", float64(counts.FinishedActions)/time.Since(s.start).Seconds())
+		case 'c':
+			// TODO: implement?
+			buf.WriteRune('?')
+		case 'p':
+			fmt.Fprintf(buf, "%3d%%", 100*counts.FinishedActions/counts.TotalActions)
+		case 'e':
+			fmt.Fprintf(buf, "%.3f", time.Since(s.start).Seconds())
+		default:
+			buf.WriteString("unknown placeholder '")
+			buf.WriteByte(c)
+			buf.WriteString("'")
+		}
+	}
+	return buf.String()
+}
+
+func (s formatter) result(result status.ActionResult) string {
+	var ret string
+	if result.Error != nil {
+		targets := strings.Join(result.Outputs, " ")
+		if s.quiet || result.Command == "" {
+			ret = fmt.Sprintf("FAILED: %s\n%s", targets, result.Output)
+		} else {
+			ret = fmt.Sprintf("FAILED: %s\n%s\n%s", targets, result.Command, result.Output)
+		}
+	} else if result.Output != "" {
+		ret = result.Output
+	}
+
+	if len(ret) > 0 && ret[len(ret)-1] != '\n' {
+		ret += "\n"
+	}
+
+	return ret
+}
diff --git a/ui/terminal/smart_status.go b/ui/terminal/smart_status.go
new file mode 100644
index 0000000..9638cdf
--- /dev/null
+++ b/ui/terminal/smart_status.go
@@ -0,0 +1,412 @@
+// Copyright 2019 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminal
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"os/signal"
+	"strconv"
+	"strings"
+	"sync"
+	"syscall"
+	"time"
+
+	"android/soong/ui/status"
+)
+
+const tableHeightEnVar = "SOONG_UI_TABLE_HEIGHT"
+
+type actionTableEntry struct {
+	action    *status.Action
+	startTime time.Time
+}
+
+type smartStatusOutput struct {
+	writer    io.Writer
+	formatter formatter
+
+	lock sync.Mutex
+
+	haveBlankLine bool
+
+	tableMode             bool
+	tableHeight           int
+	requestedTableHeight  int
+	termWidth, termHeight int
+
+	runningActions  []actionTableEntry
+	ticker          *time.Ticker
+	done            chan bool
+	sigwinch        chan os.Signal
+	sigwinchHandled chan bool
+}
+
+// NewSmartStatusOutput returns a StatusOutput that represents the
+// current build status similarly to Ninja's built-in terminal
+// output.
+func NewSmartStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
+	tableHeight, _ := strconv.Atoi(os.Getenv(tableHeightEnVar))
+
+	s := &smartStatusOutput{
+		writer:    w,
+		formatter: formatter,
+
+		haveBlankLine: true,
+
+		tableMode:            tableHeight > 0,
+		requestedTableHeight: tableHeight,
+
+		done:     make(chan bool),
+		sigwinch: make(chan os.Signal),
+	}
+
+	s.updateTermSize()
+
+	if s.tableMode {
+		// Add empty lines at the bottom of the screen to scroll back the existing history
+		// and make room for the action table.
+		// TODO: read the cursor position to see if the empty lines are necessary?
+		for i := 0; i < s.tableHeight; i++ {
+			fmt.Fprintln(w)
+		}
+
+		// Hide the cursor to prevent seeing it bouncing around
+		fmt.Fprintf(s.writer, ansi.hideCursor())
+
+		// Configure the empty action table
+		s.actionTable()
+
+		// Start a tick to update the action table periodically
+		s.startActionTableTick()
+	}
+
+	s.startSigwinch()
+
+	return s
+}
+
+func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
+	if level < status.StatusLvl {
+		return
+	}
+
+	str := s.formatter.message(level, message)
+
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	if level > status.StatusLvl {
+		s.print(str)
+	} else {
+		s.statusLine(str)
+	}
+}
+
+func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
+	startTime := time.Now()
+
+	str := action.Description
+	if str == "" {
+		str = action.Command
+	}
+
+	progress := s.formatter.progress(counts)
+
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	s.runningActions = append(s.runningActions, actionTableEntry{
+		action:    action,
+		startTime: startTime,
+	})
+
+	s.statusLine(progress + str)
+}
+
+func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
+	str := result.Description
+	if str == "" {
+		str = result.Command
+	}
+
+	progress := s.formatter.progress(counts) + str
+
+	output := s.formatter.result(result)
+
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	for i, runningAction := range s.runningActions {
+		if runningAction.action == result.Action {
+			s.runningActions = append(s.runningActions[:i], s.runningActions[i+1:]...)
+			break
+		}
+	}
+
+	if output != "" {
+		s.statusLine(progress)
+		s.requestLine()
+		s.print(output)
+	} else {
+		s.statusLine(progress)
+	}
+}
+
+func (s *smartStatusOutput) Flush() {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	s.stopSigwinch()
+
+	s.requestLine()
+
+	s.runningActions = nil
+
+	if s.tableMode {
+		s.stopActionTableTick()
+
+		// Update the table after clearing runningActions to clear it
+		s.actionTable()
+
+		// Reset the scrolling region to the whole terminal
+		fmt.Fprintf(s.writer, ansi.resetScrollingMargins())
+		_, height, _ := termSize(s.writer)
+		// Move the cursor to the top of the now-blank, previously non-scrolling region
+		fmt.Fprintf(s.writer, ansi.setCursor(height-s.tableHeight, 0))
+		// Turn the cursor back on
+		fmt.Fprintf(s.writer, ansi.showCursor())
+	}
+}
+
+func (s *smartStatusOutput) Write(p []byte) (int, error) {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+	s.print(string(p))
+	return len(p), nil
+}
+
+func (s *smartStatusOutput) requestLine() {
+	if !s.haveBlankLine {
+		fmt.Fprintln(s.writer)
+		s.haveBlankLine = true
+	}
+}
+
+func (s *smartStatusOutput) print(str string) {
+	if !s.haveBlankLine {
+		fmt.Fprint(s.writer, "\r", ansi.clearToEndOfLine())
+		s.haveBlankLine = true
+	}
+	fmt.Fprint(s.writer, str)
+	if len(str) == 0 || str[len(str)-1] != '\n' {
+		fmt.Fprint(s.writer, "\n")
+	}
+}
+
+func (s *smartStatusOutput) statusLine(str string) {
+	idx := strings.IndexRune(str, '\n')
+	if idx != -1 {
+		str = str[0:idx]
+	}
+
+	// Limit line width to the terminal width, otherwise we'll wrap onto
+	// another line and we won't delete the previous line.
+	if s.termWidth > 0 {
+		str = s.elide(str)
+	}
+
+	// Move to the beginning on the line, turn on bold, print the output,
+	// turn off bold, then clear the rest of the line.
+	start := "\r" + ansi.bold()
+	end := ansi.regular() + ansi.clearToEndOfLine()
+	fmt.Fprint(s.writer, start, str, end)
+	s.haveBlankLine = false
+}
+
+func (s *smartStatusOutput) elide(str string) string {
+	if len(str) > s.termWidth {
+		// TODO: Just do a max. Ninja elides the middle, but that's
+		// more complicated and these lines aren't that important.
+		str = str[:s.termWidth]
+	}
+
+	return str
+}
+
+func (s *smartStatusOutput) startActionTableTick() {
+	s.ticker = time.NewTicker(time.Second)
+	go func() {
+		for {
+			select {
+			case <-s.ticker.C:
+				s.lock.Lock()
+				s.actionTable()
+				s.lock.Unlock()
+			case <-s.done:
+				return
+			}
+		}
+	}()
+}
+
+func (s *smartStatusOutput) stopActionTableTick() {
+	s.ticker.Stop()
+	s.done <- true
+}
+
+func (s *smartStatusOutput) startSigwinch() {
+	signal.Notify(s.sigwinch, syscall.SIGWINCH)
+	go func() {
+		for _ = range s.sigwinch {
+			s.lock.Lock()
+			s.updateTermSize()
+			if s.tableMode {
+				s.actionTable()
+			}
+			s.lock.Unlock()
+			if s.sigwinchHandled != nil {
+				s.sigwinchHandled <- true
+			}
+		}
+	}()
+}
+
+func (s *smartStatusOutput) stopSigwinch() {
+	signal.Stop(s.sigwinch)
+	close(s.sigwinch)
+}
+
+func (s *smartStatusOutput) updateTermSize() {
+	if w, h, ok := termSize(s.writer); ok {
+		firstUpdate := s.termHeight == 0 && s.termWidth == 0
+		oldScrollingHeight := s.termHeight - s.tableHeight
+
+		s.termWidth, s.termHeight = w, h
+
+		if s.tableMode {
+			tableHeight := s.requestedTableHeight
+			if tableHeight > s.termHeight-1 {
+				tableHeight = s.termHeight - 1
+			}
+			s.tableHeight = tableHeight
+
+			scrollingHeight := s.termHeight - s.tableHeight
+
+			if !firstUpdate {
+				// If the scrolling region has changed, attempt to pan the existing text so that it is
+				// not overwritten by the table.
+				if scrollingHeight < oldScrollingHeight {
+					pan := oldScrollingHeight - scrollingHeight
+					if pan > s.tableHeight {
+						pan = s.tableHeight
+					}
+					fmt.Fprint(s.writer, ansi.panDown(pan))
+				}
+			}
+		}
+	}
+}
+
+func (s *smartStatusOutput) actionTable() {
+	scrollingHeight := s.termHeight - s.tableHeight
+
+	// Update the scrolling region in case the height of the terminal changed
+	fmt.Fprint(s.writer, ansi.setScrollingMargins(0, scrollingHeight))
+	// Move the cursor to the first line of the non-scrolling region
+	fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1, 0))
+
+	// Write as many status lines as fit in the table
+	var tableLine int
+	var runningAction actionTableEntry
+	for tableLine, runningAction = range s.runningActions {
+		if tableLine >= s.tableHeight {
+			break
+		}
+
+		seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())
+
+		desc := runningAction.action.Description
+		if desc == "" {
+			desc = runningAction.action.Command
+		}
+
+		str := fmt.Sprintf("   %2d:%02d %s", seconds/60, seconds%60, desc)
+		str = s.elide(str)
+		fmt.Fprint(s.writer, str, ansi.clearToEndOfLine())
+		if tableLine < s.tableHeight-1 {
+			fmt.Fprint(s.writer, "\n")
+		}
+	}
+
+	// Clear any remaining lines in the table
+	for ; tableLine < s.tableHeight; tableLine++ {
+		fmt.Fprint(s.writer, ansi.clearToEndOfLine())
+		if tableLine < s.tableHeight-1 {
+			fmt.Fprint(s.writer, "\n")
+		}
+	}
+
+	// Move the cursor back to the last line of the scrolling region
+	fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 0))
+}
+
+var ansi = ansiImpl{}
+
+type ansiImpl struct{}
+
+func (ansiImpl) clearToEndOfLine() string {
+	return "\x1b[K"
+}
+
+func (ansiImpl) setCursor(row, column int) string {
+	// Direct cursor address
+	return fmt.Sprintf("\x1b[%d;%dH", row, column)
+}
+
+func (ansiImpl) setScrollingMargins(top, bottom int) string {
+	// Set Top and Bottom Margins DECSTBM
+	return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
+}
+
+func (ansiImpl) resetScrollingMargins() string {
+	// Set Top and Bottom Margins DECSTBM
+	return fmt.Sprintf("\x1b[r")
+}
+
+func (ansiImpl) bold() string {
+	return "\x1b[1m"
+}
+
+func (ansiImpl) regular() string {
+	return "\x1b[0m"
+}
+
+func (ansiImpl) showCursor() string {
+	return "\x1b[?25h"
+}
+
+func (ansiImpl) hideCursor() string {
+	return "\x1b[?25l"
+}
+
+func (ansiImpl) panDown(lines int) string {
+	return fmt.Sprintf("\x1b[%dS", lines)
+}
+
+func (ansiImpl) panUp(lines int) string {
+	return fmt.Sprintf("\x1b[%dT", lines)
+}
diff --git a/ui/terminal/status.go b/ui/terminal/status.go
index 2445c5b..69a2a09 100644
--- a/ui/terminal/status.go
+++ b/ui/terminal/status.go
@@ -15,131 +15,23 @@
 package terminal
 
 import (
-	"fmt"
-	"strings"
-	"time"
+	"io"
 
 	"android/soong/ui/status"
 )
 
-type statusOutput struct {
-	writer Writer
-	format string
-
-	start time.Time
-	quiet bool
-}
-
 // NewStatusOutput returns a StatusOutput that represents the
 // current build status similarly to Ninja's built-in terminal
 // output.
 //
 // statusFormat takes nearly all the same options as NINJA_STATUS.
 // %c is currently unsupported.
-func NewStatusOutput(w Writer, statusFormat string, quietBuild bool) status.StatusOutput {
-	return &statusOutput{
-		writer: w,
-		format: statusFormat,
+func NewStatusOutput(w io.Writer, statusFormat string, quietBuild bool) status.StatusOutput {
+	formatter := newFormatter(statusFormat, quietBuild)
 
-		start: time.Now(),
-		quiet: quietBuild,
-	}
-}
-
-func (s *statusOutput) Message(level status.MsgLevel, message string) {
-	if level >= status.ErrorLvl {
-		s.writer.Print(fmt.Sprintf("FAILED: %s", message))
-	} else if level > status.StatusLvl {
-		s.writer.Print(fmt.Sprintf("%s%s", level.Prefix(), message))
-	} else if level == status.StatusLvl {
-		s.writer.StatusLine(message)
-	}
-}
-
-func (s *statusOutput) StartAction(action *status.Action, counts status.Counts) {
-	if !s.writer.isSmartTerminal() {
-		return
-	}
-
-	str := action.Description
-	if str == "" {
-		str = action.Command
-	}
-
-	s.writer.StatusLine(s.progress(counts) + str)
-}
-
-func (s *statusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
-	str := result.Description
-	if str == "" {
-		str = result.Command
-	}
-
-	progress := s.progress(counts) + str
-
-	if result.Error != nil {
-		targets := strings.Join(result.Outputs, " ")
-		if s.quiet || result.Command == "" {
-			s.writer.StatusAndMessage(progress, fmt.Sprintf("FAILED: %s\n%s", targets, result.Output))
-		} else {
-			s.writer.StatusAndMessage(progress, fmt.Sprintf("FAILED: %s\n%s\n%s", targets, result.Command, result.Output))
-		}
-	} else if result.Output != "" {
-		s.writer.StatusAndMessage(progress, result.Output)
+	if isSmartTerminal(w) {
+		return NewSmartStatusOutput(w, formatter)
 	} else {
-		s.writer.StatusLine(progress)
+		return NewDumbStatusOutput(w, formatter)
 	}
 }
-
-func (s *statusOutput) Flush() {}
-
-func (s *statusOutput) progress(counts status.Counts) string {
-	if s.format == "" {
-		return fmt.Sprintf("[%3d%% %d/%d] ", 100*counts.FinishedActions/counts.TotalActions, counts.FinishedActions, counts.TotalActions)
-	}
-
-	buf := &strings.Builder{}
-	for i := 0; i < len(s.format); i++ {
-		c := s.format[i]
-		if c != '%' {
-			buf.WriteByte(c)
-			continue
-		}
-
-		i = i + 1
-		if i == len(s.format) {
-			buf.WriteByte(c)
-			break
-		}
-
-		c = s.format[i]
-		switch c {
-		case '%':
-			buf.WriteByte(c)
-		case 's':
-			fmt.Fprintf(buf, "%d", counts.StartedActions)
-		case 't':
-			fmt.Fprintf(buf, "%d", counts.TotalActions)
-		case 'r':
-			fmt.Fprintf(buf, "%d", counts.RunningActions)
-		case 'u':
-			fmt.Fprintf(buf, "%d", counts.TotalActions-counts.StartedActions)
-		case 'f':
-			fmt.Fprintf(buf, "%d", counts.FinishedActions)
-		case 'o':
-			fmt.Fprintf(buf, "%.1f", float64(counts.FinishedActions)/time.Since(s.start).Seconds())
-		case 'c':
-			// TODO: implement?
-			buf.WriteRune('?')
-		case 'p':
-			fmt.Fprintf(buf, "%3d%%", 100*counts.FinishedActions/counts.TotalActions)
-		case 'e':
-			fmt.Fprintf(buf, "%.3f", time.Since(s.start).Seconds())
-		default:
-			buf.WriteString("unknown placeholder '")
-			buf.WriteByte(c)
-			buf.WriteString("'")
-		}
-	}
-	return buf.String()
-}
diff --git a/ui/terminal/status_test.go b/ui/terminal/status_test.go
new file mode 100644
index 0000000..81aa238
--- /dev/null
+++ b/ui/terminal/status_test.go
@@ -0,0 +1,284 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminal
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"syscall"
+	"testing"
+
+	"android/soong/ui/status"
+)
+
+func TestStatusOutput(t *testing.T) {
+	tests := []struct {
+		name  string
+		calls func(stat status.StatusOutput)
+		smart string
+		dumb  string
+	}{
+		{
+			name:  "two actions",
+			calls: twoActions,
+			smart: "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action2\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
+			dumb:  "[ 50% 1/2] action1\n[100% 2/2] action2\n",
+		},
+		{
+			name:  "two parallel actions",
+			calls: twoParallelActions,
+			smart: "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[  0% 0/2] action2\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
+			dumb:  "[ 50% 1/2] action1\n[100% 2/2] action2\n",
+		},
+		{
+			name:  "action with output",
+			calls: actionsWithOutput,
+			smart: "\r\x1b[1m[  0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\noutput1\noutput2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
+			dumb:  "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n",
+		},
+		{
+			name:  "action with output without newline",
+			calls: actionsWithOutputWithoutNewline,
+			smart: "\r\x1b[1m[  0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\noutput1\noutput2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
+			dumb:  "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n",
+		},
+		{
+			name:  "action with error",
+			calls: actionsWithError,
+			smart: "\r\x1b[1m[  0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\nFAILED: f1 f2\ntouch f1 f2\nerror1\nerror2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
+			dumb:  "[ 33% 1/3] action1\n[ 66% 2/3] action2\nFAILED: f1 f2\ntouch f1 f2\nerror1\nerror2\n[100% 3/3] action3\n",
+		},
+		{
+			name:  "action with empty description",
+			calls: actionWithEmptyDescription,
+			smart: "\r\x1b[1m[  0% 0/1] command1\x1b[0m\x1b[K\r\x1b[1m[100% 1/1] command1\x1b[0m\x1b[K\n",
+			dumb:  "[100% 1/1] command1\n",
+		},
+		{
+			name:  "messages",
+			calls: actionsWithMessages,
+			smart: "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1mstatus\x1b[0m\x1b[K\r\x1b[Kprint\nFAILED: error\n\r\x1b[1m[ 50% 1/2] action2\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
+			dumb:  "[ 50% 1/2] action1\nstatus\nprint\nFAILED: error\n[100% 2/2] action2\n",
+		},
+		{
+			name:  "action with long description",
+			calls: actionWithLongDescription,
+			smart: "\r\x1b[1m[  0% 0/2] action with very long descrip\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action with very long descrip\x1b[0m\x1b[K\n",
+			dumb:  "[ 50% 1/2] action with very long description to test eliding\n",
+		},
+		{
+			name:  "action with output with ansi codes",
+			calls: actionWithOuptutWithAnsiCodes,
+			smart: "\r\x1b[1m[  0% 0/1] action1\x1b[0m\x1b[K\r\x1b[1m[100% 1/1] action1\x1b[0m\x1b[K\n\x1b[31mcolor\x1b[0m\n",
+			dumb:  "[100% 1/1] action1\ncolor\n",
+		},
+	}
+
+	os.Setenv(tableHeightEnVar, "")
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			t.Run("smart", func(t *testing.T) {
+				smart := &fakeSmartTerminal{termWidth: 40}
+				stat := NewStatusOutput(smart, "", false)
+				tt.calls(stat)
+				stat.Flush()
+
+				if g, w := smart.String(), tt.smart; g != w {
+					t.Errorf("want:\n%q\ngot:\n%q", w, g)
+				}
+			})
+
+			t.Run("dumb", func(t *testing.T) {
+				dumb := &bytes.Buffer{}
+				stat := NewStatusOutput(dumb, "", false)
+				tt.calls(stat)
+				stat.Flush()
+
+				if g, w := dumb.String(), tt.dumb; g != w {
+					t.Errorf("want:\n%q\ngot:\n%q", w, g)
+				}
+			})
+		})
+	}
+}
+
+type runner struct {
+	counts status.Counts
+	stat   status.StatusOutput
+}
+
+func newRunner(stat status.StatusOutput, totalActions int) *runner {
+	return &runner{
+		counts: status.Counts{TotalActions: totalActions},
+		stat:   stat,
+	}
+}
+
+func (r *runner) startAction(action *status.Action) {
+	r.counts.StartedActions++
+	r.counts.RunningActions++
+	r.stat.StartAction(action, r.counts)
+}
+
+func (r *runner) finishAction(result status.ActionResult) {
+	r.counts.FinishedActions++
+	r.counts.RunningActions--
+	r.stat.FinishAction(result, r.counts)
+}
+
+func (r *runner) finishAndStartAction(result status.ActionResult, action *status.Action) {
+	r.counts.FinishedActions++
+	r.stat.FinishAction(result, r.counts)
+
+	r.counts.StartedActions++
+	r.stat.StartAction(action, r.counts)
+}
+
+var (
+	action1 = &status.Action{Description: "action1"}
+	result1 = status.ActionResult{Action: action1}
+	action2 = &status.Action{Description: "action2"}
+	result2 = status.ActionResult{Action: action2}
+	action3 = &status.Action{Description: "action3"}
+	result3 = status.ActionResult{Action: action3}
+)
+
+func twoActions(stat status.StatusOutput) {
+	runner := newRunner(stat, 2)
+	runner.startAction(action1)
+	runner.finishAction(result1)
+	runner.startAction(action2)
+	runner.finishAction(result2)
+}
+
+func twoParallelActions(stat status.StatusOutput) {
+	runner := newRunner(stat, 2)
+	runner.startAction(action1)
+	runner.startAction(action2)
+	runner.finishAction(result1)
+	runner.finishAction(result2)
+}
+
+func actionsWithOutput(stat status.StatusOutput) {
+	result2WithOutput := status.ActionResult{Action: action2, Output: "output1\noutput2\n"}
+
+	runner := newRunner(stat, 3)
+	runner.startAction(action1)
+	runner.finishAction(result1)
+	runner.startAction(action2)
+	runner.finishAction(result2WithOutput)
+	runner.startAction(action3)
+	runner.finishAction(result3)
+}
+
+func actionsWithOutputWithoutNewline(stat status.StatusOutput) {
+	result2WithOutputWithoutNewline := status.ActionResult{Action: action2, Output: "output1\noutput2"}
+
+	runner := newRunner(stat, 3)
+	runner.startAction(action1)
+	runner.finishAction(result1)
+	runner.startAction(action2)
+	runner.finishAction(result2WithOutputWithoutNewline)
+	runner.startAction(action3)
+	runner.finishAction(result3)
+}
+
+func actionsWithError(stat status.StatusOutput) {
+	action2WithError := &status.Action{Description: "action2", Outputs: []string{"f1", "f2"}, Command: "touch f1 f2"}
+	result2WithError := status.ActionResult{Action: action2WithError, Output: "error1\nerror2\n", Error: fmt.Errorf("error1")}
+
+	runner := newRunner(stat, 3)
+	runner.startAction(action1)
+	runner.finishAction(result1)
+	runner.startAction(action2WithError)
+	runner.finishAction(result2WithError)
+	runner.startAction(action3)
+	runner.finishAction(result3)
+}
+
+func actionWithEmptyDescription(stat status.StatusOutput) {
+	action1 := &status.Action{Command: "command1"}
+	result1 := status.ActionResult{Action: action1}
+
+	runner := newRunner(stat, 1)
+	runner.startAction(action1)
+	runner.finishAction(result1)
+}
+
+func actionsWithMessages(stat status.StatusOutput) {
+	runner := newRunner(stat, 2)
+
+	runner.startAction(action1)
+	runner.finishAction(result1)
+
+	stat.Message(status.VerboseLvl, "verbose")
+	stat.Message(status.StatusLvl, "status")
+	stat.Message(status.PrintLvl, "print")
+	stat.Message(status.ErrorLvl, "error")
+
+	runner.startAction(action2)
+	runner.finishAction(result2)
+}
+
+func actionWithLongDescription(stat status.StatusOutput) {
+	action1 := &status.Action{Description: "action with very long description to test eliding"}
+	result1 := status.ActionResult{Action: action1}
+
+	runner := newRunner(stat, 2)
+
+	runner.startAction(action1)
+
+	runner.finishAction(result1)
+}
+
+func actionWithOuptutWithAnsiCodes(stat status.StatusOutput) {
+	result1WithOutputWithAnsiCodes := status.ActionResult{Action: action1, Output: "\x1b[31mcolor\x1b[0m"}
+
+	runner := newRunner(stat, 1)
+	runner.startAction(action1)
+	runner.finishAction(result1WithOutputWithAnsiCodes)
+}
+
+func TestSmartStatusOutputWidthChange(t *testing.T) {
+	os.Setenv(tableHeightEnVar, "")
+
+	smart := &fakeSmartTerminal{termWidth: 40}
+	stat := NewStatusOutput(smart, "", false)
+	smartStat := stat.(*smartStatusOutput)
+	smartStat.sigwinchHandled = make(chan bool)
+
+	runner := newRunner(stat, 2)
+
+	action := &status.Action{Description: "action with very long description to test eliding"}
+	result := status.ActionResult{Action: action}
+
+	runner.startAction(action)
+	smart.termWidth = 30
+	// Fake a SIGWINCH
+	smartStat.sigwinch <- syscall.SIGWINCH
+	<-smartStat.sigwinchHandled
+	runner.finishAction(result)
+
+	stat.Flush()
+
+	w := "\r\x1b[1m[  0% 0/2] action with very long descrip\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action with very lo\x1b[0m\x1b[K\n"
+
+	if g := smart.String(); g != w {
+		t.Errorf("want:\n%q\ngot:\n%q", w, g)
+	}
+}
diff --git a/ui/terminal/stdio.go b/ui/terminal/stdio.go
new file mode 100644
index 0000000..dec2963
--- /dev/null
+++ b/ui/terminal/stdio.go
@@ -0,0 +1,55 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package terminal provides a set of interfaces that can be used to interact
+// with the terminal (including falling back when the terminal is detected to
+// be a redirect or other dumb terminal)
+package terminal
+
+import (
+	"io"
+	"os"
+)
+
+// StdioInterface represents a set of stdin/stdout/stderr Reader/Writers
+type StdioInterface interface {
+	Stdin() io.Reader
+	Stdout() io.Writer
+	Stderr() io.Writer
+}
+
+// StdioImpl uses the OS stdin/stdout/stderr to implement StdioInterface
+type StdioImpl struct{}
+
+func (StdioImpl) Stdin() io.Reader  { return os.Stdin }
+func (StdioImpl) Stdout() io.Writer { return os.Stdout }
+func (StdioImpl) Stderr() io.Writer { return os.Stderr }
+
+var _ StdioInterface = StdioImpl{}
+
+type customStdio struct {
+	stdin  io.Reader
+	stdout io.Writer
+	stderr io.Writer
+}
+
+func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface {
+	return customStdio{stdin, stdout, stderr}
+}
+
+func (c customStdio) Stdin() io.Reader  { return c.stdin }
+func (c customStdio) Stdout() io.Writer { return c.stdout }
+func (c customStdio) Stderr() io.Writer { return c.stderr }
+
+var _ StdioInterface = customStdio{}
diff --git a/ui/terminal/util.go b/ui/terminal/util.go
index a85a517..c9377f1 100644
--- a/ui/terminal/util.go
+++ b/ui/terminal/util.go
@@ -22,18 +22,20 @@
 	"unsafe"
 )
 
-func isTerminal(w io.Writer) bool {
+func isSmartTerminal(w io.Writer) bool {
 	if f, ok := w.(*os.File); ok {
 		var termios syscall.Termios
 		_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
 			ioctlGetTermios, uintptr(unsafe.Pointer(&termios)),
 			0, 0, 0)
 		return err == 0
+	} else if _, ok := w.(*fakeSmartTerminal); ok {
+		return true
 	}
 	return false
 }
 
-func termWidth(w io.Writer) (int, bool) {
+func termSize(w io.Writer) (width int, height int, ok bool) {
 	if f, ok := w.(*os.File); ok {
 		var winsize struct {
 			ws_row, ws_column    uint16
@@ -42,9 +44,11 @@
 		_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
 			syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&winsize)),
 			0, 0, 0)
-		return int(winsize.ws_column), err == 0
+		return int(winsize.ws_column), int(winsize.ws_row), err == 0
+	} else if f, ok := w.(*fakeSmartTerminal); ok {
+		return f.termWidth, f.termHeight, true
 	}
-	return 0, false
+	return 0, 0, false
 }
 
 // stripAnsiEscapes strips ANSI control codes from a byte array in place.
@@ -99,3 +103,8 @@
 
 	return input
 }
+
+type fakeSmartTerminal struct {
+	bytes.Buffer
+	termWidth, termHeight int
+}
diff --git a/ui/terminal/writer.go b/ui/terminal/writer.go
deleted file mode 100644
index ebe4b2a..0000000
--- a/ui/terminal/writer.go
+++ /dev/null
@@ -1,229 +0,0 @@
-// Copyright 2018 Google Inc. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// Package terminal provides a set of interfaces that can be used to interact
-// with the terminal (including falling back when the terminal is detected to
-// be a redirect or other dumb terminal)
-package terminal
-
-import (
-	"fmt"
-	"io"
-	"os"
-	"strings"
-	"sync"
-)
-
-// Writer provides an interface to write temporary and permanent messages to
-// the terminal.
-//
-// The terminal is considered to be a dumb terminal if TERM==dumb, or if a
-// terminal isn't detected on stdout/stderr (generally because it's a pipe or
-// file). Dumb terminals will strip out all ANSI escape sequences, including
-// colors.
-type Writer interface {
-	// Print prints the string to the terminal, overwriting any current
-	// status being displayed.
-	//
-	// On a dumb terminal, the status messages will be kept.
-	Print(str string)
-
-	// Status prints the first line of the string to the terminal,
-	// overwriting any previous status line. Strings longer than the width
-	// of the terminal will be cut off.
-	//
-	// On a dumb terminal, previous status messages will remain, and the
-	// entire first line of the string will be printed.
-	StatusLine(str string)
-
-	// StatusAndMessage prints the first line of status to the terminal,
-	// similarly to StatusLine(), then prints the full msg below that. The
-	// status line is retained.
-	//
-	// There is guaranteed to be no other output in between the status and
-	// message.
-	StatusAndMessage(status, msg string)
-
-	// Finish ensures that the output ends with a newline (preserving any
-	// current status line that is current displayed).
-	//
-	// This does nothing on dumb terminals.
-	Finish()
-
-	// Write implements the io.Writer interface. This is primarily so that
-	// the logger can use this interface to print to stderr without
-	// breaking the other semantics of this interface.
-	//
-	// Try to use any of the other functions if possible.
-	Write(p []byte) (n int, err error)
-
-	isSmartTerminal() bool
-}
-
-// NewWriter creates a new Writer based on the stdio and the TERM
-// environment variable.
-func NewWriter(stdio StdioInterface) Writer {
-	w := &writerImpl{
-		stdio: stdio,
-
-		haveBlankLine: true,
-	}
-
-	if term, ok := os.LookupEnv("TERM"); ok && term != "dumb" {
-		w.smartTerminal = isTerminal(stdio.Stdout())
-	}
-	w.stripEscapes = !w.smartTerminal
-
-	return w
-}
-
-type writerImpl struct {
-	stdio StdioInterface
-
-	haveBlankLine bool
-
-	// Protecting the above, we assume that smartTerminal and stripEscapes
-	// does not change after initial setup.
-	lock sync.Mutex
-
-	smartTerminal bool
-	stripEscapes  bool
-}
-
-func (w *writerImpl) isSmartTerminal() bool {
-	return w.smartTerminal
-}
-
-func (w *writerImpl) requestLine() {
-	if !w.haveBlankLine {
-		fmt.Fprintln(w.stdio.Stdout())
-		w.haveBlankLine = true
-	}
-}
-
-func (w *writerImpl) Print(str string) {
-	if w.stripEscapes {
-		str = string(stripAnsiEscapes([]byte(str)))
-	}
-
-	w.lock.Lock()
-	defer w.lock.Unlock()
-	w.print(str)
-}
-
-func (w *writerImpl) print(str string) {
-	if !w.haveBlankLine {
-		fmt.Fprint(w.stdio.Stdout(), "\r", "\x1b[K")
-		w.haveBlankLine = true
-	}
-	fmt.Fprint(w.stdio.Stdout(), str)
-	if len(str) == 0 || str[len(str)-1] != '\n' {
-		fmt.Fprint(w.stdio.Stdout(), "\n")
-	}
-}
-
-func (w *writerImpl) StatusLine(str string) {
-	w.lock.Lock()
-	defer w.lock.Unlock()
-
-	w.statusLine(str)
-}
-
-func (w *writerImpl) statusLine(str string) {
-	if !w.smartTerminal {
-		fmt.Fprintln(w.stdio.Stdout(), str)
-		return
-	}
-
-	idx := strings.IndexRune(str, '\n')
-	if idx != -1 {
-		str = str[0:idx]
-	}
-
-	// Limit line width to the terminal width, otherwise we'll wrap onto
-	// another line and we won't delete the previous line.
-	//
-	// Run this on every line in case the window has been resized while
-	// we're printing. This could be optimized to only re-run when we get
-	// SIGWINCH if it ever becomes too time consuming.
-	if max, ok := termWidth(w.stdio.Stdout()); ok {
-		if len(str) > max {
-			// TODO: Just do a max. Ninja elides the middle, but that's
-			// more complicated and these lines aren't that important.
-			str = str[:max]
-		}
-	}
-
-	// Move to the beginning on the line, print the output, then clear
-	// the rest of the line.
-	fmt.Fprint(w.stdio.Stdout(), "\r", str, "\x1b[K")
-	w.haveBlankLine = false
-}
-
-func (w *writerImpl) StatusAndMessage(status, msg string) {
-	if w.stripEscapes {
-		msg = string(stripAnsiEscapes([]byte(msg)))
-	}
-
-	w.lock.Lock()
-	defer w.lock.Unlock()
-
-	w.statusLine(status)
-	w.requestLine()
-	w.print(msg)
-}
-
-func (w *writerImpl) Finish() {
-	w.lock.Lock()
-	defer w.lock.Unlock()
-
-	w.requestLine()
-}
-
-func (w *writerImpl) Write(p []byte) (n int, err error) {
-	w.Print(string(p))
-	return len(p), nil
-}
-
-// StdioInterface represents a set of stdin/stdout/stderr Reader/Writers
-type StdioInterface interface {
-	Stdin() io.Reader
-	Stdout() io.Writer
-	Stderr() io.Writer
-}
-
-// StdioImpl uses the OS stdin/stdout/stderr to implement StdioInterface
-type StdioImpl struct{}
-
-func (StdioImpl) Stdin() io.Reader  { return os.Stdin }
-func (StdioImpl) Stdout() io.Writer { return os.Stdout }
-func (StdioImpl) Stderr() io.Writer { return os.Stderr }
-
-var _ StdioInterface = StdioImpl{}
-
-type customStdio struct {
-	stdin  io.Reader
-	stdout io.Writer
-	stderr io.Writer
-}
-
-func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface {
-	return customStdio{stdin, stdout, stderr}
-}
-
-func (c customStdio) Stdin() io.Reader  { return c.stdin }
-func (c customStdio) Stdout() io.Writer { return c.stdout }
-func (c customStdio) Stderr() io.Writer { return c.stderr }
-
-var _ StdioInterface = customStdio{}
diff --git a/ui/tracer/status.go b/ui/tracer/status.go
index af50e2d..c831255 100644
--- a/ui/tracer/status.go
+++ b/ui/tracer/status.go
@@ -85,3 +85,8 @@
 
 func (s *statusOutput) Flush()                                        {}
 func (s *statusOutput) Message(level status.MsgLevel, message string) {}
+
+func (s *statusOutput) Write(p []byte) (int, error) {
+	// Discard writes
+	return len(p), nil
+}
diff --git a/zip/cmd/main.go b/zip/cmd/main.go
index 6f40a3e..fba2e4b 100644
--- a/zip/cmd/main.go
+++ b/zip/cmd/main.go
@@ -136,6 +136,7 @@
 	writeIfChanged := flags.Bool("write_if_changed", false, "only update resultant .zip if it has changed")
 	ignoreMissingFiles := flags.Bool("ignore_missing_files", false, "continue if a requested file does not exist")
 	symlinks := flags.Bool("symlinks", true, "store symbolic links in zip instead of following them")
+	srcJar := flags.Bool("srcjar", false, "move .java files to locations that match their package statement")
 
 	parallelJobs := flags.Int("parallel", runtime.NumCPU(), "number of parallel threads to use")
 	cpuProfile := flags.String("cpuprofile", "", "write cpu profile to file")
@@ -191,6 +192,7 @@
 		FileArgs:                 fileArgsBuilder.FileArgs(),
 		OutputFilePath:           *out,
 		EmulateJar:               *emulateJar,
+		SrcJar:                   *srcJar,
 		AddDirectoryEntriesToZip: *directories,
 		CompressionLevel:         *compLevel,
 		ManifestSourcePath:       *manifest,
diff --git a/zip/zip.go b/zip/zip.go
index 1f5fe43..707c4ef 100644
--- a/zip/zip.go
+++ b/zip/zip.go
@@ -210,6 +210,7 @@
 	FileArgs                 []FileArg
 	OutputFilePath           string
 	EmulateJar               bool
+	SrcJar                   bool
 	AddDirectoryEntriesToZip bool
 	CompressionLevel         int
 	ManifestSourcePath       string
@@ -364,7 +365,7 @@
 		}
 	}
 
-	return z.write(w, pathMappings, args.ManifestSourcePath, args.EmulateJar, args.NumParallelJobs)
+	return z.write(w, pathMappings, args.ManifestSourcePath, args.EmulateJar, args.SrcJar, args.NumParallelJobs)
 }
 
 func Zip(args ZipArgs) error {
@@ -446,7 +447,9 @@
 	sort.SliceStable(mappings, less)
 }
 
-func (z *ZipWriter) write(f io.Writer, pathMappings []pathMapping, manifest string, emulateJar bool, parallelJobs int) error {
+func (z *ZipWriter) write(f io.Writer, pathMappings []pathMapping, manifest string, emulateJar, srcJar bool,
+	parallelJobs int) error {
+
 	z.errors = make(chan error)
 	defer close(z.errors)
 
@@ -489,7 +492,7 @@
 			if emulateJar && ele.dest == jar.ManifestFile {
 				err = z.addManifest(ele.dest, ele.src, ele.zipMethod)
 			} else {
-				err = z.addFile(ele.dest, ele.src, ele.zipMethod, emulateJar)
+				err = z.addFile(ele.dest, ele.src, ele.zipMethod, emulateJar, srcJar)
 			}
 			if err != nil {
 				z.errors <- err
@@ -588,7 +591,7 @@
 }
 
 // imports (possibly with compression) <src> into the zip at sub-path <dest>
-func (z *ZipWriter) addFile(dest, src string, method uint16, emulateJar bool) error {
+func (z *ZipWriter) addFile(dest, src string, method uint16, emulateJar, srcJar bool) error {
 	var fileSize int64
 	var executable bool
 
@@ -606,12 +609,9 @@
 			return nil
 		}
 		return err
-	} else if s.IsDir() {
-		if z.directories {
-			return z.writeDirectory(dest, src, emulateJar)
-		}
-		return nil
-	} else {
+	}
+
+	createParentDirs := func(dest, src string) error {
 		if err := z.writeDirectory(filepath.Dir(dest), src, emulateJar); err != nil {
 			return err
 		}
@@ -625,32 +625,64 @@
 
 		z.createdFiles[dest] = src
 
-		if s.Mode()&os.ModeSymlink != 0 {
-			return z.writeSymlink(dest, src)
-		} else if !s.Mode().IsRegular() {
-			return fmt.Errorf("%s is not a file, directory, or symlink", src)
+		return nil
+	}
+
+	if s.IsDir() {
+		if z.directories {
+			return z.writeDirectory(dest, src, emulateJar)
+		}
+		return nil
+	} else if s.Mode()&os.ModeSymlink != 0 {
+		err = createParentDirs(dest, src)
+		if err != nil {
+			return err
+		}
+
+		return z.writeSymlink(dest, src)
+	} else if s.Mode().IsRegular() {
+		r, err := z.fs.Open(src)
+		if err != nil {
+			return err
+		}
+
+		if srcJar && filepath.Ext(src) == ".java" {
+			// rewrite the destination using the package path if it can be determined
+			pkg, err := jar.JavaPackage(r, src)
+			if err != nil {
+				// ignore errors for now, leaving the file at in its original location in the zip
+			} else {
+				dest = filepath.Join(filepath.Join(strings.Split(pkg, ".")...), filepath.Base(src))
+			}
+
+			_, err = r.Seek(0, io.SeekStart)
+			if err != nil {
+				return err
+			}
 		}
 
 		fileSize = s.Size()
 		executable = s.Mode()&0100 != 0
-	}
 
-	r, err := z.fs.Open(src)
-	if err != nil {
-		return err
-	}
+		header := &zip.FileHeader{
+			Name:               dest,
+			Method:             method,
+			UncompressedSize64: uint64(fileSize),
+		}
 
-	header := &zip.FileHeader{
-		Name:               dest,
-		Method:             method,
-		UncompressedSize64: uint64(fileSize),
-	}
+		if executable {
+			header.SetMode(0700)
+		}
 
-	if executable {
-		header.SetMode(0700)
-	}
+		err = createParentDirs(dest, src)
+		if err != nil {
+			return err
+		}
 
-	return z.writeFileContents(header, r)
+		return z.writeFileContents(header, r)
+	} else {
+		return fmt.Errorf("%s is not a file, directory, or symlink", src)
+	}
 }
 
 func (z *ZipWriter) addManifest(dest string, src string, method uint16) error {
diff --git a/zip/zip_test.go b/zip/zip_test.go
index 93c5f3d..84317d1 100644
--- a/zip/zip_test.go
+++ b/zip/zip_test.go
@@ -40,14 +40,15 @@
 )
 
 var mockFs = pathtools.MockFs(map[string][]byte{
-	"a/a/a":            fileA,
-	"a/a/b":            fileB,
-	"a/a/c -> ../../c": nil,
-	"a/a/d -> b":       nil,
-	"c":                fileC,
-	"l":                []byte("a/a/a\na/a/b\nc\n"),
-	"l2":               []byte("missing\n"),
-	"manifest.txt":     fileCustomManifest,
+	"a/a/a":               fileA,
+	"a/a/b":               fileB,
+	"a/a/c -> ../../c":    nil,
+	"dangling -> missing": nil,
+	"a/a/d -> b":          nil,
+	"c":                   fileC,
+	"l":                   []byte("a/a/a\na/a/b\nc\n"),
+	"l2":                  []byte("missing\n"),
+	"manifest.txt":        fileCustomManifest,
 })
 
 func fh(name string, contents []byte, method uint16) zip.FileHeader {
@@ -210,6 +211,17 @@
 			},
 		},
 		{
+			name: "dangling symlinks",
+			args: fileArgsBuilder().
+				File("dangling"),
+			compressionLevel: 9,
+			storeSymlinks:    true,
+
+			files: []zip.FileHeader{
+				fhLink("dangling", "missing"),
+			},
+		},
+		{
 			name: "list",
 			args: fileArgsBuilder().
 				List("l"),
@@ -554,3 +566,70 @@
 		})
 	}
 }
+
+func TestSrcJar(t *testing.T) {
+	mockFs := pathtools.MockFs(map[string][]byte{
+		"wrong_package.java":       []byte("package foo;"),
+		"foo/correct_package.java": []byte("package foo;"),
+		"src/no_package.java":      nil,
+		"src2/parse_error.java":    []byte("error"),
+	})
+
+	want := []string{
+		"foo/",
+		"foo/wrong_package.java",
+		"foo/correct_package.java",
+		"no_package.java",
+		"src2/",
+		"src2/parse_error.java",
+	}
+
+	args := ZipArgs{}
+	args.FileArgs = NewFileArgsBuilder().File("**/*.java").FileArgs()
+
+	args.SrcJar = true
+	args.AddDirectoryEntriesToZip = true
+	args.Filesystem = mockFs
+	args.Stderr = &bytes.Buffer{}
+
+	buf := &bytes.Buffer{}
+	err := ZipTo(args, buf)
+	if err != nil {
+		t.Fatalf("got error %v", err)
+	}
+
+	br := bytes.NewReader(buf.Bytes())
+	zr, err := zip.NewReader(br, int64(br.Len()))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var got []string
+	for _, f := range zr.File {
+		r, err := f.Open()
+		if err != nil {
+			t.Fatalf("error when opening %s: %s", f.Name, err)
+		}
+
+		crc := crc32.NewIEEE()
+		len, err := io.Copy(crc, r)
+		r.Close()
+		if err != nil {
+			t.Fatalf("error when reading %s: %s", f.Name, err)
+		}
+
+		if uint64(len) != f.UncompressedSize64 {
+			t.Errorf("incorrect length for %s, want %d got %d", f.Name, f.UncompressedSize64, len)
+		}
+
+		if crc.Sum32() != f.CRC32 {
+			t.Errorf("incorrect crc for %s, want %x got %x", f.Name, f.CRC32, crc)
+		}
+
+		got = append(got, f.Name)
+	}
+
+	if !reflect.DeepEqual(want, got) {
+		t.Errorf("want files %q, got %q", want, got)
+	}
+}