• 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	"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