Merge "Move common functions to script_utils.sh"
diff --git a/android/bazel_handler.go b/android/bazel_handler.go
index e9c97d0..c2fd954 100644
--- a/android/bazel_handler.go
+++ b/android/bazel_handler.go
@@ -36,11 +36,6 @@
 )
 
 var (
-	writeBazelFile = pctx.AndroidStaticRule("bazelWriteFileRule", blueprint.RuleParams{
-		Command:        `sed "s/\\\\n/\n/g" ${out}.rsp >${out}`,
-		Rspfile:        "${out}.rsp",
-		RspfileContent: "${content}",
-	}, "content")
 	_                 = pctx.HostBinToolVariable("bazelBuildRunfilesTool", "build-runfiles")
 	buildRunfilesRule = pctx.AndroidStaticRule("bazelBuildRunfiles", blueprint.RuleParams{
 		Command:     "${bazelBuildRunfilesTool} ${in} ${outDir}",
@@ -437,16 +432,26 @@
 	vars := []struct {
 		name string
 		ptr  *string
+
+		// True if the environment variable needs to be tracked so that changes to the variable
+		// cause the ninja file to be regenerated, false otherwise. False should only be set for
+		// environment variables that have no effect on the generated ninja file.
+		track bool
 	}{
-		{"BAZEL_HOME", &paths.homeDir},
-		{"BAZEL_PATH", &paths.bazelPath},
-		{"BAZEL_OUTPUT_BASE", &paths.outputBase},
-		{"BAZEL_WORKSPACE", &paths.workspaceDir},
-		{"BAZEL_METRICS_DIR", &paths.metricsDir},
-		{"BAZEL_DEPS_FILE", &paths.bazelDepsFile},
+		{"BAZEL_HOME", &paths.homeDir, true},
+		{"BAZEL_PATH", &paths.bazelPath, true},
+		{"BAZEL_OUTPUT_BASE", &paths.outputBase, true},
+		{"BAZEL_WORKSPACE", &paths.workspaceDir, true},
+		{"BAZEL_METRICS_DIR", &paths.metricsDir, false},
+		{"BAZEL_DEPS_FILE", &paths.bazelDepsFile, true},
 	}
 	for _, v := range vars {
-		if s := c.Getenv(v.name); len(s) > 1 {
+		if v.track {
+			if s := c.Getenv(v.name); len(s) > 1 {
+				*v.ptr = s
+				continue
+			}
+		} else if s, ok := c.env[v.name]; ok {
 			*v.ptr = s
 		} else {
 			missing = append(missing, v.name)
@@ -1089,19 +1094,8 @@
 		// because this would cause circular dependency. So, until we move aquery processing
 		// to the 'android' package, we need to handle special cases here.
 		if buildStatement.Mnemonic == "FileWrite" || buildStatement.Mnemonic == "SourceSymlinkManifest" {
-			// Pass file contents as the value of the rule's "content" argument.
-			// Escape newlines and $ in the contents (the action "writeBazelFile" restores "\\n"
-			// back to the newline, and Ninja reads $$ as $.
-			escaped := strings.ReplaceAll(strings.ReplaceAll(buildStatement.FileContents, "\n", "\\n"),
-				"$", "$$")
-			ctx.Build(pctx, BuildParams{
-				Rule:        writeBazelFile,
-				Output:      PathForBazelOut(ctx, buildStatement.OutputPaths[0]),
-				Description: fmt.Sprintf("%s %s", buildStatement.Mnemonic, buildStatement.OutputPaths[0]),
-				Args: map[string]string{
-					"content": escaped,
-				},
-			})
+			out := PathForBazelOut(ctx, buildStatement.OutputPaths[0])
+			WriteFileRuleVerbatim(ctx, out, buildStatement.FileContents)
 		} else if buildStatement.Mnemonic == "SymlinkTree" {
 			// build-runfiles arguments are the manifest file and the target directory
 			// where it creates the symlink tree according to this manifest (and then
diff --git a/android/buildinfo_prop.go b/android/buildinfo_prop.go
index acebdbb..46f6488 100644
--- a/android/buildinfo_prop.go
+++ b/android/buildinfo_prop.go
@@ -61,11 +61,10 @@
 		return
 	}
 
-	rule := NewRuleBuilder(pctx, ctx)
-	cmd := rule.Command().Text("(")
+	lines := make([]string, 0)
 
 	writeString := func(str string) {
-		cmd.Text(`echo "` + str + `" && `)
+		lines = append(lines, str)
 	}
 
 	writeString("# begin build properties")
@@ -142,8 +141,7 @@
 
 	writeString("# end build properties")
 
-	cmd.Text("true) > ").Output(p.outputFilePath)
-	rule.Build("build.prop", "generating build.prop")
+	WriteFileRule(ctx, p.outputFilePath, strings.Join(lines, "\n"))
 
 	if !p.installable() {
 		p.SkipInstall()
diff --git a/android/defs.go b/android/defs.go
index 9ae360e..6e5bb05 100644
--- a/android/defs.go
+++ b/android/defs.go
@@ -174,10 +174,15 @@
 // WriteFileRule creates a ninja rule to write contents to a file.  The contents will be escaped
 // so that the file contains exactly the contents passed to the function, plus a trailing newline.
 func WriteFileRule(ctx BuilderContext, outputFile WritablePath, content string) {
+	WriteFileRuleVerbatim(ctx, outputFile, content+"\n")
+}
+
+// WriteFileRuleVerbatim creates a ninja rule to write contents to a file.  The contents will be
+// escaped so that the file contains exactly the contents passed to the function.
+func WriteFileRuleVerbatim(ctx BuilderContext, outputFile WritablePath, content string) {
 	// This is MAX_ARG_STRLEN subtracted with some safety to account for shell escapes
 	const SHARD_SIZE = 131072 - 10000
 
-	content += "\n"
 	if len(content) > SHARD_SIZE {
 		var chunks WritablePaths
 		for i, c := range ShardString(content, SHARD_SIZE) {
diff --git a/apex/builder.go b/apex/builder.go
index 82a523c..3b9cac0 100644
--- a/apex/builder.go
+++ b/apex/builder.go
@@ -179,13 +179,6 @@
 		Description: "app bundle",
 	}, "abi", "config")
 
-	emitApexContentRule = pctx.StaticRule("emitApexContentRule", blueprint.RuleParams{
-		Command:        `rm -f ${out} && touch ${out} && (. ${out}.emit_commands)`,
-		Rspfile:        "${out}.emit_commands",
-		RspfileContent: "${emit_commands}",
-		Description:    "Emit APEX image content",
-	}, "emit_commands")
-
 	diffApexContentRule = pctx.StaticRule("diffApexContentRule", blueprint.RuleParams{
 		Command: `diff --unchanged-group-format='' \` +
 			`--changed-group-format='%<' \` +
@@ -546,29 +539,20 @@
 	// to be using this at this moment. Furthermore, this looks very similar to what
 	// buildInstalledFilesFile does. At least, move this to somewhere else so that this doesn't
 	// hurt readability.
-	// TODO(jiyong): use RuleBuilder
 	if a.overridableProperties.Allowed_files != nil {
 		// Build content.txt
-		var emitCommands []string
+		var contentLines []string
 		imageContentFile := android.PathForModuleOut(ctx, "content.txt")
-		emitCommands = append(emitCommands, "echo ./apex_manifest.pb >> "+imageContentFile.String())
+		contentLines = append(contentLines, "./apex_manifest.pb")
 		minSdkVersion := a.minSdkVersion(ctx)
 		if minSdkVersion.EqualTo(android.SdkVersion_Android10) {
-			emitCommands = append(emitCommands, "echo ./apex_manifest.json >> "+imageContentFile.String())
+			contentLines = append(contentLines, "./apex_manifest.json")
 		}
 		for _, fi := range a.filesInfo {
-			emitCommands = append(emitCommands, "echo './"+fi.path()+"' >> "+imageContentFile.String())
+			contentLines = append(contentLines, "./"+fi.path())
 		}
-		emitCommands = append(emitCommands, "sort -o "+imageContentFile.String()+" "+imageContentFile.String())
-		ctx.Build(pctx, android.BuildParams{
-			Rule:        emitApexContentRule,
-			Implicits:   implicitInputs,
-			Output:      imageContentFile,
-			Description: "emit apex image content",
-			Args: map[string]string{
-				"emit_commands": strings.Join(emitCommands, " && "),
-			},
-		})
+		sort.Strings(contentLines)
+		android.WriteFileRule(ctx, imageContentFile, strings.Join(contentLines, "\n"))
 		implicitInputs = append(implicitInputs, imageContentFile)
 
 		// Compare content.txt against allowed_files.
diff --git a/bp2build/java_library_conversion_test.go b/bp2build/java_library_conversion_test.go
index e37fa62..84c664e 100644
--- a/bp2build/java_library_conversion_test.go
+++ b/bp2build/java_library_conversion_test.go
@@ -685,6 +685,7 @@
         "c.kt",
     ]`,
 			}),
+			MakeNeverlinkDuplicateTarget("kt_jvm_library", "java-lib-1"),
 		},
 	})
 }
@@ -707,6 +708,7 @@
     ]`,
 				"common_srcs": `["c.kt"]`,
 			}),
+			MakeNeverlinkDuplicateTarget("kt_jvm_library", "java-lib-1"),
 		},
 	})
 }
diff --git a/cc/test.go b/cc/test.go
index 2a4861c..16ef0ef 100644
--- a/cc/test.go
+++ b/cc/test.go
@@ -415,8 +415,16 @@
 	testInstallBase := getTestInstallBase(useVendor)
 	configs := getTradefedConfigOptions(ctx, &test.Properties, test.isolated(ctx))
 
-	test.testConfig = tradefed.AutoGenNativeTestConfig(ctx, test.Properties.Test_config,
-		test.Properties.Test_config_template, test.testDecorator.InstallerProperties.Test_suites, configs, test.Properties.Auto_gen_config, testInstallBase)
+	test.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
+		TestConfigProp:         test.Properties.Test_config,
+		TestConfigTemplateProp: test.Properties.Test_config_template,
+		TestSuites:             test.testDecorator.InstallerProperties.Test_suites,
+		Config:                 configs,
+		AutoGenConfig:          test.Properties.Auto_gen_config,
+		TestInstallBase:        testInstallBase,
+		DeviceTemplate:         "${NativeTestConfigTemplate}",
+		HostTemplate:           "${NativeHostTestConfigTemplate}",
+	})
 
 	test.extraTestConfigs = android.PathsForModuleSrc(ctx, test.Properties.Test_options.Extra_test_configs)
 
@@ -630,8 +638,15 @@
 	if Bool(benchmark.Properties.Require_root) {
 		configs = append(configs, tradefed.Object{"target_preparer", "com.android.tradefed.targetprep.RootTargetPreparer", nil})
 	}
-	benchmark.testConfig = tradefed.AutoGenNativeBenchmarkTestConfig(ctx, benchmark.Properties.Test_config,
-		benchmark.Properties.Test_config_template, benchmark.Properties.Test_suites, configs, benchmark.Properties.Auto_gen_config)
+	benchmark.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
+		TestConfigProp:         benchmark.Properties.Test_config,
+		TestConfigTemplateProp: benchmark.Properties.Test_config_template,
+		TestSuites:             benchmark.Properties.Test_suites,
+		Config:                 configs,
+		AutoGenConfig:          benchmark.Properties.Auto_gen_config,
+		DeviceTemplate:         "${NativeBenchmarkTestConfigTemplate}",
+		HostTemplate:           "${NativeBenchmarkTestConfigTemplate}",
+	})
 
 	benchmark.binaryDecorator.baseInstaller.dir = filepath.Join("benchmarktest", ctx.ModuleName())
 	benchmark.binaryDecorator.baseInstaller.dir64 = filepath.Join("benchmarktest64", ctx.ModuleName())
diff --git a/cmd/soong_build/main.go b/cmd/soong_build/main.go
index 744a10c..fe928ad 100644
--- a/cmd/soong_build/main.go
+++ b/cmd/soong_build/main.go
@@ -277,7 +277,7 @@
 	switch ctx.Config().BuildMode {
 	case android.GenerateModuleGraph:
 		stopBefore = bootstrap.StopBeforeWriteNinja
-	case android.GenerateQueryView | android.GenerateDocFile:
+	case android.GenerateQueryView, android.GenerateDocFile:
 		stopBefore = bootstrap.StopBeforePrepareBuildActions
 	default:
 		stopBefore = bootstrap.DoEverything
diff --git a/cmd/soong_ui/main.go b/cmd/soong_ui/main.go
index 713ccbe..8c99988 100644
--- a/cmd/soong_ui/main.go
+++ b/cmd/soong_ui/main.go
@@ -58,7 +58,7 @@
 	stdio func() terminal.StdioInterface
 
 	// run the command
-	run func(ctx build.Context, config build.Config, args []string, logsDir string)
+	run func(ctx build.Context, config build.Config, args []string)
 }
 
 // list of supported commands (flags) supported by soong ui
@@ -91,6 +91,15 @@
 		config:      buildActionConfig,
 		stdio:       stdio,
 		run:         runMake,
+	}, {
+		flag:        "--upload-metrics-only",
+		description: "upload metrics without building anything",
+		config:      uploadOnlyConfig,
+		stdio:       stdio,
+		// Upload-only mode mostly skips to the metrics-uploading phase of soong_ui.
+		// However, this invocation marks the true "end of the build", and thus we
+		// need to update the total runtime of the build to include this upload step.
+		run: updateTotalRealTime,
 	},
 }
 
@@ -182,18 +191,49 @@
 	}}
 
 	config := c.config(buildCtx, args...)
+	config.SetLogsPrefix(c.logsPrefix)
+	logsDir := config.LogsDir()
+	buildStarted = config.BuildStartedTimeOrDefault(buildStarted)
 
+	buildErrorFile := filepath.Join(logsDir, c.logsPrefix+"build_error")
+	soongMetricsFile := filepath.Join(logsDir, c.logsPrefix+"soong_metrics")
+	rbeMetricsFile := filepath.Join(logsDir, c.logsPrefix+"rbe_metrics.pb")
+	bp2buildMetricsFile := filepath.Join(logsDir, c.logsPrefix+"bp2build_metrics.pb")
+	metricsFiles := []string{
+		buildErrorFile,           // build error strings
+		rbeMetricsFile,           // high level metrics related to remote build execution.
+		bp2buildMetricsFile,      // high level metrics related to bp2build.
+		soongMetricsFile,         // high level metrics related to this build system.
+		config.BazelMetricsDir(), // directory that contains a set of bazel metrics.
+	}
+
+	os.MkdirAll(logsDir, 0777)
+
+	log.SetOutput(filepath.Join(logsDir, c.logsPrefix+"soong.log"))
+
+	trace.SetOutput(filepath.Join(logsDir, c.logsPrefix+"build.trace"))
+
+	c.run(buildCtx, config, args)
+
+	defer met.Dump(soongMetricsFile)
+	if !config.SkipMetricsUpload() {
+		defer build.UploadMetrics(buildCtx, config, c.simpleOutput, buildStarted, metricsFiles...)
+	}
+
+}
+
+func logAndSymlinkSetup(buildCtx build.Context, config build.Config) {
+	log := buildCtx.ContextImpl.Logger
+	logsPrefix := config.GetLogsPrefix()
 	build.SetupOutDir(buildCtx, config)
-
-	// Set up files to be outputted in the log directory.
 	logsDir := config.LogsDir()
 
 	// Common list of metric file definition.
-	buildErrorFile := filepath.Join(logsDir, c.logsPrefix+"build_error")
-	rbeMetricsFile := filepath.Join(logsDir, c.logsPrefix+"rbe_metrics.pb")
-	soongMetricsFile := filepath.Join(logsDir, c.logsPrefix+"soong_metrics")
-	bp2buildMetricsFile := filepath.Join(logsDir, c.logsPrefix+"bp2build_metrics.pb")
-	soongBuildMetricsFile := filepath.Join(logsDir, c.logsPrefix+"soong_build_metrics.pb")
+	buildErrorFile := filepath.Join(logsDir, logsPrefix+"build_error")
+	rbeMetricsFile := filepath.Join(logsDir, logsPrefix+"rbe_metrics.pb")
+	soongMetricsFile := filepath.Join(logsDir, logsPrefix+"soong_metrics")
+	bp2buildMetricsFile := filepath.Join(logsDir, logsPrefix+"bp2build_metrics.pb")
+	soongBuildMetricsFile := filepath.Join(logsDir, logsPrefix+"soong_build_metrics.pb")
 
 	//Delete the stale metrics files
 	staleFileSlice := []string{buildErrorFile, rbeMetricsFile, soongMetricsFile, bp2buildMetricsFile, soongBuildMetricsFile}
@@ -203,14 +243,12 @@
 
 	build.PrintOutDirWarning(buildCtx, config)
 
-	os.MkdirAll(logsDir, 0777)
-	log.SetOutput(filepath.Join(logsDir, c.logsPrefix+"soong.log"))
-	trace.SetOutput(filepath.Join(logsDir, c.logsPrefix+"build.trace"))
-	stat.AddOutput(status.NewVerboseLog(log, filepath.Join(logsDir, c.logsPrefix+"verbose.log")))
-	stat.AddOutput(status.NewErrorLog(log, filepath.Join(logsDir, c.logsPrefix+"error.log")))
+	stat := buildCtx.Status
+	stat.AddOutput(status.NewVerboseLog(log, filepath.Join(logsDir, logsPrefix+"verbose.log")))
+	stat.AddOutput(status.NewErrorLog(log, filepath.Join(logsDir, logsPrefix+"error.log")))
 	stat.AddOutput(status.NewProtoErrorLog(log, buildErrorFile))
 	stat.AddOutput(status.NewCriticalPath(log))
-	stat.AddOutput(status.NewBuildProgressLog(log, filepath.Join(logsDir, c.logsPrefix+"build_progress.pb")))
+	stat.AddOutput(status.NewBuildProgressLog(log, filepath.Join(logsDir, logsPrefix+"build_progress.pb")))
 
 	buildCtx.Verbosef("Detected %.3v GB total RAM", float32(config.TotalRAM())/(1024*1024*1024))
 	buildCtx.Verbosef("Parallelism (local/remote/highmem): %v/%v/%v",
@@ -218,27 +256,7 @@
 
 	setMaxFiles(buildCtx)
 
-	{
-		// The order of the function calls is important. The last defer function call
-		// is the first one that is executed to save the rbe metrics to a protobuf
-		// file. The soong metrics file is then next. Bazel profiles are written
-		// before the uploadMetrics is invoked. The written files are then uploaded
-		// if the uploading of the metrics is enabled.
-		files := []string{
-			buildErrorFile,           // build error strings
-			rbeMetricsFile,           // high level metrics related to remote build execution.
-			soongBuildMetricsFile,    // high level metrics related to soong build(except bp2build).
-			bp2buildMetricsFile,      // high level metrics related to bp2build.
-			soongMetricsFile,         // high level metrics related to this build system.
-			config.BazelMetricsDir(), // directory that contains a set of bazel metrics.
-		}
-
-		if !config.SkipMetricsUpload() {
-			defer build.UploadMetrics(buildCtx, config, c.simpleOutput, buildStarted, files...)
-		}
-		defer met.Dump(soongMetricsFile)
-		defer build.CheckProdCreds(buildCtx, config)
-	}
+	defer build.CheckProdCreds(buildCtx, config)
 
 	// Read the time at the starting point.
 	if start, ok := os.LookupEnv("TRACE_BEGIN_SOONG"); ok {
@@ -253,7 +271,7 @@
 		}
 
 		if executable, err := os.Executable(); err == nil {
-			trace.ImportMicrofactoryLog(filepath.Join(filepath.Dir(executable), "."+filepath.Base(executable)+".trace"))
+			buildCtx.ContextImpl.Tracer.ImportMicrofactoryLog(filepath.Join(filepath.Dir(executable), "."+filepath.Base(executable)+".trace"))
 		}
 	}
 
@@ -266,8 +284,6 @@
 	f := build.NewSourceFinder(buildCtx, config)
 	defer f.Shutdown()
 	build.FindSources(buildCtx, config, f)
-
-	c.run(buildCtx, config, args, logsDir)
 }
 
 func fixBadDanglingLink(ctx build.Context, name string) {
@@ -284,7 +300,8 @@
 	}
 }
 
-func dumpVar(ctx build.Context, config build.Config, args []string, _ string) {
+func dumpVar(ctx build.Context, config build.Config, args []string) {
+	logAndSymlinkSetup(ctx, config)
 	flags := flag.NewFlagSet("dumpvar", flag.ExitOnError)
 	flags.SetOutput(ctx.Writer)
 
@@ -336,7 +353,9 @@
 	}
 }
 
-func dumpVars(ctx build.Context, config build.Config, args []string, _ string) {
+func dumpVars(ctx build.Context, config build.Config, args []string) {
+	logAndSymlinkSetup(ctx, config)
+
 	flags := flag.NewFlagSet("dumpvars", flag.ExitOnError)
 	flags.SetOutput(ctx.Writer)
 
@@ -421,6 +440,14 @@
 	return build.NewConfig(ctx)
 }
 
+// uploadOnlyConfig explicitly requires no arguments.
+func uploadOnlyConfig(ctx build.Context, args ...string) build.Config {
+	if len(args) > 0 {
+		fmt.Printf("--upload-only does not require arguments.")
+	}
+	return build.UploadOnlyConfig(ctx)
+}
+
 func buildActionConfig(ctx build.Context, args ...string) build.Config {
 	flags := flag.NewFlagSet("build-mode", flag.ContinueOnError)
 	flags.SetOutput(ctx.Writer)
@@ -514,7 +541,9 @@
 	return build.NewBuildActionConfig(buildAction, *dir, ctx, args...)
 }
 
-func runMake(ctx build.Context, config build.Config, _ []string, logsDir string) {
+func runMake(ctx build.Context, config build.Config, _ []string) {
+	logAndSymlinkSetup(ctx, config)
+	logsDir := config.LogsDir()
 	if config.IsVerbose() {
 		writer := ctx.Writer
 		fmt.Fprintln(writer, "! The argument `showcommands` is no longer supported.")
@@ -659,3 +688,19 @@
 		ctx.Println("Failed to increase file limit:", err)
 	}
 }
+
+func updateTotalRealTime(ctx build.Context, config build.Config, args []string) {
+	soongMetricsFile := filepath.Join(config.LogsDir(), "soong_metrics")
+
+	//read file into proto
+	data, err := os.ReadFile(soongMetricsFile)
+	if err != nil {
+		ctx.Fatal(err)
+	}
+	met := ctx.ContextImpl.Metrics
+
+	err = met.UpdateTotalRealTime(data)
+	if err != nil {
+		ctx.Fatal(err)
+	}
+}
diff --git a/java/java.go b/java/java.go
index 275abbe..6d53e1e 100644
--- a/java/java.go
+++ b/java/java.go
@@ -915,6 +915,10 @@
 
 	// a list of extra test configuration files that should be installed with the module.
 	Extra_test_configs []string `android:"path,arch_variant"`
+
+	// Extra <option> tags to add to the auto generated test xml file. The "key"
+	// is optional in each of these.
+	Tradefed_options []tradefed.Option
 }
 
 type testProperties struct {
@@ -1192,9 +1196,18 @@
 		defaultUnitTest := !inList("tradefed", j.properties.Libs) && !inList("cts", j.testProperties.Test_suites)
 		j.testProperties.Test_options.Unit_test = proptools.BoolPtr(defaultUnitTest)
 	}
-
-	j.testConfig = tradefed.AutoGenJavaTestConfig(ctx, j.testProperties.Test_config, j.testProperties.Test_config_template,
-		j.testProperties.Test_suites, configs, j.testProperties.Auto_gen_config, j.testProperties.Test_options.Unit_test)
+	j.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
+		TestConfigProp:          j.testProperties.Test_config,
+		TestConfigTemplateProp:  j.testProperties.Test_config_template,
+		TestSuites:              j.testProperties.Test_suites,
+		Config:                  configs,
+		OptionsForAutogenerated: j.testProperties.Test_options.Tradefed_options,
+		AutoGenConfig:           j.testProperties.Auto_gen_config,
+		UnitTest:                j.testProperties.Test_options.Unit_test,
+		DeviceTemplate:          "${JavaTestConfigTemplate}",
+		HostTemplate:            "${JavaHostTestConfigTemplate}",
+		HostUnitTestTemplate:    "${JavaHostUnitTestConfigTemplate}",
+	})
 
 	j.data = android.PathsForModuleSrc(ctx, j.testProperties.Data)
 
@@ -1239,8 +1252,13 @@
 }
 
 func (j *JavaTestImport) GenerateAndroidBuildActions(ctx android.ModuleContext) {
-	j.testConfig = tradefed.AutoGenJavaTestConfig(ctx, j.prebuiltTestProperties.Test_config, nil,
-		j.prebuiltTestProperties.Test_suites, nil, nil, nil)
+	j.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
+		TestConfigProp:       j.prebuiltTestProperties.Test_config,
+		TestSuites:           j.prebuiltTestProperties.Test_suites,
+		DeviceTemplate:       "${JavaTestConfigTemplate}",
+		HostTemplate:         "${JavaHostTestConfigTemplate}",
+		HostUnitTestTemplate: "${JavaHostUnitTestConfigTemplate}",
+	})
 
 	j.Import.GenerateAndroidBuildActions(ctx)
 }
@@ -2738,14 +2756,6 @@
 			Rule_class:        "java_library",
 			Bzl_load_location: "//build/bazel/rules/java:library.bzl",
 		}
-
-		ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: name}, attrs)
-		neverlinkProp := true
-		neverLinkAttrs := &javaLibraryAttributes{
-			Exports:   bazel.MakeSingleLabelListAttribute(bazel.Label{Label: ":" + name}),
-			Neverlink: bazel.BoolAttribute{Value: &neverlinkProp},
-		}
-		ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: name + "-neverlink"}, neverLinkAttrs)
 	} else {
 		attrs.Common_srcs = bazel.MakeLabelListAttribute(android.BazelLabelForModuleSrc(ctx, m.properties.Common_srcs))
 
@@ -2753,10 +2763,16 @@
 			Rule_class:        "kt_jvm_library",
 			Bzl_load_location: "@rules_kotlin//kotlin:jvm_library.bzl",
 		}
-		// TODO (b/244210934): create neverlink-duplicate target once kt_jvm_library supports neverlink attribute
-		ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: name}, attrs)
 	}
 
+	ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: name}, attrs)
+	neverlinkProp := true
+	neverLinkAttrs := &javaLibraryAttributes{
+		Exports:   bazel.MakeSingleLabelListAttribute(bazel.Label{Label: ":" + name}),
+		Neverlink: bazel.BoolAttribute{Value: &neverlinkProp},
+	}
+	ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: name + "-neverlink"}, neverLinkAttrs)
+
 }
 
 type javaBinaryHostAttributes struct {
diff --git a/java/java_test.go b/java/java_test.go
index dff1fd0..085f627 100644
--- a/java/java_test.go
+++ b/java/java_test.go
@@ -1945,3 +1945,27 @@
 		}
 	}
 }
+
+func TestTradefedOptions(t *testing.T) {
+	result := PrepareForTestWithJavaBuildComponents.RunTestWithBp(t, `
+java_test_host {
+	name: "foo",
+	test_options: {
+		tradefed_options: [
+			{
+				name: "exclude-path",
+				value: "org/apache"
+			}
+		]
+	}
+}
+`)
+
+	buildOS := result.Config.BuildOS.String()
+	args := result.ModuleForTests("foo", buildOS+"_common").
+		Output("out/soong/.intermediates/foo/" + buildOS + "_common/foo.config").Args
+	expected := proptools.NinjaAndShellEscape("<option name=\"exclude-path\" value=\"org/apache\" />")
+	if args["extraConfigs"] != expected {
+		t.Errorf("Expected args[\"extraConfigs\"] to equal %q, was %q", expected, args["extraConfigs"])
+	}
+}
diff --git a/java/robolectric.go b/java/robolectric.go
index 6e8d591..68f27b8 100644
--- a/java/robolectric.go
+++ b/java/robolectric.go
@@ -131,9 +131,14 @@
 	r.forceOSType = ctx.Config().BuildOS
 	r.forceArchType = ctx.Config().BuildArch
 
-	r.testConfig = tradefed.AutoGenRobolectricTestConfig(ctx, r.testProperties.Test_config,
-		r.testProperties.Test_config_template, r.testProperties.Test_suites,
-		r.testProperties.Auto_gen_config)
+	r.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
+		TestConfigProp:         r.testProperties.Test_config,
+		TestConfigTemplateProp: r.testProperties.Test_config_template,
+		TestSuites:             r.testProperties.Test_suites,
+		AutoGenConfig:          r.testProperties.Auto_gen_config,
+		DeviceTemplate:         "${RobolectricTestConfigTemplate}",
+		HostTemplate:           "${RobolectricTestConfigTemplate}",
+	})
 	r.data = android.PathsForModuleSrc(ctx, r.testProperties.Data)
 
 	roboTestConfig := android.PathForModuleGen(ctx, "robolectric").
diff --git a/python/test.go b/python/test.go
index b9b3465..fc5c211 100644
--- a/python/test.go
+++ b/python/test.go
@@ -67,9 +67,14 @@
 }
 
 func (test *testDecorator) install(ctx android.ModuleContext, file android.Path) {
-	test.testConfig = tradefed.AutoGenPythonBinaryHostTestConfig(ctx, test.testProperties.Test_config,
-		test.testProperties.Test_config_template, test.binaryDecorator.binaryProperties.Test_suites,
-		test.binaryDecorator.binaryProperties.Auto_gen_config)
+	test.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
+		TestConfigProp:         test.testProperties.Test_config,
+		TestConfigTemplateProp: test.testProperties.Test_config_template,
+		TestSuites:             test.binaryDecorator.binaryProperties.Test_suites,
+		AutoGenConfig:          test.binaryDecorator.binaryProperties.Auto_gen_config,
+		DeviceTemplate:         "${PythonBinaryHostTestConfigTemplate}",
+		HostTemplate:           "${PythonBinaryHostTestConfigTemplate}",
+	})
 
 	test.binaryDecorator.pythonInstaller.dir = "nativetest"
 	test.binaryDecorator.pythonInstaller.dir64 = "nativetest64"
diff --git a/rust/benchmark.go b/rust/benchmark.go
index 0e84243..c0f1e24 100644
--- a/rust/benchmark.go
+++ b/rust/benchmark.go
@@ -112,12 +112,14 @@
 }
 
 func (benchmark *benchmarkDecorator) install(ctx ModuleContext) {
-	benchmark.testConfig = tradefed.AutoGenRustBenchmarkConfig(ctx,
-		benchmark.Properties.Test_config,
-		benchmark.Properties.Test_config_template,
-		benchmark.Properties.Test_suites,
-		nil,
-		benchmark.Properties.Auto_gen_config)
+	benchmark.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
+		TestConfigProp:         benchmark.Properties.Test_config,
+		TestConfigTemplateProp: benchmark.Properties.Test_config_template,
+		TestSuites:             benchmark.Properties.Test_suites,
+		AutoGenConfig:          benchmark.Properties.Auto_gen_config,
+		DeviceTemplate:         "${RustDeviceBenchmarkConfigTemplate}",
+		HostTemplate:           "${RustHostBenchmarkConfigTemplate}",
+	})
 
 	// default relative install path is module name
 	if !Bool(benchmark.Properties.No_named_install_directory) {
diff --git a/rust/test.go b/rust/test.go
index 0cc3bca..4f922b4 100644
--- a/rust/test.go
+++ b/rust/test.go
@@ -130,13 +130,16 @@
 		configs = append(configs, tradefed.Object{"target_preparer", "com.android.tradefed.targetprep.RootTargetPreparer", options})
 	}
 
-	test.testConfig = tradefed.AutoGenRustTestConfig(ctx,
-		test.Properties.Test_config,
-		test.Properties.Test_config_template,
-		test.Properties.Test_suites,
-		configs,
-		test.Properties.Auto_gen_config,
-		testInstallBase)
+	test.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
+		TestConfigProp:         test.Properties.Test_config,
+		TestConfigTemplateProp: test.Properties.Test_config_template,
+		TestSuites:             test.Properties.Test_suites,
+		Config:                 configs,
+		AutoGenConfig:          test.Properties.Auto_gen_config,
+		TestInstallBase:        testInstallBase,
+		DeviceTemplate:         "${RustDeviceTestConfigTemplate}",
+		HostTemplate:           "${RustHostTestConfigTemplate}",
+	})
 
 	dataSrcPaths := android.PathsForModuleSrc(ctx, test.Properties.Data)
 
diff --git a/scripts/run-ckati.sh b/scripts/run-ckati.sh
new file mode 100755
index 0000000..b670c8a
--- /dev/null
+++ b/scripts/run-ckati.sh
@@ -0,0 +1,91 @@
+#! /bin/bash -eu
+
+# Run CKati step separately, tracing given Makefile variables.
+# It is expected that the regular Android null build (`m nothing`)
+# has been run so that $OUT_DIR/soong/Android-${TARGET_PRODUCT}.mk,
+# $OUT_DIR/soong/make_vars-${TARGET_PRODUCT}.mk, etc. files exist.
+#
+# The output file is in JSON format and can be processed with, say,
+# `jq`. For instance, the following invocation outputs all assignment
+# traces concisely:
+#  jq -c  '.assignments[] | (select (.operation == "assign")) | {("n"): .name, ("l"): .value_stack[0]?, ("v"): .value }' out/ckati.trace
+# generates
+#  {"n":"<var1>","l":"<file>:<line>","v":"<value1>"}
+#  ...
+
+function die() { format=$1; shift; printf "$format\n" $@; exit 1; }
+function usage() { die "Usage: %s [-o FILE] VAR ...\n(without -o the output goes to ${outfile})"  ${0##*/}; }
+
+[[ -d build/soong ]] || die "run this script from the top of the Android source tree"
+declare -r out=${OUT_DIR:-out}
+[[ -x ${out}/soong_ui ]] || die "run Android build first"
+: ${TARGET_PRODUCT:?not set, run lunch?}
+: ${TARGET_BUILD_VARIANT:?not set, run lunch?}
+declare -r androidmk=${out}/soong/Android-${TARGET_PRODUCT}.mk
+declare -r makevarsmk=${out}/soong/make_vars-${TARGET_PRODUCT}.mk
+declare -r target_device_dir=$(${out}/soong_ui --dumpvar-mode TARGET_DEVICE_DIR)
+: ${target_device_dir:?cannot find device directory for ${TARGET_PRODUCT}}
+declare -r target_device=$(${out}/soong_ui --dumpvar-mode TARGET_DEVICE)
+: ${target_device:?cannot find target device for ${TARGET_PRODUCT}}
+declare -r timestamp_file=${out}/build_date.txt
+# Files should exist, so ls should succeed:
+ls -1d "$androidmk" "$makevarsmk" "$target_device_dir" "$timestamp_file" >/dev/null
+
+outfile=${out}/ckati.trace
+while getopts "ho:" opt; do
+  case $opt in
+    h) usage ;;
+    o) outfile=$OPTARG ;;
+    ?) usage ;;
+  esac
+done
+
+if (($#>0)); then
+  declare -a tracing=(--variable_assignment_trace_filter="$*" --dump_variable_assignment_trace "$outfile")
+else
+  printf "running ckati without tracing variables\n"
+fi
+
+# Touch one input for ckati, otherwise it will just print
+# 'No need to regenerate ninja file' and exit.
+touch "$androidmk"
+prebuilts/build-tools/linux-x86/bin/ckati \
+  --gen_all_targets \
+  -i \
+  --ignore_optional_include=out/%.P \
+  --ninja \
+  --ninja_dir=out \
+  --ninja_suffix=-${TARGET_PRODUCT} \
+  --no_builtin_rules \
+  --no_ninja_prelude \
+  --regen \
+  --top_level_phony \
+  --use_find_emulator \
+  --use_ninja_phony_output \
+  --use_ninja_symlink_outputs \
+  --werror_find_emulator \
+  --werror_implicit_rules \
+  --werror_overriding_commands \
+  --werror_phony_looks_real \
+  --werror_real_to_phony \
+  --werror_suffix_rules \
+  --werror_writable \
+  --writable out/ \
+  -f build/make/core/main.mk \
+  "${tracing[@]}" \
+  ANDROID_JAVA_HOME=prebuilts/jdk/jdk17/linux-x86 \
+  ASAN_SYMBOLIZER_PATH=$PWD/prebuilts/clang/host/linux-x86/llvm-binutils-stable/llvm-symbolizer \
+  BUILD_DATETIME_FILE="$timestamp_file" \
+  BUILD_HOSTNAME=$(hostname) \
+  BUILD_USERNAME="$USER" \
+  JAVA_HOME=$PWD/prebuilts/jdk/jdk17/linux-x86 \
+  KATI_PACKAGE_MK_DIR="{$out}/target/product/${target_device}/CONFIG/kati_packaging" \
+  OUT_DIR="$out" \
+  PATH="$PWD/prebuilts/build-tools/path/linux-x86:$PWD/${out}/.path" \
+  PYTHONDONTWRITEBYTECODE=1 \
+  SOONG_ANDROID_MK="$androidmk" \
+  SOONG_MAKEVARS_MK="$makevarsmk" \
+  TARGET_BUILD_VARIANT="$TARGET_BUILD_VARIANT" \
+  TARGET_DEVICE_DIR="$target_device_dir" \
+  TARGET_PRODUCT=${TARGET_PRODUCT} \
+  TMPDIR="$PWD/$out/soong/.temp"
diff --git a/sh/sh_binary.go b/sh/sh_binary.go
index 9627329..c921ca6 100644
--- a/sh/sh_binary.go
+++ b/sh/sh_binary.go
@@ -379,8 +379,16 @@
 		}
 		configs = append(configs, tradefed.Object{"target_preparer", "com.android.tradefed.targetprep.PushFilePreparer", options})
 	}
-	s.testConfig = tradefed.AutoGenShellTestConfig(ctx, s.testProperties.Test_config,
-		s.testProperties.Test_config_template, s.testProperties.Test_suites, configs, s.testProperties.Auto_gen_config, s.outputFilePath.Base())
+	s.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
+		TestConfigProp:         s.testProperties.Test_config,
+		TestConfigTemplateProp: s.testProperties.Test_config_template,
+		TestSuites:             s.testProperties.Test_suites,
+		Config:                 configs,
+		AutoGenConfig:          s.testProperties.Auto_gen_config,
+		OutputFileName:         s.outputFilePath.Base(),
+		DeviceTemplate:         "${ShellTestConfigTemplate}",
+		HostTemplate:           "${ShellTestConfigTemplate}",
+	})
 
 	s.dataModules = make(map[string]android.Path)
 	ctx.VisitDirectDeps(func(dep android.Module) {
diff --git a/tradefed/autogen.go b/tradefed/autogen.go
index c2429ab..49b09af 100644
--- a/tradefed/autogen.go
+++ b/tradefed/autogen.go
@@ -107,15 +107,10 @@
 
 }
 
-func autogenTemplate(ctx android.ModuleContext, output android.WritablePath, template string, configs []Config, testInstallBase string) {
-	autogenTemplateWithNameAndOutputFile(ctx, ctx.ModuleName(), output, template, configs, "", testInstallBase)
-}
-
-func autogenTemplateWithName(ctx android.ModuleContext, name string, output android.WritablePath, template string, configs []Config, testInstallBase string) {
-	autogenTemplateWithNameAndOutputFile(ctx, name, output, template, configs, "", testInstallBase)
-}
-
-func autogenTemplateWithNameAndOutputFile(ctx android.ModuleContext, name string, output android.WritablePath, template string, configs []Config, outputFileName string, testInstallBase string) {
+func autogenTemplate(ctx android.ModuleContext, name string, output android.WritablePath, template string, configs []Config, outputFileName string, testInstallBase string) {
+	if template == "" {
+		ctx.ModuleErrorf("Empty template")
+	}
 	var configStrings []string
 	for _, config := range configs {
 		configStrings = append(configStrings, config.Config())
@@ -137,144 +132,53 @@
 	})
 }
 
-func AutoGenNativeTestConfig(ctx android.ModuleContext, testConfigProp *string,
-	testConfigTemplateProp *string, testSuites []string, config []Config, autoGenConfig *bool, testInstallBase string) android.Path {
+// AutoGenTestConfigOptions is used so that we can supply many optional
+// arguments to the AutoGenTestConfig function.
+type AutoGenTestConfigOptions struct {
+	Name                    string
+	OutputFileName          string
+	TestConfigProp          *string
+	TestConfigTemplateProp  *string
+	TestSuites              []string
+	Config                  []Config
+	OptionsForAutogenerated []Option
+	AutoGenConfig           *bool
+	UnitTest                *bool
+	TestInstallBase         string
+	DeviceTemplate          string
+	HostTemplate            string
+	HostUnitTestTemplate    string
+}
 
-	path, autogenPath := testConfigPath(ctx, testConfigProp, testSuites, autoGenConfig, testConfigTemplateProp)
+func AutoGenTestConfig(ctx android.ModuleContext, options AutoGenTestConfigOptions) android.Path {
+	configs := append([]Config{}, options.Config...)
+	for _, c := range options.OptionsForAutogenerated {
+		configs = append(configs, c)
+	}
+	name := options.Name
+	if name == "" {
+		name = ctx.ModuleName()
+	}
+	path, autogenPath := testConfigPath(ctx, options.TestConfigProp, options.TestSuites, options.AutoGenConfig, options.TestConfigTemplateProp)
 	if autogenPath != nil {
-		templatePath := getTestConfigTemplate(ctx, testConfigTemplateProp)
+		templatePath := getTestConfigTemplate(ctx, options.TestConfigTemplateProp)
 		if templatePath.Valid() {
-			autogenTemplate(ctx, autogenPath, templatePath.String(), config, testInstallBase)
+			autogenTemplate(ctx, name, autogenPath, templatePath.String(), configs, options.OutputFileName, options.TestInstallBase)
 		} else {
 			if ctx.Device() {
-				autogenTemplate(ctx, autogenPath, "${NativeTestConfigTemplate}", config, testInstallBase)
+				autogenTemplate(ctx, name, autogenPath, options.DeviceTemplate, configs, options.OutputFileName, options.TestInstallBase)
 			} else {
-				autogenTemplate(ctx, autogenPath, "${NativeHostTestConfigTemplate}", config, testInstallBase)
-			}
-		}
-		return autogenPath
-	}
-	return path
-}
-
-func AutoGenShellTestConfig(ctx android.ModuleContext, testConfigProp *string,
-	testConfigTemplateProp *string, testSuites []string, config []Config, autoGenConfig *bool, outputFileName string) android.Path {
-	path, autogenPath := testConfigPath(ctx, testConfigProp, testSuites, autoGenConfig, testConfigTemplateProp)
-	if autogenPath != nil {
-		templatePath := getTestConfigTemplate(ctx, testConfigTemplateProp)
-		if templatePath.Valid() {
-			autogenTemplateWithNameAndOutputFile(ctx, ctx.ModuleName(), autogenPath, templatePath.String(), config, outputFileName, "")
-		} else {
-			autogenTemplateWithNameAndOutputFile(ctx, ctx.ModuleName(), autogenPath, "${ShellTestConfigTemplate}", config, outputFileName, "")
-		}
-		return autogenPath
-	}
-	return path
-}
-
-func AutoGenNativeBenchmarkTestConfig(ctx android.ModuleContext, testConfigProp *string,
-	testConfigTemplateProp *string, testSuites []string, configs []Config, autoGenConfig *bool) android.Path {
-	path, autogenPath := testConfigPath(ctx, testConfigProp, testSuites, autoGenConfig, testConfigTemplateProp)
-	if autogenPath != nil {
-		templatePath := getTestConfigTemplate(ctx, testConfigTemplateProp)
-		if templatePath.Valid() {
-			autogenTemplate(ctx, autogenPath, templatePath.String(), configs, "")
-		} else {
-			autogenTemplate(ctx, autogenPath, "${NativeBenchmarkTestConfigTemplate}", configs, "")
-		}
-		return autogenPath
-	}
-	return path
-}
-
-func AutoGenJavaTestConfig(ctx android.ModuleContext, testConfigProp *string, testConfigTemplateProp *string,
-	testSuites []string, config []Config, autoGenConfig *bool, unitTest *bool) android.Path {
-	path, autogenPath := testConfigPath(ctx, testConfigProp, testSuites, autoGenConfig, testConfigTemplateProp)
-	if autogenPath != nil {
-		templatePath := getTestConfigTemplate(ctx, testConfigTemplateProp)
-		if templatePath.Valid() {
-			autogenTemplate(ctx, autogenPath, templatePath.String(), config, "")
-		} else {
-			if ctx.Device() {
-				autogenTemplate(ctx, autogenPath, "${JavaTestConfigTemplate}", config, "")
-			} else {
-				if Bool(unitTest) {
-					autogenTemplate(ctx, autogenPath, "${JavaHostUnitTestConfigTemplate}", config, "")
+				if Bool(options.UnitTest) {
+					autogenTemplate(ctx, name, autogenPath, options.HostUnitTestTemplate, configs, options.OutputFileName, options.TestInstallBase)
 				} else {
-					autogenTemplate(ctx, autogenPath, "${JavaHostTestConfigTemplate}", config, "")
+					autogenTemplate(ctx, name, autogenPath, options.HostTemplate, configs, options.OutputFileName, options.TestInstallBase)
 				}
 			}
 		}
 		return autogenPath
 	}
-	return path
-}
-
-func AutoGenPythonBinaryHostTestConfig(ctx android.ModuleContext, testConfigProp *string,
-	testConfigTemplateProp *string, testSuites []string, autoGenConfig *bool) android.Path {
-
-	path, autogenPath := testConfigPath(ctx, testConfigProp, testSuites, autoGenConfig, testConfigTemplateProp)
-	if autogenPath != nil {
-		templatePath := getTestConfigTemplate(ctx, testConfigTemplateProp)
-		if templatePath.Valid() {
-			autogenTemplate(ctx, autogenPath, templatePath.String(), nil, "")
-		} else {
-			autogenTemplate(ctx, autogenPath, "${PythonBinaryHostTestConfigTemplate}", nil, "")
-		}
-		return autogenPath
-	}
-	return path
-}
-
-func AutoGenRustTestConfig(ctx android.ModuleContext, testConfigProp *string,
-	testConfigTemplateProp *string, testSuites []string, config []Config, autoGenConfig *bool, testInstallBase string) android.Path {
-	path, autogenPath := testConfigPath(ctx, testConfigProp, testSuites, autoGenConfig, testConfigTemplateProp)
-	if autogenPath != nil {
-		templatePath := getTestConfigTemplate(ctx, testConfigTemplateProp)
-		if templatePath.Valid() {
-			autogenTemplate(ctx, autogenPath, templatePath.String(), config, testInstallBase)
-		} else {
-			if ctx.Device() {
-				autogenTemplate(ctx, autogenPath, "${RustDeviceTestConfigTemplate}", config, testInstallBase)
-			} else {
-				autogenTemplate(ctx, autogenPath, "${RustHostTestConfigTemplate}", config, testInstallBase)
-			}
-		}
-		return autogenPath
-	}
-	return path
-}
-
-func AutoGenRustBenchmarkConfig(ctx android.ModuleContext, testConfigProp *string,
-	testConfigTemplateProp *string, testSuites []string, config []Config, autoGenConfig *bool) android.Path {
-	path, autogenPath := testConfigPath(ctx, testConfigProp, testSuites, autoGenConfig, testConfigTemplateProp)
-	if autogenPath != nil {
-		templatePath := getTestConfigTemplate(ctx, testConfigTemplateProp)
-		if templatePath.Valid() {
-			autogenTemplate(ctx, autogenPath, templatePath.String(), config, "")
-		} else {
-			if ctx.Device() {
-				autogenTemplate(ctx, autogenPath, "${RustDeviceBenchmarkConfigTemplate}", config, "")
-			} else {
-				autogenTemplate(ctx, autogenPath, "${RustHostBenchmarkConfigTemplate}", config, "")
-			}
-		}
-		return autogenPath
-	}
-	return path
-}
-
-func AutoGenRobolectricTestConfig(ctx android.ModuleContext, testConfigProp *string, testConfigTemplateProp *string,
-	testSuites []string, autoGenConfig *bool) android.Path {
-	path, autogenPath := testConfigPath(ctx, testConfigProp, testSuites, autoGenConfig, testConfigTemplateProp)
-	if autogenPath != nil {
-		templatePath := getTestConfigTemplate(ctx, testConfigTemplateProp)
-		if templatePath.Valid() {
-			autogenTemplate(ctx, autogenPath, templatePath.String(), nil, "")
-		} else {
-			autogenTemplate(ctx, autogenPath, "${RobolectricTestConfigTemplate}", nil, "")
-		}
-		return autogenPath
+	if len(options.OptionsForAutogenerated) > 0 {
+		ctx.ModuleErrorf("Extra tradefed configurations were provided for an autogenerated xml file, but the autogenerated xml file was not used.")
 	}
 	return path
 }
diff --git a/ui/build/config.go b/ui/build/config.go
index a6bba15..61f6b1c 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -63,6 +63,7 @@
 	environ       *Environment
 	distDir       string
 	buildDateTime string
+	logsPrefix    string
 
 	// From the arguments
 	parallel          int
@@ -84,6 +85,7 @@
 	skipSoongTests    bool
 	searchApiDir      bool // Scan the Android.bp files generated in out/api_surfaces
 	skipMetricsUpload bool
+	buildStartedTime  int64 // For metrics-upload-only - manually specify a build-started time
 
 	// From the product config
 	katiArgs        []string
@@ -255,6 +257,14 @@
 	return true
 }
 
+func UploadOnlyConfig(ctx Context, _ ...string) Config {
+	ret := &configImpl{
+		environ:       OsEnvironment(),
+		sandboxConfig: &SandboxConfig{},
+	}
+	return Config{ret}
+}
+
 func NewConfig(ctx Context, args ...string) Config {
 	ret := &configImpl{
 		environ:       OsEnvironment(),
@@ -266,9 +276,7 @@
 	ret.keepGoing = 1
 
 	ret.totalRAM = detectTotalRAM(ctx)
-
 	ret.parseArgs(ctx, args)
-
 	// Make sure OUT_DIR is set appropriately
 	if outDir, ok := ret.environ.Get("OUT_DIR"); ok {
 		ret.environ.Set("OUT_DIR", filepath.Clean(outDir))
@@ -756,6 +764,14 @@
 			ctx.Metrics.SetBuildCommand([]string{buildCmd})
 		} else if strings.HasPrefix(arg, "--bazel-force-enabled-modules=") {
 			c.bazelForceEnabledModules = strings.TrimPrefix(arg, "--bazel-force-enabled-modules=")
+		} else if strings.HasPrefix(arg, "--build-started-time-unix-millis=") {
+			buildTimeStr := strings.TrimPrefix(arg, "--build-started-time-unix-millis=")
+			val, err := strconv.ParseInt(buildTimeStr, 10, 64)
+			if err == nil {
+				c.buildStartedTime = val
+			} else {
+				ctx.Fatalf("Error parsing build-time-started-unix-millis", err)
+			}
 		} else if len(arg) > 0 && arg[0] == '-' {
 			parseArgNum := func(def int) int {
 				if len(arg) > 2 {
@@ -1092,6 +1108,14 @@
 	c.includeTags = i
 }
 
+func (c *configImpl) GetLogsPrefix() string {
+	return c.logsPrefix
+}
+
+func (c *configImpl) SetLogsPrefix(prefix string) {
+	c.logsPrefix = prefix
+}
+
 func (c *configImpl) HighmemParallel() int {
 	if i, ok := c.environ.GetInt("NINJA_HIGHMEM_NUM_JOBS"); ok {
 		return i
@@ -1519,6 +1543,15 @@
 	return c.skipMetricsUpload
 }
 
+// Returns a Time object if one was passed via a command-line flag.
+// Otherwise returns the passed default.
+func (c *configImpl) BuildStartedTimeOrDefault(defaultTime time.Time) time.Time {
+	if c.buildStartedTime == 0 {
+		return defaultTime
+	}
+	return time.UnixMilli(c.buildStartedTime)
+}
+
 func GetMetricsUploader(topDir string, env *Environment) string {
 	if p, ok := env.Get("METRICS_UPLOADER"); ok {
 		metricsUploader := filepath.Join(topDir, p)
diff --git a/ui/metrics/bazel_metrics_proto/bazel_metrics.pb.go b/ui/metrics/bazel_metrics_proto/bazel_metrics.pb.go
index 760b592..f8b8fd6 100644
--- a/ui/metrics/bazel_metrics_proto/bazel_metrics.pb.go
+++ b/ui/metrics/bazel_metrics_proto/bazel_metrics.pb.go
@@ -97,8 +97,9 @@
 	// E.g. "execution", "analysis", "launch"
 	PhaseName     *string `protobuf:"bytes,1,opt,name=phase_name,json=phaseName,proto3,oneof" json:"phase_name,omitempty"`
 	DurationNanos *int64  `protobuf:"varint,2,opt,name=duration_nanos,json=durationNanos,proto3,oneof" json:"duration_nanos,omitempty"`
-	// What percentage of the build time this phase took
-	PercentageMillis *int32 `protobuf:"varint,3,opt,name=percentage_millis,json=percentageMillis,proto3,oneof" json:"percentage_millis,omitempty"`
+	// What portion of the build time this phase took, with ten-thousandths precision.
+	// E.g., 1111 = 11.11%, 111 = 1.11%
+	PortionOfBuildTime *int32 `protobuf:"varint,3,opt,name=portion_of_build_time,json=portionOfBuildTime,proto3,oneof" json:"portion_of_build_time,omitempty"`
 }
 
 func (x *PhaseTiming) Reset() {
@@ -147,9 +148,9 @@
 	return 0
 }
 
-func (x *PhaseTiming) GetPercentageMillis() int32 {
-	if x != nil && x.PercentageMillis != nil {
-		return *x.PercentageMillis
+func (x *PhaseTiming) GetPortionOfBuildTime() int32 {
+	if x != nil && x.PortionOfBuildTime != nil {
+		return *x.PortionOfBuildTime
 	}
 	return 0
 }
@@ -168,23 +169,23 @@
 	0x52, 0x0c, 0x70, 0x68, 0x61, 0x73, 0x65, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x19,
 	0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52,
 	0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x74, 0x6f,
-	0x74, 0x61, 0x6c, 0x22, 0xc7, 0x01, 0x0a, 0x0b, 0x50, 0x68, 0x61, 0x73, 0x65, 0x54, 0x69, 0x6d,
+	0x74, 0x61, 0x6c, 0x22, 0xd1, 0x01, 0x0a, 0x0b, 0x50, 0x68, 0x61, 0x73, 0x65, 0x54, 0x69, 0x6d,
 	0x69, 0x6e, 0x67, 0x12, 0x22, 0x0a, 0x0a, 0x70, 0x68, 0x61, 0x73, 0x65, 0x5f, 0x6e, 0x61, 0x6d,
 	0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x70, 0x68, 0x61, 0x73, 0x65,
 	0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x2a, 0x0a, 0x0e, 0x64, 0x75, 0x72, 0x61, 0x74,
 	0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x48,
 	0x01, 0x52, 0x0d, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6e, 0x6f, 0x73,
-	0x88, 0x01, 0x01, 0x12, 0x30, 0x0a, 0x11, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67,
-	0x65, 0x5f, 0x6d, 0x69, 0x6c, 0x6c, 0x69, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x02,
-	0x52, 0x10, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x4d, 0x69, 0x6c, 0x6c,
-	0x69, 0x73, 0x88, 0x01, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x70, 0x68, 0x61, 0x73, 0x65, 0x5f,
-	0x6e, 0x61, 0x6d, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f,
-	0x6e, 0x5f, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x70, 0x65, 0x72, 0x63,
-	0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x5f, 0x6d, 0x69, 0x6c, 0x6c, 0x69, 0x73, 0x42, 0x2e, 0x5a,
-	0x2c, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f, 0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x2f, 0x75,
-	0x69, 0x2f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x2f, 0x62, 0x61, 0x7a, 0x65, 0x6c, 0x5f,
-	0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x88, 0x01, 0x01, 0x12, 0x36, 0x0a, 0x15, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6f,
+	0x66, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x05, 0x48, 0x02, 0x52, 0x12, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x66, 0x42,
+	0x75, 0x69, 0x6c, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f,
+	0x70, 0x68, 0x61, 0x73, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x64,
+	0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x42, 0x18, 0x0a,
+	0x16, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x66, 0x5f, 0x62, 0x75, 0x69,
+	0x6c, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x42, 0x2e, 0x5a, 0x2c, 0x61, 0x6e, 0x64, 0x72, 0x6f,
+	0x69, 0x64, 0x2f, 0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x2f, 0x75, 0x69, 0x2f, 0x6d, 0x65, 0x74, 0x72,
+	0x69, 0x63, 0x73, 0x2f, 0x62, 0x61, 0x7a, 0x65, 0x6c, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63,
+	0x73, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/ui/metrics/bazel_metrics_proto/bazel_metrics.proto b/ui/metrics/bazel_metrics_proto/bazel_metrics.proto
index e78a6b9..57eed4c 100644
--- a/ui/metrics/bazel_metrics_proto/bazel_metrics.proto
+++ b/ui/metrics/bazel_metrics_proto/bazel_metrics.proto
@@ -26,6 +26,7 @@
   // E.g. "execution", "analysis", "launch"
   optional string phase_name = 1;
   optional int64 duration_nanos = 2;
-  // What percentage of the build time this phase took
-  optional int32 percentage_millis = 3;
+  // What portion of the build time this phase took, with ten-thousandths precision.
+  // E.g., 1111 = 11.11%, 111 = 1.11%
+  optional int32 portion_of_build_time = 3;
 }
diff --git a/ui/metrics/metrics.go b/ui/metrics/metrics.go
index ce2d946..717530c 100644
--- a/ui/metrics/metrics.go
+++ b/ui/metrics/metrics.go
@@ -32,13 +32,13 @@
 // of what an event is and how the metrics system is a stack based system.
 
 import (
+	"fmt"
 	"os"
 	"runtime"
 	"strings"
 	"time"
 
 	"android/soong/shared"
-
 	"google.golang.org/protobuf/proto"
 
 	soong_metrics_proto "android/soong/ui/metrics/metrics_proto"
@@ -223,6 +223,17 @@
 	m.metrics.BuildDateTimestamp = proto.Int64(buildTimestamp.UnixNano() / int64(time.Second))
 }
 
+func (m *Metrics) UpdateTotalRealTime(data []byte) error {
+	if err := proto.Unmarshal(data, &m.metrics); err != nil {
+		return fmt.Errorf("Failed to unmarshal proto", err)
+	}
+	startTime := *m.metrics.Total.StartTime
+	endTime := uint64(time.Now().UnixNano())
+
+	*m.metrics.Total.RealTime = *proto.Uint64(endTime - startTime)
+	return nil
+}
+
 // SetBuildCommand adds the build command specified by the user to the
 // list of collected metrics.
 func (m *Metrics) SetBuildCommand(cmd []string) {