1// Copyright 2017 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package main 16 17import ( 18 "bufio" 19 "context" 20 "flag" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "log" 25 "os" 26 "os/exec" 27 "path/filepath" 28 "regexp" 29 "runtime" 30 "strings" 31 "sync" 32 "syscall" 33 "time" 34 35 "android/soong/ui/logger" 36 "android/soong/ui/signal" 37 "android/soong/ui/status" 38 "android/soong/ui/terminal" 39 "android/soong/ui/tracer" 40 "android/soong/zip" 41) 42 43var numJobs = flag.Int("j", 0, "number of parallel jobs [0=autodetect]") 44 45var keepArtifacts = flag.Bool("keep", false, "keep archives of artifacts") 46var incremental = flag.Bool("incremental", false, "run in incremental mode (saving intermediates)") 47 48var outDir = flag.String("out", "", "path to store output directories (defaults to tmpdir under $OUT when empty)") 49var alternateResultDir = flag.Bool("dist", false, "write select results to $DIST_DIR (or <out>/dist when empty)") 50 51var bazelMode = flag.Bool("bazel-mode", false, "use bazel for analysis of certain modules") 52var bazelModeStaging = flag.Bool("bazel-mode-staging", false, "use bazel for analysis of certain near-ready modules") 53var bazelModeDev = flag.Bool("bazel-mode-dev", false, "use bazel for analysis of a large number of modules (less stable)") 54 55var onlyConfig = flag.Bool("only-config", false, "Only run product config (not Soong or Kati)") 56var onlySoong = flag.Bool("only-soong", false, "Only run product config and Soong (not Kati)") 57 58var buildVariant = flag.String("variant", "eng", "build variant to use") 59 60var shardCount = flag.Int("shard-count", 1, "split the products into multiple shards (to spread the build onto multiple machines, etc)") 61var shard = flag.Int("shard", 1, "1-indexed shard to execute") 62 63var skipProducts multipleStringArg 64var includeProducts multipleStringArg 65 66func init() { 67 flag.Var(&skipProducts, "skip-products", "comma-separated list of products to skip (known failures, etc)") 68 flag.Var(&includeProducts, "products", "comma-separated list of products to build") 69} 70 71// multipleStringArg is a flag.Value that takes comma separated lists and converts them to a 72// []string. The argument can be passed multiple times to append more values. 73type multipleStringArg []string 74 75func (m *multipleStringArg) String() string { 76 return strings.Join(*m, `, `) 77} 78 79func (m *multipleStringArg) Set(s string) error { 80 *m = append(*m, strings.Split(s, ",")...) 81 return nil 82} 83 84const errorLeadingLines = 20 85const errorTrailingLines = 20 86 87func errMsgFromLog(filename string) string { 88 if filename == "" { 89 return "" 90 } 91 92 data, err := ioutil.ReadFile(filename) 93 if err != nil { 94 return "" 95 } 96 97 lines := strings.Split(strings.TrimSpace(string(data)), "\n") 98 if len(lines) > errorLeadingLines+errorTrailingLines+1 { 99 lines[errorLeadingLines] = fmt.Sprintf("... skipping %d lines ...", 100 len(lines)-errorLeadingLines-errorTrailingLines) 101 102 lines = append(lines[:errorLeadingLines+1], 103 lines[len(lines)-errorTrailingLines:]...) 104 } 105 var buf strings.Builder 106 for _, line := range lines { 107 buf.WriteString("> ") 108 buf.WriteString(line) 109 buf.WriteString("\n") 110 } 111 return buf.String() 112} 113 114// TODO(b/70370883): This tool uses a lot of open files -- over the default 115// soft limit of 1024 on some systems. So bump up to the hard limit until I fix 116// the algorithm. 117func setMaxFiles(log logger.Logger) { 118 var limits syscall.Rlimit 119 120 err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limits) 121 if err != nil { 122 log.Println("Failed to get file limit:", err) 123 return 124 } 125 126 log.Verbosef("Current file limits: %d soft, %d hard", limits.Cur, limits.Max) 127 if limits.Cur == limits.Max { 128 return 129 } 130 131 limits.Cur = limits.Max 132 err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limits) 133 if err != nil { 134 log.Println("Failed to increase file limit:", err) 135 } 136} 137 138func inList(str string, list []string) bool { 139 for _, other := range list { 140 if str == other { 141 return true 142 } 143 } 144 return false 145} 146 147func copyFile(from, to string) error { 148 fromFile, err := os.Open(from) 149 if err != nil { 150 return err 151 } 152 defer fromFile.Close() 153 154 toFile, err := os.Create(to) 155 if err != nil { 156 return err 157 } 158 defer toFile.Close() 159 160 _, err = io.Copy(toFile, fromFile) 161 return err 162} 163 164type mpContext struct { 165 Logger logger.Logger 166 Status status.ToolStatus 167 168 SoongUi string 169 MainOutDir string 170 MainLogsDir string 171} 172 173func findNamedProducts(soongUi string, log logger.Logger) []string { 174 cmd := exec.Command(soongUi, "--dumpvars-mode", "--vars=all_named_products") 175 output, err := cmd.Output() 176 if err != nil { 177 log.Fatalf("Cannot determine named products: %v", err) 178 } 179 180 rx := regexp.MustCompile(`^all_named_products='(.*)'$`) 181 match := rx.FindStringSubmatch(strings.TrimSpace(string(output))) 182 return strings.Fields(match[1]) 183} 184 185// ensureEmptyFileExists ensures that the containing directory exists, and the 186// specified file exists. If it doesn't exist, it will write an empty file. 187func ensureEmptyFileExists(file string, log logger.Logger) { 188 if _, err := os.Stat(file); os.IsNotExist(err) { 189 f, err := os.Create(file) 190 if err != nil { 191 log.Fatalf("Error creating %s: %q\n", file, err) 192 } 193 f.Close() 194 } else if err != nil { 195 log.Fatalf("Error checking %s: %q\n", file, err) 196 } 197} 198 199func outDirBase() string { 200 outDirBase := os.Getenv("OUT_DIR") 201 if outDirBase == "" { 202 return "out" 203 } else { 204 return outDirBase 205 } 206} 207 208func distDir(outDir string) string { 209 if distDir := os.Getenv("DIST_DIR"); distDir != "" { 210 return filepath.Clean(distDir) 211 } else { 212 return filepath.Join(outDir, "dist") 213 } 214} 215 216func forceAnsiOutput() bool { 217 value := os.Getenv("SOONG_UI_ANSI_OUTPUT") 218 return value == "1" || value == "y" || value == "yes" || value == "on" || value == "true" 219} 220 221func getBazelArg() string { 222 count := 0 223 str := "" 224 if *bazelMode { 225 count++ 226 str = "--bazel-mode" 227 } 228 if *bazelModeStaging { 229 count++ 230 str = "--bazel-mode-staging" 231 } 232 if *bazelModeDev { 233 count++ 234 str = "--bazel-mode-dev" 235 } 236 237 if count > 1 { 238 // Can't set more than one 239 fmt.Errorf("Only one bazel mode is permitted to be set.") 240 os.Exit(1) 241 } 242 243 return str 244} 245 246func main() { 247 stdio := terminal.StdioImpl{} 248 249 output := terminal.NewStatusOutput(stdio.Stdout(), "", false, false, 250 forceAnsiOutput()) 251 log := logger.New(output) 252 defer log.Cleanup() 253 254 for _, v := range os.Environ() { 255 log.Println("Environment: " + v) 256 } 257 258 log.Printf("Argv: %v\n", os.Args) 259 260 flag.Parse() 261 262 _, cancel := context.WithCancel(context.Background()) 263 defer cancel() 264 265 trace := tracer.New(log) 266 defer trace.Close() 267 268 stat := &status.Status{} 269 defer stat.Finish() 270 stat.AddOutput(output) 271 272 var failures failureCount 273 stat.AddOutput(&failures) 274 275 signal.SetupSignals(log, cancel, func() { 276 trace.Close() 277 log.Cleanup() 278 stat.Finish() 279 }) 280 281 soongUi := "build/soong/soong_ui.bash" 282 283 var outputDir string 284 if *outDir != "" { 285 outputDir = *outDir 286 } else { 287 name := "multiproduct" 288 if !*incremental { 289 name += "-" + time.Now().Format("20060102150405") 290 } 291 outputDir = filepath.Join(outDirBase(), name) 292 } 293 294 log.Println("Output directory:", outputDir) 295 296 // The ninja_build file is used by our buildbots to understand that the output 297 // can be parsed as ninja output. 298 if err := os.MkdirAll(outputDir, 0777); err != nil { 299 log.Fatalf("Failed to create output directory: %v", err) 300 } 301 ensureEmptyFileExists(filepath.Join(outputDir, "ninja_build"), log) 302 303 logsDir := filepath.Join(outputDir, "logs") 304 os.MkdirAll(logsDir, 0777) 305 306 var configLogsDir string 307 if *alternateResultDir { 308 configLogsDir = filepath.Join(distDir(outDirBase()), "logs") 309 } else { 310 configLogsDir = outputDir 311 } 312 313 log.Println("Logs dir: " + configLogsDir) 314 315 os.MkdirAll(configLogsDir, 0777) 316 log.SetOutput(filepath.Join(configLogsDir, "soong.log")) 317 trace.SetOutput(filepath.Join(configLogsDir, "build.trace")) 318 319 var jobs = *numJobs 320 if jobs < 1 { 321 jobs = runtime.NumCPU() / 4 322 323 ramGb := int(detectTotalRAM() / (1024 * 1024 * 1024)) 324 if ramJobs := ramGb / 40; ramGb > 0 && jobs > ramJobs { 325 jobs = ramJobs 326 } 327 328 if jobs < 1 { 329 jobs = 1 330 } 331 } 332 log.Verbosef("Using %d parallel jobs", jobs) 333 334 setMaxFiles(log) 335 336 allProducts := findNamedProducts(soongUi, log) 337 var productsList []string 338 339 if len(includeProducts) > 0 { 340 var missingProducts []string 341 for _, product := range includeProducts { 342 if inList(product, allProducts) { 343 productsList = append(productsList, product) 344 } else { 345 missingProducts = append(missingProducts, product) 346 } 347 } 348 if len(missingProducts) > 0 { 349 log.Fatalf("Products don't exist: %s\n", missingProducts) 350 } 351 } else { 352 productsList = allProducts 353 } 354 355 finalProductsList := make([]string, 0, len(productsList)) 356 skipProduct := func(p string) bool { 357 for _, s := range skipProducts { 358 if p == s { 359 return true 360 } 361 } 362 return false 363 } 364 for _, product := range productsList { 365 if !skipProduct(product) { 366 finalProductsList = append(finalProductsList, product) 367 } else { 368 log.Verbose("Skipping: ", product) 369 } 370 } 371 372 if *shard < 1 { 373 log.Fatalf("--shard value must be >= 1, not %d\n", *shard) 374 } else if *shardCount < 1 { 375 log.Fatalf("--shard-count value must be >= 1, not %d\n", *shardCount) 376 } else if *shard > *shardCount { 377 log.Fatalf("--shard (%d) must not be greater than --shard-count (%d)\n", *shard, 378 *shardCount) 379 } else if *shardCount > 1 { 380 finalProductsList = splitList(finalProductsList, *shardCount)[*shard-1] 381 } 382 383 log.Verbose("Got product list: ", finalProductsList) 384 385 s := stat.StartTool() 386 s.SetTotalActions(len(finalProductsList)) 387 388 mpCtx := &mpContext{ 389 Logger: log, 390 Status: s, 391 SoongUi: soongUi, 392 MainOutDir: outputDir, 393 MainLogsDir: logsDir, 394 } 395 396 products := make(chan string, len(productsList)) 397 go func() { 398 defer close(products) 399 for _, product := range finalProductsList { 400 products <- product 401 } 402 }() 403 404 var wg sync.WaitGroup 405 for i := 0; i < jobs; i++ { 406 wg.Add(1) 407 go func() { 408 defer wg.Done() 409 for { 410 select { 411 case product := <-products: 412 if product == "" { 413 return 414 } 415 runSoongUiForProduct(mpCtx, product) 416 } 417 } 418 }() 419 } 420 wg.Wait() 421 422 if *alternateResultDir { 423 args := zip.ZipArgs{ 424 FileArgs: []zip.FileArg{ 425 {GlobDir: logsDir, SourcePrefixToStrip: logsDir}, 426 }, 427 OutputFilePath: filepath.Join(distDir(outDirBase()), "logs.zip"), 428 NumParallelJobs: runtime.NumCPU(), 429 CompressionLevel: 5, 430 } 431 log.Printf("Logs zip: %v\n", args.OutputFilePath) 432 if err := zip.Zip(args); err != nil { 433 log.Fatalf("Error zipping logs: %v", err) 434 } 435 } 436 437 s.Finish() 438 439 if failures.count == 1 { 440 log.Fatal("1 failure") 441 } else if failures.count > 1 { 442 log.Fatalf("%d failures %q", failures.count, failures.fails) 443 } else { 444 fmt.Fprintln(output, "Success") 445 } 446} 447 448func cleanupAfterProduct(outDir, productZip string) { 449 if *keepArtifacts { 450 args := zip.ZipArgs{ 451 FileArgs: []zip.FileArg{ 452 { 453 GlobDir: outDir, 454 SourcePrefixToStrip: outDir, 455 }, 456 }, 457 OutputFilePath: productZip, 458 NumParallelJobs: runtime.NumCPU(), 459 CompressionLevel: 5, 460 } 461 if err := zip.Zip(args); err != nil { 462 log.Fatalf("Error zipping artifacts: %v", err) 463 } 464 } 465 if !*incremental { 466 os.RemoveAll(outDir) 467 } 468} 469 470func runSoongUiForProduct(mpctx *mpContext, product string) { 471 outDir := filepath.Join(mpctx.MainOutDir, product) 472 logsDir := filepath.Join(mpctx.MainLogsDir, product) 473 productZip := filepath.Join(mpctx.MainOutDir, product+".zip") 474 consoleLogPath := filepath.Join(logsDir, "std.log") 475 476 if err := os.MkdirAll(outDir, 0777); err != nil { 477 mpctx.Logger.Fatalf("Error creating out directory: %v", err) 478 } 479 if err := os.MkdirAll(logsDir, 0777); err != nil { 480 mpctx.Logger.Fatalf("Error creating log directory: %v", err) 481 } 482 483 consoleLogFile, err := os.Create(consoleLogPath) 484 if err != nil { 485 mpctx.Logger.Fatalf("Error creating console log file: %v", err) 486 } 487 defer consoleLogFile.Close() 488 489 consoleLogWriter := bufio.NewWriter(consoleLogFile) 490 defer consoleLogWriter.Flush() 491 492 args := []string{"--make-mode", "--skip-soong-tests", "--skip-ninja"} 493 494 if !*keepArtifacts { 495 args = append(args, "--empty-ninja-file") 496 } 497 498 if *onlyConfig { 499 args = append(args, "--config-only") 500 } else if *onlySoong { 501 args = append(args, "--soong-only") 502 } 503 504 bazelStr := getBazelArg() 505 if bazelStr != "" { 506 args = append(args, bazelStr) 507 } 508 509 cmd := exec.Command(mpctx.SoongUi, args...) 510 cmd.Stdout = consoleLogWriter 511 cmd.Stderr = consoleLogWriter 512 cmd.Env = append(os.Environ(), 513 "OUT_DIR="+outDir, 514 "TARGET_PRODUCT="+product, 515 "TARGET_BUILD_VARIANT="+*buildVariant, 516 "TARGET_BUILD_TYPE=release", 517 "TARGET_BUILD_APPS=", 518 "TARGET_BUILD_UNBUNDLED=", 519 "USE_RBE=false") // Disabling RBE saves ~10 secs per product 520 521 if *alternateResultDir { 522 cmd.Env = append(cmd.Env, 523 "DIST_DIR="+filepath.Join(distDir(outDirBase()), "products/"+product)) 524 } 525 526 action := &status.Action{ 527 Description: product, 528 Outputs: []string{product}, 529 } 530 531 mpctx.Status.StartAction(action) 532 defer cleanupAfterProduct(outDir, productZip) 533 534 before := time.Now() 535 err = cmd.Run() 536 537 if !*onlyConfig && !*onlySoong { 538 katiBuildNinjaFile := filepath.Join(outDir, "build-"+product+".ninja") 539 if after, err := os.Stat(katiBuildNinjaFile); err == nil && after.ModTime().After(before) { 540 err := copyFile(consoleLogPath, filepath.Join(filepath.Dir(consoleLogPath), "std_full.log")) 541 if err != nil { 542 log.Fatalf("Error copying log file: %s", err) 543 } 544 } 545 } 546 var errOutput string 547 if err == nil { 548 errOutput = "" 549 } else { 550 errOutput = errMsgFromLog(consoleLogPath) 551 } 552 553 mpctx.Status.FinishAction(status.ActionResult{ 554 Action: action, 555 Error: err, 556 Output: errOutput, 557 }) 558} 559 560type failureCount struct { 561 count int 562 fails []string 563} 564 565func (f *failureCount) StartAction(action *status.Action, counts status.Counts) {} 566 567func (f *failureCount) FinishAction(result status.ActionResult, counts status.Counts) { 568 if result.Error != nil { 569 f.count += 1 570 f.fails = append(f.fails, result.Action.Description) 571 } 572} 573 574func (f *failureCount) Message(level status.MsgLevel, message string) { 575 if level >= status.ErrorLvl { 576 f.count += 1 577 } 578} 579 580func (f *failureCount) Flush() {} 581 582func (f *failureCount) Write(p []byte) (int, error) { 583 // discard writes 584 return len(p), nil 585} 586 587func splitList(list []string, shardCount int) (ret [][]string) { 588 each := len(list) / shardCount 589 extra := len(list) % shardCount 590 for i := 0; i < shardCount; i++ { 591 count := each 592 if extra > 0 { 593 count += 1 594 extra -= 1 595 } 596 ret = append(ret, list[:count]) 597 list = list[count:] 598 } 599 return 600} 601