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 15 package main 16 17 import ( 18 "bytes" 19 "context" 20 "flag" 21 "fmt" 22 "io/ioutil" 23 "os" 24 "path/filepath" 25 "runtime" 26 "strings" 27 "sync" 28 "time" 29 30 "android/soong/ui/build" 31 "android/soong/ui/logger" 32 "android/soong/ui/tracer" 33 ) 34 35 // We default to number of cpus / 4, which seems to be the sweet spot for my 36 // system. I suspect this is mostly due to memory or disk bandwidth though, and 37 // may depend on the size ofthe source tree, so this probably isn't a great 38 // default. 39 func detectNumJobs() int { 40 if runtime.NumCPU() < 4 { 41 return 1 42 } 43 return runtime.NumCPU() / 4 44 } 45 46 var numJobs = flag.Int("j", detectNumJobs(), "number of parallel kati jobs") 47 48 var keep = flag.Bool("keep", false, "keep successful output files") 49 50 var outDir = flag.String("out", "", "path to store output directories (defaults to tmpdir under $OUT when empty)") 51 var alternateResultDir = flag.Bool("dist", false, "write select results to $DIST_DIR (or <out>/dist when empty)") 52 53 var onlyConfig = flag.Bool("only-config", false, "Only run product config (not Soong or Kati)") 54 var onlySoong = flag.Bool("only-soong", false, "Only run product config and Soong (not Kati)") 55 56 var buildVariant = flag.String("variant", "eng", "build variant to use") 57 58 const errorLeadingLines = 20 59 const errorTrailingLines = 20 60 61 type Product struct { 62 ctx build.Context 63 config build.Config 64 logFile string 65 } 66 67 type Status struct { 68 cur int 69 total int 70 failed int 71 72 ctx build.Context 73 haveBlankLine bool 74 smartTerminal bool 75 76 lock sync.Mutex 77 } 78 79 func NewStatus(ctx build.Context) *Status { 80 return &Status{ 81 ctx: ctx, 82 haveBlankLine: true, 83 smartTerminal: ctx.IsTerminal(), 84 } 85 } 86 87 func (s *Status) SetTotal(total int) { 88 s.total = total 89 } 90 91 func (s *Status) Fail(product string, err error, logFile string) { 92 s.Finish(product) 93 94 s.lock.Lock() 95 defer s.lock.Unlock() 96 97 if s.smartTerminal && !s.haveBlankLine { 98 fmt.Fprintln(s.ctx.Stdout()) 99 s.haveBlankLine = true 100 } 101 102 s.failed++ 103 fmt.Fprintln(s.ctx.Stderr(), "FAILED:", product) 104 s.ctx.Verboseln("FAILED:", product) 105 106 if logFile != "" { 107 data, err := ioutil.ReadFile(logFile) 108 if err == nil { 109 lines := strings.Split(strings.TrimSpace(string(data)), "\n") 110 if len(lines) > errorLeadingLines+errorTrailingLines+1 { 111 lines[errorLeadingLines] = fmt.Sprintf("... skipping %d lines ...", 112 len(lines)-errorLeadingLines-errorTrailingLines) 113 114 lines = append(lines[:errorLeadingLines+1], 115 lines[len(lines)-errorTrailingLines:]...) 116 } 117 for _, line := range lines { 118 fmt.Fprintln(s.ctx.Stderr(), "> ", line) 119 s.ctx.Verboseln(line) 120 } 121 } 122 } 123 124 s.ctx.Print(err) 125 } 126 127 func (s *Status) Finish(product string) { 128 s.lock.Lock() 129 defer s.lock.Unlock() 130 131 s.cur++ 132 line := fmt.Sprintf("[%d/%d] %s", s.cur, s.total, product) 133 134 if s.smartTerminal { 135 if max, ok := s.ctx.TermWidth(); ok { 136 if len(line) > max { 137 line = line[:max] 138 } 139 } 140 141 fmt.Fprint(s.ctx.Stdout(), "\r", line, "\x1b[K") 142 s.haveBlankLine = false 143 } else { 144 s.ctx.Println(line) 145 } 146 } 147 148 func (s *Status) Finished() int { 149 s.lock.Lock() 150 defer s.lock.Unlock() 151 152 if !s.haveBlankLine { 153 fmt.Fprintln(s.ctx.Stdout()) 154 s.haveBlankLine = true 155 } 156 return s.failed 157 } 158 159 func main() { 160 log := logger.New(os.Stderr) 161 defer log.Cleanup() 162 163 flag.Parse() 164 165 ctx, cancel := context.WithCancel(context.Background()) 166 defer cancel() 167 168 trace := tracer.New(log) 169 defer trace.Close() 170 171 build.SetupSignals(log, cancel, func() { 172 trace.Close() 173 log.Cleanup() 174 }) 175 176 buildCtx := build.Context{&build.ContextImpl{ 177 Context: ctx, 178 Logger: log, 179 Tracer: trace, 180 StdioInterface: build.StdioImpl{}, 181 }} 182 183 status := NewStatus(buildCtx) 184 185 config := build.NewConfig(buildCtx) 186 if *outDir == "" { 187 name := "multiproduct-" + time.Now().Format("20060102150405") 188 189 *outDir = filepath.Join(config.OutDir(), name) 190 191 // Ensure the empty files exist in the output directory 192 // containing our output directory too. This is mostly for 193 // safety, but also triggers the ninja_build file so that our 194 // build servers know that they can parse the output as if it 195 // was ninja output. 196 build.SetupOutDir(buildCtx, config) 197 198 if err := os.MkdirAll(*outDir, 0777); err != nil { 199 log.Fatalf("Failed to create tempdir: %v", err) 200 } 201 202 if !*keep { 203 defer func() { 204 if status.Finished() == 0 { 205 os.RemoveAll(*outDir) 206 } 207 }() 208 } 209 } 210 config.Environment().Set("OUT_DIR", *outDir) 211 log.Println("Output directory:", *outDir) 212 213 build.SetupOutDir(buildCtx, config) 214 if *alternateResultDir { 215 logsDir := filepath.Join(config.DistDir(), "logs") 216 os.MkdirAll(logsDir, 0777) 217 log.SetOutput(filepath.Join(logsDir, "soong.log")) 218 trace.SetOutput(filepath.Join(logsDir, "build.trace")) 219 } else { 220 log.SetOutput(filepath.Join(config.OutDir(), "soong.log")) 221 trace.SetOutput(filepath.Join(config.OutDir(), "build.trace")) 222 } 223 224 vars, err := build.DumpMakeVars(buildCtx, config, nil, nil, []string{"all_named_products"}) 225 if err != nil { 226 log.Fatal(err) 227 } 228 products := strings.Fields(vars["all_named_products"]) 229 log.Verbose("Got product list:", products) 230 231 status.SetTotal(len(products)) 232 233 var wg sync.WaitGroup 234 productConfigs := make(chan Product, len(products)) 235 236 // Run the product config for every product in parallel 237 for _, product := range products { 238 wg.Add(1) 239 go func(product string) { 240 var stdLog string 241 242 defer wg.Done() 243 defer logger.Recover(func(err error) { 244 status.Fail(product, err, stdLog) 245 }) 246 247 productOutDir := filepath.Join(config.OutDir(), product) 248 productLogDir := productOutDir 249 if *alternateResultDir { 250 productLogDir = filepath.Join(config.DistDir(), product) 251 if err := os.MkdirAll(productLogDir, 0777); err != nil { 252 log.Fatalf("Error creating log directory: %v", err) 253 } 254 } 255 256 if err := os.MkdirAll(productOutDir, 0777); err != nil { 257 log.Fatalf("Error creating out directory: %v", err) 258 } 259 260 stdLog = filepath.Join(productLogDir, "std.log") 261 f, err := os.Create(stdLog) 262 if err != nil { 263 log.Fatalf("Error creating std.log: %v", err) 264 } 265 266 productLog := logger.New(&bytes.Buffer{}) 267 productLog.SetOutput(filepath.Join(productLogDir, "soong.log")) 268 269 productCtx := build.Context{&build.ContextImpl{ 270 Context: ctx, 271 Logger: productLog, 272 Tracer: trace, 273 StdioInterface: build.NewCustomStdio(nil, f, f), 274 Thread: trace.NewThread(product), 275 }} 276 277 productConfig := build.NewConfig(productCtx) 278 productConfig.Environment().Set("OUT_DIR", productOutDir) 279 productConfig.Lunch(productCtx, product, *buildVariant) 280 281 build.Build(productCtx, productConfig, build.BuildProductConfig) 282 productConfigs <- Product{productCtx, productConfig, stdLog} 283 }(product) 284 } 285 go func() { 286 defer close(productConfigs) 287 wg.Wait() 288 }() 289 290 var wg2 sync.WaitGroup 291 // Then run up to numJobs worth of Soong and Kati 292 for i := 0; i < *numJobs; i++ { 293 wg2.Add(1) 294 go func() { 295 defer wg2.Done() 296 for product := range productConfigs { 297 func() { 298 defer logger.Recover(func(err error) { 299 status.Fail(product.config.TargetProduct(), err, product.logFile) 300 }) 301 302 buildWhat := 0 303 if !*onlyConfig { 304 buildWhat |= build.BuildSoong 305 if !*onlySoong { 306 buildWhat |= build.BuildKati 307 } 308 } 309 build.Build(product.ctx, product.config, buildWhat) 310 if !*keep { 311 os.RemoveAll(product.config.OutDir()) 312 } 313 status.Finish(product.config.TargetProduct()) 314 }() 315 } 316 }() 317 } 318 wg2.Wait() 319 320 if count := status.Finished(); count > 0 { 321 log.Fatalln(count, "products failed") 322 } 323 } 324