Add EXTERNAL_FILE option for ninja weight list

Test: m --ninja_weight_source=file,<file path>
Bug: 271527305
Change-Id: Ibeae4c757dff281be69486a9758dbee3584d9dec
diff --git a/ui/build/config.go b/ui/build/config.go
index 20cc9fb..3fb38d7 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -132,6 +132,8 @@
 	NINJA_LOG
 	// ninja thinks every task has the same weight.
 	EVENLY_DISTRIBUTED
+	// ninja uses an external custom weight list
+	EXTERNAL_FILE
 )
 const srcDirFileCheck = "build/soong/root.bp"
 
@@ -547,6 +549,8 @@
 		return smpb.BuildConfig_NINJA_LOG.Enum()
 	case EVENLY_DISTRIBUTED:
 		return smpb.BuildConfig_EVENLY_DISTRIBUTED.Enum()
+	case EXTERNAL_FILE:
+		return smpb.BuildConfig_EXTERNAL_FILE.Enum()
 	default:
 		return smpb.BuildConfig_NOT_USED.Enum()
 	}
@@ -830,6 +834,17 @@
 				c.ninjaWeightListSource = EVENLY_DISTRIBUTED
 			} else if source == "not_used" {
 				c.ninjaWeightListSource = NOT_USED
+			} else if strings.HasPrefix(source, "file,") {
+				c.ninjaWeightListSource = EXTERNAL_FILE
+				filePath := strings.TrimPrefix(source, "file,")
+				err := validateNinjaWeightList(filePath)
+				if err != nil {
+					ctx.Fatalf("Malformed weight list from %s: %s", filePath, err)
+				}
+				_, err = copyFile(filePath, filepath.Join(c.OutDir(), ".ninja_weight_list"))
+				if err != nil {
+					ctx.Fatalf("Error to copy ninja weight list from %s: %s", filePath, err)
+				}
 			} else {
 				ctx.Fatalf("unknown option for ninja_weight_source: %s", source)
 			}
@@ -903,6 +918,25 @@
 	}
 }
 
+func validateNinjaWeightList(weightListFilePath string) (err error) {
+	data, err := os.ReadFile(weightListFilePath)
+	if err != nil {
+		return
+	}
+	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
+	for _, line := range lines {
+		fields := strings.Split(line, ",")
+		if len(fields) != 2 {
+			return fmt.Errorf("wrong format, each line should have two fields, but '%s'", line)
+		}
+		_, err = strconv.Atoi(fields[1])
+		if err != nil {
+			return
+		}
+	}
+	return
+}
+
 func (c *configImpl) configureLocale(ctx Context) {
 	cmd := Command(ctx, Config{c}, "locale", "locale", "-a")
 	output, err := cmd.Output()
diff --git a/ui/build/ninja.go b/ui/build/ninja.go
index 4734494..cd528ba 100644
--- a/ui/build/ninja.go
+++ b/ui/build/ninja.go
@@ -30,8 +30,9 @@
 
 const (
 	// File containing the environment state when ninja is executed
-	ninjaEnvFileName = "ninja.environment"
-	ninjaLogFileName = ".ninja_log"
+	ninjaEnvFileName        = "ninja.environment"
+	ninjaLogFileName        = ".ninja_log"
+	ninjaWeightListFileName = ".ninja_weight_list"
 )
 
 func useNinjaBuildLog(ctx Context, config Config, cmd *Cmd) {
@@ -66,7 +67,7 @@
 		ctx.Verbosef("There is an error during reading ninja log, so ninja will use empty weight list: %s", err)
 	}
 
-	weightListFile := filepath.Join(config.OutDir(), ".ninja_weight_list")
+	weightListFile := filepath.Join(config.OutDir(), ninjaWeightListFileName)
 
 	err = os.WriteFile(weightListFile, []byte(outputBuilder.String()), 0644)
 	if err == nil {
@@ -134,6 +135,10 @@
 	case EVENLY_DISTRIBUTED:
 		// pass empty weight list means ninja considers every tasks's weight as 1(default value).
 		cmd.Args = append(cmd.Args, "-o", "usesweightlist=/dev/null")
+	case EXTERNAL_FILE:
+		// The weight list is already copied.
+		ninjaWeightListPath := filepath.Join(config.OutDir(), ninjaWeightListFileName)
+		cmd.Args = append(cmd.Args, "-o", "usesweightlist="+ninjaWeightListPath)
 	}
 
 	// Allow both NINJA_ARGS and NINJA_EXTRA_ARGS, since both have been