Move auto installclean to soong_ui

This way kati won't need to be run as often (either initially, or when
switching products with the same device).

Bug: 35970961
Test: m clean; m -j blueprint_tools; m -j blueprint_tools; m -j blueprint_tools
Test: lunch aosp_arm-eng; m -j blueprint_tools; lunch full-eng; m -j blueprint_tools; <repeat>
Change-Id: Ie9fca3c8f1dd412459ea47c7090c7c5fdb0bcf6e
diff --git a/ui/build/build.go b/ui/build/build.go
index b84dd7d..598e342 100644
--- a/ui/build/build.go
+++ b/ui/build/build.go
@@ -18,6 +18,7 @@
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strings"
 	"text/template"
 )
 
@@ -91,6 +92,62 @@
 	}
 }
 
+// Since products and build variants (unfortunately) shared the same
+// PRODUCT_OUT staging directory, things can get out of sync if different
+// build configurations are built in the same tree. This function will
+// notice when the configuration has changed and call installclean to
+// remove the files necessary to keep things consistent.
+func installcleanIfNecessary(ctx Context, config Config) {
+	if inList("installclean", config.Arguments()) {
+		return
+	}
+
+	configFile := config.DevicePreviousProductConfig()
+	prefix := "PREVIOUS_BUILD_CONFIG := "
+	suffix := "\n"
+	currentProduct := prefix + config.TargetProduct() + "-" + config.TargetBuildVariant() + suffix
+
+	writeConfig := func() {
+		err := ioutil.WriteFile(configFile, []byte(currentProduct), 0777)
+		if err != nil {
+			ctx.Fatalln("Failed to write product config:", err)
+		}
+	}
+
+	prev, err := ioutil.ReadFile(configFile)
+	if err != nil {
+		if os.IsNotExist(err) {
+			writeConfig()
+			return
+		} else {
+			ctx.Fatalln("Failed to read previous product config:", err)
+		}
+	} else if string(prev) == currentProduct {
+		return
+	}
+
+	if disable, _ := config.Environment().Get("DISABLE_AUTO_INSTALLCLEAN"); disable == "true" {
+		ctx.Println("DISABLE_AUTO_INSTALLCLEAN is set; skipping auto-clean. Your tree may be in an inconsistent state.")
+		return
+	}
+
+	ctx.BeginTrace("installclean")
+	defer ctx.EndTrace()
+
+	prevConfig := strings.TrimPrefix(strings.TrimSuffix(string(prev), suffix), prefix)
+	currentConfig := strings.TrimPrefix(strings.TrimSuffix(currentProduct, suffix), prefix)
+
+	ctx.Printf("Build configuration changed: %q -> %q, forcing installclean\n", prevConfig, currentConfig)
+
+	cleanConfig := CopyConfig(ctx, config, "installclean")
+	cleanConfig.SetKatiArgs([]string{"installclean"})
+	cleanConfig.SetNinjaArgs([]string{"installclean"})
+
+	Build(ctx, cleanConfig, BuildKati|BuildNinja)
+
+	writeConfig()
+}
+
 // Build the tree. The 'what' argument can be used to chose which components of
 // the build to run.
 func Build(ctx Context, config Config, what int) {
@@ -145,6 +202,8 @@
 	}
 
 	if what&BuildNinja != 0 {
+		installcleanIfNecessary(ctx, config)
+
 		// Write combined ninja file
 		createCombinedBuildNinjaFile(ctx, config)
 
diff --git a/ui/build/config.go b/ui/build/config.go
index 0d29924..51cff50 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -38,9 +38,10 @@
 	dist      bool
 
 	// From the product config
-	katiArgs   []string
-	ninjaArgs  []string
-	katiSuffix string
+	katiArgs     []string
+	ninjaArgs    []string
+	katiSuffix   string
+	targetDevice string
 }
 
 const srcDirFileCheck = "build/soong/root.bp"
@@ -166,6 +167,22 @@
 	return Config{ret}
 }
 
+// CopyConfig copies the configuration from an existing configuration, but replaces
+// the Arguments() list with a new set. Useful if you need to run a different build
+// with the same state as an existing build config.
+func CopyConfig(ctx Context, config Config, args ...string) Config {
+	return Config{&configImpl{
+		arguments: args,
+		goma:      config.goma,
+		environ:   config.environ.Copy(),
+
+		parallel:  config.parallel,
+		keepGoing: config.keepGoing,
+		verbose:   config.verbose,
+		dist:      config.dist,
+	}}
+}
+
 // Lunch configures the environment for a specific product similarly to the
 // `lunch` bash function.
 func (c *configImpl) Lunch(ctx Context, product, variant string) {
@@ -271,6 +288,21 @@
 	panic("TARGET_PRODUCT is not defined")
 }
 
+func (c *configImpl) TargetDevice() string {
+	return c.targetDevice
+}
+
+func (c *configImpl) SetTargetDevice(device string) {
+	c.targetDevice = device
+}
+
+func (c *configImpl) TargetBuildVariant() string {
+	if v, ok := c.environ.Get("TARGET_BUILD_VARIANT"); ok {
+		return v
+	}
+	panic("TARGET_BUILD_VARIANT is not defined")
+}
+
 func (c *configImpl) KatiArgs() []string {
 	return c.katiArgs
 }
@@ -337,6 +369,10 @@
 	return filepath.Join(c.SoongOutDir(), "make_vars-"+c.TargetProduct()+".mk")
 }
 
+func (c *configImpl) DevicePreviousProductConfig() string {
+	return filepath.Join(c.OutDir(), "target", "product", c.TargetDevice(), "previous_build_config.mk")
+}
+
 func (c *configImpl) HostPrebuiltTag() string {
 	if runtime.GOOS == "linux" {
 		return "linux-x86"
diff --git a/ui/build/make.go b/ui/build/make.go
index 32dc17b..2b39926 100644
--- a/ui/build/make.go
+++ b/ui/build/make.go
@@ -83,6 +83,7 @@
 		// So that we can use the correct TARGET_PRODUCT if it's been
 		// modified by PRODUCT-* arguments
 		"TARGET_PRODUCT",
+		"TARGET_BUILD_VARIANT",
 
 		// compiler wrappers set up by make
 		"CC_WRAPPER",
@@ -129,6 +130,9 @@
 		// Used to execute Kati and Ninja
 		"NINJA_GOALS",
 		"KATI_GOALS",
+
+		// To find target/product/<DEVICE>
+		"TARGET_DEVICE",
 	}, exportEnvVars...), bannerVars...)
 
 	make_vars, err := DumpMakeVars(ctx, config, config.Arguments(), []string{
@@ -159,4 +163,5 @@
 
 	config.SetKatiArgs(strings.Fields(make_vars["KATI_GOALS"]))
 	config.SetNinjaArgs(strings.Fields(make_vars["NINJA_GOALS"]))
+	config.SetTargetDevice(make_vars["TARGET_DEVICE"])
 }