| // 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 main |
| |
| import ( |
| "bufio" |
| "context" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "strings" |
| "sync" |
| "syscall" |
| "time" |
| |
| "android/soong/ui/logger" |
| "android/soong/ui/signal" |
| "android/soong/ui/status" |
| "android/soong/ui/terminal" |
| "android/soong/ui/tracer" |
| "android/soong/zip" |
| ) |
| |
| var numJobs = flag.Int("j", 0, "number of parallel jobs [0=autodetect]") |
| |
| var keepArtifacts = flag.Bool("keep", false, "keep archives of artifacts") |
| var incremental = flag.Bool("incremental", false, "run in incremental mode (saving intermediates)") |
| |
| var outDir = flag.String("out", "", "path to store output directories (defaults to tmpdir under $OUT when empty)") |
| var alternateResultDir = flag.Bool("dist", false, "write select results to $DIST_DIR (or <out>/dist when empty)") |
| |
| var onlyConfig = flag.Bool("only-config", false, "Only run product config (not Soong or Kati)") |
| var onlySoong = flag.Bool("only-soong", false, "Only run product config and Soong (not Kati)") |
| |
| var buildVariant = flag.String("variant", "eng", "build variant to use") |
| |
| var shardCount = flag.Int("shard-count", 1, "split the products into multiple shards (to spread the build onto multiple machines, etc)") |
| var shard = flag.Int("shard", 1, "1-indexed shard to execute") |
| |
| var skipProducts multipleStringArg |
| var includeProducts multipleStringArg |
| |
| func init() { |
| flag.Var(&skipProducts, "skip-products", "comma-separated list of products to skip (known failures, etc)") |
| flag.Var(&includeProducts, "products", "comma-separated list of products to build") |
| } |
| |
| // multipleStringArg is a flag.Value that takes comma separated lists and converts them to a |
| // []string. The argument can be passed multiple times to append more values. |
| type multipleStringArg []string |
| |
| func (m *multipleStringArg) String() string { |
| return strings.Join(*m, `, `) |
| } |
| |
| func (m *multipleStringArg) Set(s string) error { |
| *m = append(*m, strings.Split(s, ",")...) |
| return nil |
| } |
| |
| const errorLeadingLines = 20 |
| const errorTrailingLines = 20 |
| |
| func errMsgFromLog(filename string) string { |
| if filename == "" { |
| return "" |
| } |
| |
| data, err := ioutil.ReadFile(filename) |
| if err != nil { |
| return "" |
| } |
| |
| lines := strings.Split(strings.TrimSpace(string(data)), "\n") |
| if len(lines) > errorLeadingLines+errorTrailingLines+1 { |
| lines[errorLeadingLines] = fmt.Sprintf("... skipping %d lines ...", |
| len(lines)-errorLeadingLines-errorTrailingLines) |
| |
| lines = append(lines[:errorLeadingLines+1], |
| lines[len(lines)-errorTrailingLines:]...) |
| } |
| var buf strings.Builder |
| for _, line := range lines { |
| buf.WriteString("> ") |
| buf.WriteString(line) |
| buf.WriteString("\n") |
| } |
| return buf.String() |
| } |
| |
| // TODO(b/70370883): This tool uses a lot of open files -- over the default |
| // soft limit of 1024 on some systems. So bump up to the hard limit until I fix |
| // the algorithm. |
| func setMaxFiles(log logger.Logger) { |
| var limits syscall.Rlimit |
| |
| err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limits) |
| if err != nil { |
| log.Println("Failed to get file limit:", err) |
| return |
| } |
| |
| log.Verbosef("Current file limits: %d soft, %d hard", limits.Cur, limits.Max) |
| if limits.Cur == limits.Max { |
| return |
| } |
| |
| limits.Cur = limits.Max |
| err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limits) |
| if err != nil { |
| log.Println("Failed to increase file limit:", err) |
| } |
| } |
| |
| func inList(str string, list []string) bool { |
| for _, other := range list { |
| if str == other { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func copyFile(from, to string) error { |
| fromFile, err := os.Open(from) |
| if err != nil { |
| return err |
| } |
| defer fromFile.Close() |
| |
| toFile, err := os.Create(to) |
| if err != nil { |
| return err |
| } |
| defer toFile.Close() |
| |
| _, err = io.Copy(toFile, fromFile) |
| return err |
| } |
| |
| type mpContext struct { |
| Logger logger.Logger |
| Status status.ToolStatus |
| |
| SoongUi string |
| MainOutDir string |
| MainLogsDir string |
| } |
| |
| func findNamedProducts(soongUi string, log logger.Logger) []string { |
| cmd := exec.Command(soongUi, "--dumpvars-mode", "--vars=all_named_products") |
| output, err := cmd.Output() |
| if err != nil { |
| log.Fatalf("Cannot determine named products: %v", err) |
| } |
| |
| rx := regexp.MustCompile(`^all_named_products='(.*)'$`) |
| match := rx.FindStringSubmatch(strings.TrimSpace(string(output))) |
| return strings.Fields(match[1]) |
| } |
| |
| // ensureEmptyFileExists ensures that the containing directory exists, and the |
| // specified file exists. If it doesn't exist, it will write an empty file. |
| func ensureEmptyFileExists(file string, log logger.Logger) { |
| if _, err := os.Stat(file); os.IsNotExist(err) { |
| f, err := os.Create(file) |
| if err != nil { |
| log.Fatalf("Error creating %s: %q\n", file, err) |
| } |
| f.Close() |
| } else if err != nil { |
| log.Fatalf("Error checking %s: %q\n", file, err) |
| } |
| } |
| |
| func outDirBase() string { |
| outDirBase := os.Getenv("OUT_DIR") |
| if outDirBase == "" { |
| return "out" |
| } else { |
| return outDirBase |
| } |
| } |
| |
| func distDir(outDir string) string { |
| if distDir := os.Getenv("DIST_DIR"); distDir != "" { |
| return filepath.Clean(distDir) |
| } else { |
| return filepath.Join(outDir, "dist") |
| } |
| } |
| |
| func forceAnsiOutput() bool { |
| value := os.Getenv("SOONG_UI_ANSI_OUTPUT") |
| return value == "1" || value == "y" || value == "yes" || value == "on" || value == "true" |
| } |
| |
| func main() { |
| stdio := terminal.StdioImpl{} |
| |
| output := terminal.NewStatusOutput(stdio.Stdout(), "", false, false, |
| forceAnsiOutput()) |
| log := logger.New(output) |
| defer log.Cleanup() |
| |
| for _, v := range os.Environ() { |
| log.Println("Environment: " + v) |
| } |
| |
| log.Printf("Argv: %v\n", os.Args) |
| |
| flag.Parse() |
| |
| _, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| |
| trace := tracer.New(log) |
| defer trace.Close() |
| |
| stat := &status.Status{} |
| defer stat.Finish() |
| stat.AddOutput(output) |
| |
| var failures failureCount |
| stat.AddOutput(&failures) |
| |
| signal.SetupSignals(log, cancel, func() { |
| trace.Close() |
| log.Cleanup() |
| stat.Finish() |
| }) |
| |
| soongUi := "build/soong/soong_ui.bash" |
| |
| var outputDir string |
| if *outDir != "" { |
| outputDir = *outDir |
| } else { |
| name := "multiproduct" |
| if !*incremental { |
| name += "-" + time.Now().Format("20060102150405") |
| } |
| outputDir = filepath.Join(outDirBase(), name) |
| } |
| |
| log.Println("Output directory:", outputDir) |
| |
| // The ninja_build file is used by our buildbots to understand that the output |
| // can be parsed as ninja output. |
| if err := os.MkdirAll(outputDir, 0777); err != nil { |
| log.Fatalf("Failed to create output directory: %v", err) |
| } |
| ensureEmptyFileExists(filepath.Join(outputDir, "ninja_build"), log) |
| |
| logsDir := filepath.Join(outputDir, "logs") |
| os.MkdirAll(logsDir, 0777) |
| |
| var configLogsDir string |
| if *alternateResultDir { |
| configLogsDir = filepath.Join(distDir(outDirBase()), "logs") |
| } else { |
| configLogsDir = outputDir |
| } |
| |
| log.Println("Logs dir: " + configLogsDir) |
| |
| os.MkdirAll(configLogsDir, 0777) |
| log.SetOutput(filepath.Join(configLogsDir, "soong.log")) |
| trace.SetOutput(filepath.Join(configLogsDir, "build.trace")) |
| |
| var jobs = *numJobs |
| if jobs < 1 { |
| jobs = runtime.NumCPU() / 4 |
| |
| ramGb := int(detectTotalRAM() / (1024 * 1024 * 1024)) |
| if ramJobs := ramGb / 30; ramGb > 0 && jobs > ramJobs { |
| jobs = ramJobs |
| } |
| |
| if jobs < 1 { |
| jobs = 1 |
| } |
| } |
| log.Verbosef("Using %d parallel jobs", jobs) |
| |
| setMaxFiles(log) |
| |
| allProducts := findNamedProducts(soongUi, log) |
| var productsList []string |
| |
| if len(includeProducts) > 0 { |
| var missingProducts []string |
| for _, product := range includeProducts { |
| if inList(product, allProducts) { |
| productsList = append(productsList, product) |
| } else { |
| missingProducts = append(missingProducts, product) |
| } |
| } |
| if len(missingProducts) > 0 { |
| log.Fatalf("Products don't exist: %s\n", missingProducts) |
| } |
| } else { |
| productsList = allProducts |
| } |
| |
| finalProductsList := make([]string, 0, len(productsList)) |
| skipProduct := func(p string) bool { |
| for _, s := range skipProducts { |
| if p == s { |
| return true |
| } |
| } |
| return false |
| } |
| for _, product := range productsList { |
| if !skipProduct(product) { |
| finalProductsList = append(finalProductsList, product) |
| } else { |
| log.Verbose("Skipping: ", product) |
| } |
| } |
| |
| if *shard < 1 { |
| log.Fatalf("--shard value must be >= 1, not %d\n", *shard) |
| } else if *shardCount < 1 { |
| log.Fatalf("--shard-count value must be >= 1, not %d\n", *shardCount) |
| } else if *shard > *shardCount { |
| log.Fatalf("--shard (%d) must not be greater than --shard-count (%d)\n", *shard, |
| *shardCount) |
| } else if *shardCount > 1 { |
| finalProductsList = splitList(finalProductsList, *shardCount)[*shard-1] |
| } |
| |
| log.Verbose("Got product list: ", finalProductsList) |
| |
| s := stat.StartTool() |
| s.SetTotalActions(len(finalProductsList)) |
| |
| mpCtx := &mpContext{ |
| Logger: log, |
| Status: s, |
| SoongUi: soongUi, |
| MainOutDir: outputDir, |
| MainLogsDir: logsDir, |
| } |
| |
| products := make(chan string, len(productsList)) |
| go func() { |
| defer close(products) |
| for _, product := range finalProductsList { |
| products <- product |
| } |
| }() |
| |
| var wg sync.WaitGroup |
| for i := 0; i < jobs; i++ { |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| for { |
| select { |
| case product := <-products: |
| if product == "" { |
| return |
| } |
| runSoongUiForProduct(mpCtx, product) |
| } |
| } |
| }() |
| } |
| wg.Wait() |
| |
| if *alternateResultDir { |
| args := zip.ZipArgs{ |
| FileArgs: []zip.FileArg{ |
| {GlobDir: logsDir, SourcePrefixToStrip: logsDir}, |
| }, |
| OutputFilePath: filepath.Join(distDir(outDirBase()), "logs.zip"), |
| NumParallelJobs: runtime.NumCPU(), |
| CompressionLevel: 5, |
| } |
| log.Printf("Logs zip: %v\n", args.OutputFilePath) |
| if err := zip.Zip(args); err != nil { |
| log.Fatalf("Error zipping logs: %v", err) |
| } |
| } |
| |
| s.Finish() |
| |
| if failures.count == 1 { |
| log.Fatal("1 failure") |
| } else if failures.count > 1 { |
| log.Fatalf("%d failures %q", failures.count, failures.fails) |
| } else { |
| fmt.Fprintln(output, "Success") |
| } |
| } |
| |
| func cleanupAfterProduct(outDir, productZip string) { |
| if *keepArtifacts { |
| args := zip.ZipArgs{ |
| FileArgs: []zip.FileArg{ |
| { |
| GlobDir: outDir, |
| SourcePrefixToStrip: outDir, |
| }, |
| }, |
| OutputFilePath: productZip, |
| NumParallelJobs: runtime.NumCPU(), |
| CompressionLevel: 5, |
| } |
| if err := zip.Zip(args); err != nil { |
| log.Fatalf("Error zipping artifacts: %v", err) |
| } |
| } |
| if !*incremental { |
| os.RemoveAll(outDir) |
| } |
| } |
| |
| func runSoongUiForProduct(mpctx *mpContext, product string) { |
| outDir := filepath.Join(mpctx.MainOutDir, product) |
| logsDir := filepath.Join(mpctx.MainLogsDir, product) |
| productZip := filepath.Join(mpctx.MainOutDir, product+".zip") |
| consoleLogPath := filepath.Join(logsDir, "std.log") |
| |
| if err := os.MkdirAll(outDir, 0777); err != nil { |
| mpctx.Logger.Fatalf("Error creating out directory: %v", err) |
| } |
| if err := os.MkdirAll(logsDir, 0777); err != nil { |
| mpctx.Logger.Fatalf("Error creating log directory: %v", err) |
| } |
| |
| consoleLogFile, err := os.Create(consoleLogPath) |
| if err != nil { |
| mpctx.Logger.Fatalf("Error creating console log file: %v", err) |
| } |
| defer consoleLogFile.Close() |
| |
| consoleLogWriter := bufio.NewWriter(consoleLogFile) |
| defer consoleLogWriter.Flush() |
| |
| args := []string{"--make-mode", "--skip-soong-tests", "--skip-ninja"} |
| |
| if !*keepArtifacts { |
| args = append(args, "--empty-ninja-file") |
| } |
| |
| if *onlyConfig { |
| args = append(args, "--config-only") |
| } else if *onlySoong { |
| args = append(args, "--soong-only") |
| } |
| |
| cmd := exec.Command(mpctx.SoongUi, args...) |
| cmd.Stdout = consoleLogWriter |
| cmd.Stderr = consoleLogWriter |
| cmd.Env = append(os.Environ(), |
| "OUT_DIR="+outDir, |
| "TARGET_PRODUCT="+product, |
| "TARGET_BUILD_VARIANT="+*buildVariant, |
| "TARGET_BUILD_TYPE=release", |
| "TARGET_BUILD_APPS=", |
| "TARGET_BUILD_UNBUNDLED=") |
| |
| if *alternateResultDir { |
| cmd.Env = append(cmd.Env, |
| "DIST_DIR="+filepath.Join(distDir(outDirBase()), "products/"+product)) |
| } |
| |
| action := &status.Action{ |
| Description: product, |
| Outputs: []string{product}, |
| } |
| |
| mpctx.Status.StartAction(action) |
| defer cleanupAfterProduct(outDir, productZip) |
| |
| before := time.Now() |
| err = cmd.Run() |
| |
| if !*onlyConfig && !*onlySoong { |
| katiBuildNinjaFile := filepath.Join(outDir, "build-"+product+".ninja") |
| if after, err := os.Stat(katiBuildNinjaFile); err == nil && after.ModTime().After(before) { |
| err := copyFile(consoleLogPath, filepath.Join(filepath.Dir(consoleLogPath), "std_full.log")) |
| if err != nil { |
| log.Fatalf("Error copying log file: %s", err) |
| } |
| } |
| } |
| var errOutput string |
| if err == nil { |
| errOutput = "" |
| } else { |
| errOutput = errMsgFromLog(consoleLogPath) |
| } |
| |
| mpctx.Status.FinishAction(status.ActionResult{ |
| Action: action, |
| Error: err, |
| Output: errOutput, |
| }) |
| } |
| |
| type failureCount struct { |
| count int |
| fails []string |
| } |
| |
| func (f *failureCount) StartAction(action *status.Action, counts status.Counts) {} |
| |
| func (f *failureCount) FinishAction(result status.ActionResult, counts status.Counts) { |
| if result.Error != nil { |
| f.count += 1 |
| f.fails = append(f.fails, result.Action.Description) |
| } |
| } |
| |
| func (f *failureCount) Message(level status.MsgLevel, message string) { |
| if level >= status.ErrorLvl { |
| f.count += 1 |
| } |
| } |
| |
| func (f *failureCount) Flush() {} |
| |
| func (f *failureCount) Write(p []byte) (int, error) { |
| // discard writes |
| return len(p), nil |
| } |
| |
| func splitList(list []string, shardCount int) (ret [][]string) { |
| each := len(list) / shardCount |
| extra := len(list) % shardCount |
| for i := 0; i < shardCount; i++ { |
| count := each |
| if extra > 0 { |
| count += 1 |
| extra -= 1 |
| } |
| ret = append(ret, list[:count]) |
| list = list[count:] |
| } |
| return |
| } |