• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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