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 "context" 19 "flag" 20 "fmt" 21 "io" 22 "io/ioutil" 23 "os" 24 "path/filepath" 25 "runtime" 26 "strings" 27 "sync" 28 "syscall" 29 "time" 30 31 "android/soong/finder" 32 "android/soong/ui/build" 33 "android/soong/ui/logger" 34 "android/soong/ui/status" 35 "android/soong/ui/terminal" 36 "android/soong/ui/tracer" 37 "android/soong/zip" 38) 39 40// We default to number of cpus / 4, which seems to be the sweet spot for my 41// system. I suspect this is mostly due to memory or disk bandwidth though, and 42// may depend on the size ofthe source tree, so this probably isn't a great 43// default. 44func detectNumJobs() int { 45 if runtime.NumCPU() < 4 { 46 return 1 47 } 48 return runtime.NumCPU() / 4 49} 50 51var numJobs = flag.Int("j", detectNumJobs(), "number of parallel kati jobs") 52 53var keepArtifacts = flag.Bool("keep", false, "keep archives of artifacts") 54var incremental = flag.Bool("incremental", false, "run in incremental mode (saving intermediates)") 55 56var outDir = flag.String("out", "", "path to store output directories (defaults to tmpdir under $OUT when empty)") 57var alternateResultDir = flag.Bool("dist", false, "write select results to $DIST_DIR (or <out>/dist when empty)") 58 59var onlyConfig = flag.Bool("only-config", false, "Only run product config (not Soong or Kati)") 60var onlySoong = flag.Bool("only-soong", false, "Only run product config and Soong (not Kati)") 61 62var buildVariant = flag.String("variant", "eng", "build variant to use") 63 64var skipProducts = flag.String("skip-products", "", "comma-separated list of products to skip (known failures, etc)") 65var includeProducts = flag.String("products", "", "comma-separated list of products to build") 66 67const errorLeadingLines = 20 68const errorTrailingLines = 20 69 70func errMsgFromLog(filename string) string { 71 if filename == "" { 72 return "" 73 } 74 75 data, err := ioutil.ReadFile(filename) 76 if err != nil { 77 return "" 78 } 79 80 lines := strings.Split(strings.TrimSpace(string(data)), "\n") 81 if len(lines) > errorLeadingLines+errorTrailingLines+1 { 82 lines[errorLeadingLines] = fmt.Sprintf("... skipping %d lines ...", 83 len(lines)-errorLeadingLines-errorTrailingLines) 84 85 lines = append(lines[:errorLeadingLines+1], 86 lines[len(lines)-errorTrailingLines:]...) 87 } 88 var buf strings.Builder 89 for _, line := range lines { 90 buf.WriteString("> ") 91 buf.WriteString(line) 92 buf.WriteString("\n") 93 } 94 return buf.String() 95} 96 97// TODO(b/70370883): This tool uses a lot of open files -- over the default 98// soft limit of 1024 on some systems. So bump up to the hard limit until I fix 99// the algorithm. 100func setMaxFiles(log logger.Logger) { 101 var limits syscall.Rlimit 102 103 err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limits) 104 if err != nil { 105 log.Println("Failed to get file limit:", err) 106 return 107 } 108 109 log.Verbosef("Current file limits: %d soft, %d hard", limits.Cur, limits.Max) 110 if limits.Cur == limits.Max { 111 return 112 } 113 114 limits.Cur = limits.Max 115 err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limits) 116 if err != nil { 117 log.Println("Failed to increase file limit:", err) 118 } 119} 120 121func inList(str string, list []string) bool { 122 for _, other := range list { 123 if str == other { 124 return true 125 } 126 } 127 return false 128} 129 130func copyFile(from, to string) error { 131 fromFile, err := os.Open(from) 132 if err != nil { 133 return err 134 } 135 defer fromFile.Close() 136 137 toFile, err := os.Create(to) 138 if err != nil { 139 return err 140 } 141 defer toFile.Close() 142 143 _, err = io.Copy(toFile, fromFile) 144 return err 145} 146 147type mpContext struct { 148 Context context.Context 149 Logger logger.Logger 150 Status status.ToolStatus 151 Tracer tracer.Tracer 152 Finder *finder.Finder 153 Config build.Config 154 155 LogsDir string 156} 157 158func main() { 159 writer := terminal.NewWriter(terminal.StdioImpl{}) 160 defer writer.Finish() 161 162 log := logger.New(writer) 163 defer log.Cleanup() 164 165 flag.Parse() 166 167 ctx, cancel := context.WithCancel(context.Background()) 168 defer cancel() 169 170 trace := tracer.New(log) 171 defer trace.Close() 172 173 stat := &status.Status{} 174 defer stat.Finish() 175 stat.AddOutput(terminal.NewStatusOutput(writer, "", 176 build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))) 177 178 var failures failureCount 179 stat.AddOutput(&failures) 180 181 build.SetupSignals(log, cancel, func() { 182 trace.Close() 183 log.Cleanup() 184 stat.Finish() 185 }) 186 187 buildCtx := build.Context{ContextImpl: &build.ContextImpl{ 188 Context: ctx, 189 Logger: log, 190 Tracer: trace, 191 Writer: writer, 192 Status: stat, 193 }} 194 195 config := build.NewConfig(buildCtx) 196 if *outDir == "" { 197 name := "multiproduct" 198 if !*incremental { 199 name += "-" + time.Now().Format("20060102150405") 200 } 201 202 *outDir = filepath.Join(config.OutDir(), name) 203 204 // Ensure the empty files exist in the output directory 205 // containing our output directory too. This is mostly for 206 // safety, but also triggers the ninja_build file so that our 207 // build servers know that they can parse the output as if it 208 // was ninja output. 209 build.SetupOutDir(buildCtx, config) 210 211 if err := os.MkdirAll(*outDir, 0777); err != nil { 212 log.Fatalf("Failed to create tempdir: %v", err) 213 } 214 } 215 config.Environment().Set("OUT_DIR", *outDir) 216 log.Println("Output directory:", *outDir) 217 218 logsDir := filepath.Join(config.OutDir(), "logs") 219 os.MkdirAll(logsDir, 0777) 220 221 build.SetupOutDir(buildCtx, config) 222 if *alternateResultDir { 223 distLogsDir := filepath.Join(config.DistDir(), "logs") 224 os.MkdirAll(distLogsDir, 0777) 225 log.SetOutput(filepath.Join(distLogsDir, "soong.log")) 226 trace.SetOutput(filepath.Join(distLogsDir, "build.trace")) 227 } else { 228 log.SetOutput(filepath.Join(config.OutDir(), "soong.log")) 229 trace.SetOutput(filepath.Join(config.OutDir(), "build.trace")) 230 } 231 232 setMaxFiles(log) 233 234 finder := build.NewSourceFinder(buildCtx, config) 235 defer finder.Shutdown() 236 237 build.FindSources(buildCtx, config, finder) 238 239 vars, err := build.DumpMakeVars(buildCtx, config, nil, []string{"all_named_products"}) 240 if err != nil { 241 log.Fatal(err) 242 } 243 var productsList []string 244 allProducts := strings.Fields(vars["all_named_products"]) 245 246 if *includeProducts != "" { 247 missingProducts := []string{} 248 for _, product := range strings.Split(*includeProducts, ",") { 249 if inList(product, allProducts) { 250 productsList = append(productsList, product) 251 } else { 252 missingProducts = append(missingProducts, product) 253 } 254 } 255 if len(missingProducts) > 0 { 256 log.Fatalf("Products don't exist: %s\n", missingProducts) 257 } 258 } else { 259 productsList = allProducts 260 } 261 262 finalProductsList := make([]string, 0, len(productsList)) 263 skipList := strings.Split(*skipProducts, ",") 264 skipProduct := func(p string) bool { 265 for _, s := range skipList { 266 if p == s { 267 return true 268 } 269 } 270 return false 271 } 272 for _, product := range productsList { 273 if !skipProduct(product) { 274 finalProductsList = append(finalProductsList, product) 275 } else { 276 log.Verbose("Skipping: ", product) 277 } 278 } 279 280 log.Verbose("Got product list: ", finalProductsList) 281 282 s := buildCtx.Status.StartTool() 283 s.SetTotalActions(len(finalProductsList)) 284 285 mpCtx := &mpContext{ 286 Context: ctx, 287 Logger: log, 288 Status: s, 289 Tracer: trace, 290 291 Finder: finder, 292 Config: config, 293 294 LogsDir: logsDir, 295 } 296 297 products := make(chan string, len(productsList)) 298 go func() { 299 defer close(products) 300 for _, product := range finalProductsList { 301 products <- product 302 } 303 }() 304 305 var wg sync.WaitGroup 306 for i := 0; i < *numJobs; i++ { 307 wg.Add(1) 308 go func() { 309 defer wg.Done() 310 for { 311 select { 312 case product := <-products: 313 if product == "" { 314 return 315 } 316 buildProduct(mpCtx, product) 317 } 318 } 319 }() 320 } 321 wg.Wait() 322 323 if *alternateResultDir { 324 args := zip.ZipArgs{ 325 FileArgs: []zip.FileArg{ 326 {GlobDir: logsDir, SourcePrefixToStrip: logsDir}, 327 }, 328 OutputFilePath: filepath.Join(config.DistDir(), "logs.zip"), 329 NumParallelJobs: runtime.NumCPU(), 330 CompressionLevel: 5, 331 } 332 if err := zip.Zip(args); err != nil { 333 log.Fatalf("Error zipping logs: %v", err) 334 } 335 } 336 337 s.Finish() 338 339 if failures == 1 { 340 log.Fatal("1 failure") 341 } else if failures > 1 { 342 log.Fatalf("%d failures", failures) 343 } else { 344 writer.Print("Success") 345 } 346} 347 348func buildProduct(mpctx *mpContext, product string) { 349 var stdLog string 350 351 outDir := filepath.Join(mpctx.Config.OutDir(), product) 352 logsDir := filepath.Join(mpctx.LogsDir, product) 353 354 if err := os.MkdirAll(outDir, 0777); err != nil { 355 mpctx.Logger.Fatalf("Error creating out directory: %v", err) 356 } 357 if err := os.MkdirAll(logsDir, 0777); err != nil { 358 mpctx.Logger.Fatalf("Error creating log directory: %v", err) 359 } 360 361 stdLog = filepath.Join(logsDir, "std.log") 362 f, err := os.Create(stdLog) 363 if err != nil { 364 mpctx.Logger.Fatalf("Error creating std.log: %v", err) 365 } 366 defer f.Close() 367 368 log := logger.New(f) 369 defer log.Cleanup() 370 log.SetOutput(filepath.Join(logsDir, "soong.log")) 371 372 action := &status.Action{ 373 Description: product, 374 Outputs: []string{product}, 375 } 376 mpctx.Status.StartAction(action) 377 defer logger.Recover(func(err error) { 378 mpctx.Status.FinishAction(status.ActionResult{ 379 Action: action, 380 Error: err, 381 Output: errMsgFromLog(stdLog), 382 }) 383 }) 384 385 ctx := build.Context{ContextImpl: &build.ContextImpl{ 386 Context: mpctx.Context, 387 Logger: log, 388 Tracer: mpctx.Tracer, 389 Writer: terminal.NewWriter(terminal.NewCustomStdio(nil, f, f)), 390 Thread: mpctx.Tracer.NewThread(product), 391 Status: &status.Status{}, 392 }} 393 ctx.Status.AddOutput(terminal.NewStatusOutput(ctx.Writer, "", 394 build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))) 395 396 config := build.NewConfig(ctx, flag.Args()...) 397 config.Environment().Set("OUT_DIR", outDir) 398 if !*keepArtifacts { 399 config.Environment().Set("EMPTY_NINJA_FILE", "true") 400 } 401 build.FindSources(ctx, config, mpctx.Finder) 402 config.Lunch(ctx, product, *buildVariant) 403 404 defer func() { 405 if *keepArtifacts { 406 args := zip.ZipArgs{ 407 FileArgs: []zip.FileArg{ 408 { 409 GlobDir: outDir, 410 SourcePrefixToStrip: outDir, 411 }, 412 }, 413 OutputFilePath: filepath.Join(mpctx.Config.OutDir(), product+".zip"), 414 NumParallelJobs: runtime.NumCPU(), 415 CompressionLevel: 5, 416 } 417 if err := zip.Zip(args); err != nil { 418 log.Fatalf("Error zipping artifacts: %v", err) 419 } 420 } 421 if !*incremental { 422 os.RemoveAll(outDir) 423 } 424 }() 425 426 buildWhat := build.BuildProductConfig 427 if !*onlyConfig { 428 buildWhat |= build.BuildSoong 429 if !*onlySoong { 430 buildWhat |= build.BuildKati 431 } 432 } 433 434 before := time.Now() 435 build.Build(ctx, config, buildWhat) 436 437 // Save std_full.log if Kati re-read the makefiles 438 if buildWhat&build.BuildKati != 0 { 439 if after, err := os.Stat(config.KatiBuildNinjaFile()); err == nil && after.ModTime().After(before) { 440 err := copyFile(stdLog, filepath.Join(filepath.Dir(stdLog), "std_full.log")) 441 if err != nil { 442 log.Fatalf("Error copying log file: %s", err) 443 } 444 } 445 } 446 447 mpctx.Status.FinishAction(status.ActionResult{ 448 Action: action, 449 }) 450} 451 452type failureCount int 453 454func (f *failureCount) StartAction(action *status.Action, counts status.Counts) {} 455 456func (f *failureCount) FinishAction(result status.ActionResult, counts status.Counts) { 457 if result.Error != nil { 458 *f += 1 459 } 460} 461 462func (f *failureCount) Message(level status.MsgLevel, message string) { 463 if level >= status.ErrorLvl { 464 *f += 1 465 } 466} 467 468func (f *failureCount) Flush() {} 469