Update SOONG_PARTIAL_COMPILE logic

Separate PARTIAL_COMPILE changes that affect analysis from actually
using it.

- SOONG_PARTIAL_COMPILE affects how we generate the Ninja file.  Changes
  to this cause reanalysis.

- SOONG_USE_PARTIAL_COMPILE is used in the rules to determine if we use
  the PARTIAL_COMPILE implementation, or the legacy rules.  This means
  that the developer can switch between full compiles and partial
  compiles at will without waiting for analysis to happen each time they
  change it.

Bug: b/365536323
Test: manual
Change-Id: Icb06687d17f63edb22bf2457a659b452537dadba
diff --git a/android/config.go b/android/config.go
index 06d71c0..5132c62 100644
--- a/android/config.go
+++ b/android/config.go
@@ -324,6 +324,9 @@
 	AndroidCommonTarget      Target // the Target for common modules for the Android device
 	AndroidFirstDeviceTarget Target // the first Target for modules for the Android device
 
+	// Flags for Partial Compile, derived from SOONG_PARTIAL_COMPILE.
+	partialCompileFlags partialCompileFlags
+
 	// multilibConflicts for an ArchType is true if there is earlier configured
 	// device architecture with the same multilib value.
 	multilibConflicts map[ArchType]bool
@@ -373,6 +376,16 @@
 	ensureAllowlistIntegrity bool
 }
 
+type partialCompileFlags struct {
+	// Is partial compilation enabled at all?
+	enabled bool
+
+	// Whether to use d8 instead of r8
+	use_d8 bool
+
+	// Add others as needed.
+}
+
 type deviceConfig struct {
 	config *config
 	OncePer
@@ -382,6 +395,88 @@
 	SetDefaultConfig()
 }
 
+// Parse SOONG_PARTIAL_COMPILE.
+//
+// SOONG_PARTIAL_COMPILE determines which features are enabled or disabled in
+// rule generation.  Changing this environment variable causes reanalysis.
+//
+// SOONG_USE_PARTIAL_COMPILE determines whether or not we **use** PARTIAL_COMPILE.
+// Rule generation must support both cases, since changing it does not cause
+// reanalysis.
+//
+// The user-facing documentation shows:
+//
+// - empty or not set: "The current default state"
+// - "true" or "on": enable all stable partial compile features.
+// - "false" or "off": disable partial compile completely.
+//
+// What we actually allow is a comma separated list of tokens, whose first
+// character may be "+" (enable) or "-" (disable).  If neither is present, "+"
+// is assumed.  For example, "on,+use_d8" will enable partial compilation, and
+// additionally set the use_d8 flag (regardless of whether it is opt-in or
+// opt-out).
+//
+// To add a new feature to the list, add the field in the struct
+// `partialCompileFlags` above, and then add the name of the field in the
+// switch statement below.
+func (c *config) parsePartialCompileFlags() (partialCompileFlags, error) {
+	defaultFlags := partialCompileFlags{
+		// Set any opt-out flags here.  Opt-in flags are off by default.
+		enabled: false,
+	}
+	value := c.Getenv("SOONG_PARTIAL_COMPILE")
+
+	if value == "" {
+		return defaultFlags, nil
+	}
+
+	ret := defaultFlags
+	tokens := strings.Split(strings.ToLower(value), ",")
+	makeVal := func(state string, defaultValue bool) bool {
+		switch state {
+		case "":
+			return defaultValue
+		case "-":
+			return false
+		case "+":
+			return true
+		}
+		return false
+	}
+	for _, tok := range tokens {
+		var state string
+		if len(tok) == 0 {
+			continue
+		}
+		switch tok[0:1] {
+		case "":
+			// Ignore empty tokens.
+			continue
+		case "-", "+":
+			state = tok[0:1]
+			tok = tok[1:]
+		default:
+			// Treat `feature` as `+feature`.
+			state = "+"
+		}
+		switch tok {
+		case "true":
+			ret = defaultFlags
+			ret.enabled = true
+		case "false":
+			// Set everything to false.
+			ret = partialCompileFlags{}
+		case "enabled":
+			ret.enabled = makeVal(state, defaultFlags.enabled)
+		case "use_d8":
+			ret.use_d8 = makeVal(state, defaultFlags.use_d8)
+		default:
+			return partialCompileFlags{}, fmt.Errorf("Unknown SOONG_PARTIAL_COMPILE value: %v", value)
+		}
+	}
+	return ret, nil
+}
+
 func loadConfig(config *config) error {
 	return loadFromConfigFile(&config.productVariables, absolutePath(config.ProductVariablesFileName))
 }
@@ -568,6 +663,11 @@
 		return Config{}, err
 	}
 
+	config.partialCompileFlags, err = config.parsePartialCompileFlags()
+	if err != nil {
+		return Config{}, err
+	}
+
 	// Make the CommonOS OsType available for all products.
 	targets[CommonOS] = []Target{commonTargetMap[CommonOS.Name]}
 
@@ -999,6 +1099,10 @@
 	return ApiLevelOrPanic(ctx, codename)
 }
 
+func (c *config) PartialCompileFlags() partialCompileFlags {
+	return c.partialCompileFlags
+}
+
 func (c *config) AppsDefaultVersionName() string {
 	return String(c.productVariables.AppsDefaultVersionName)
 }