Home | History | Annotate | Download | only in multiproduct_kati
      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