• 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	"os"
22	"path/filepath"
23	"strconv"
24	"strings"
25	"syscall"
26	"time"
27
28	"android/soong/shared"
29	"android/soong/ui/build"
30	"android/soong/ui/logger"
31	"android/soong/ui/metrics"
32	"android/soong/ui/signal"
33	"android/soong/ui/status"
34	"android/soong/ui/terminal"
35	"android/soong/ui/tracer"
36)
37
38// A command represents an operation to be executed in the soong build
39// system.
40type command struct {
41	// The flag name (must have double dashes).
42	flag string
43
44	// Description for the flag (to display when running help).
45	description string
46
47	// Stream the build status output into the simple terminal mode.
48	simpleOutput bool
49
50	// Sets a prefix string to use for filenames of log files.
51	logsPrefix string
52
53	// Creates the build configuration based on the args and build context.
54	config func(ctx build.Context, args ...string) build.Config
55
56	// Returns what type of IO redirection this Command requires.
57	stdio func() terminal.StdioInterface
58
59	// run the command
60	run func(ctx build.Context, config build.Config, args []string)
61}
62
63// list of supported commands (flags) supported by soong ui
64var commands = []command{
65	{
66		flag:        "--make-mode",
67		description: "build the modules by the target name (i.e. soong_docs)",
68		config:      build.NewConfig,
69		stdio:       stdio,
70		run:         runMake,
71	}, {
72		flag:         "--dumpvar-mode",
73		description:  "print the value of the legacy make variable VAR to stdout",
74		simpleOutput: true,
75		logsPrefix:   "dumpvars-",
76		config:       dumpVarConfig,
77		stdio:        customStdio,
78		run:          dumpVar,
79	}, {
80		flag:         "--dumpvars-mode",
81		description:  "dump the values of one or more legacy make variables, in shell syntax",
82		simpleOutput: true,
83		logsPrefix:   "dumpvars-",
84		config:       dumpVarConfig,
85		stdio:        customStdio,
86		run:          dumpVars,
87	}, {
88		flag:        "--build-mode",
89		description: "build modules based on the specified build action",
90		config:      buildActionConfig,
91		stdio:       stdio,
92		run:         runMake,
93	},
94}
95
96// indexList returns the index of first found s. -1 is return if s is not
97// found.
98func indexList(s string, list []string) int {
99	for i, l := range list {
100		if l == s {
101			return i
102		}
103	}
104	return -1
105}
106
107// inList returns true if one or more of s is in the list.
108func inList(s string, list []string) bool {
109	return indexList(s, list) != -1
110}
111
112func deleteStaleMetrics(metricsFilePathSlice []string) error {
113	for _, metricsFilePath := range metricsFilePathSlice {
114		if err := os.Remove(metricsFilePath); err != nil && !os.IsNotExist(err) {
115			return fmt.Errorf("Failed to remove %s\nError message: %w", metricsFilePath, err)
116		}
117	}
118	return nil
119}
120
121// Main execution of soong_ui. The command format is as follows:
122//
123//	soong_ui <command> [<arg 1> <arg 2> ... <arg n>]
124//
125// Command is the type of soong_ui execution. Only one type of
126// execution is specified. The args are specific to the command.
127func main() {
128	shared.ReexecWithDelveMaybe(os.Getenv("SOONG_UI_DELVE"), shared.ResolveDelveBinary())
129
130	buildStarted := time.Now()
131
132	c, args, err := getCommand(os.Args)
133	if err != nil {
134		fmt.Fprintf(os.Stderr, "Error parsing `soong` args: %s.\n", err)
135		os.Exit(1)
136	}
137
138	// Create a terminal output that mimics Ninja's.
139	output := terminal.NewStatusOutput(c.stdio().Stdout(), os.Getenv("NINJA_STATUS"), c.simpleOutput,
140		build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"),
141		build.OsEnvironment().IsEnvTrue("SOONG_UI_ANSI_OUTPUT"))
142
143	// Create and start a new metric record.
144	met := metrics.New()
145	met.SetBuildDateTime(buildStarted)
146	met.SetBuildCommand(os.Args)
147
148	// Attach a new logger instance to the terminal output.
149	log := logger.NewWithMetrics(output, met)
150	defer log.Cleanup()
151
152	// Create a context to simplify the program termination process.
153	ctx, cancel := context.WithCancel(context.Background())
154	defer cancel()
155
156	// Create a new trace file writer, making it log events to the log instance.
157	trace := tracer.New(log)
158
159	// Create a new Status instance, which manages action counts and event output channels.
160	stat := &status.Status{}
161
162	// Hook up the terminal output and tracer to Status.
163	stat.AddOutput(output)
164	stat.AddOutput(trace.StatusTracer())
165
166	// Set up a cleanup procedure in case the normal termination process doesn't work.
167	signal.SetupSignals(log, cancel, func() {
168		trace.Close()
169		log.Cleanup()
170		stat.Finish()
171	})
172	criticalPath := status.NewCriticalPath()
173	buildCtx := build.Context{ContextImpl: &build.ContextImpl{
174		Context:      ctx,
175		Logger:       log,
176		Metrics:      met,
177		Tracer:       trace,
178		Writer:       output,
179		Status:       stat,
180		CriticalPath: criticalPath,
181	}}
182
183	freshConfig := func() build.Config {
184		config := c.config(buildCtx, args...)
185		config.SetLogsPrefix(c.logsPrefix)
186		return config
187	}
188	config := freshConfig()
189	logsDir := config.LogsDir()
190	buildStarted = config.BuildStartedTimeOrDefault(buildStarted)
191
192	buildErrorFile := filepath.Join(logsDir, c.logsPrefix+"build_error")
193	soongMetricsFile := filepath.Join(logsDir, c.logsPrefix+"soong_metrics")
194	rbeMetricsFile := filepath.Join(logsDir, c.logsPrefix+"rbe_metrics.pb")
195	soongBuildMetricsFile := filepath.Join(logsDir, c.logsPrefix+"soong_build_metrics.pb")
196	buildTraceFile := filepath.Join(logsDir, c.logsPrefix+"build.trace.gz")
197
198	metricsFiles := []string{
199		buildErrorFile,        // build error strings
200		rbeMetricsFile,        // high level metrics related to remote build execution.
201		soongMetricsFile,      // high level metrics related to this build system.
202		soongBuildMetricsFile, // high level metrics related to soong build
203		buildTraceFile,
204	}
205
206	defer func() {
207		stat.Finish()
208		criticalPath.WriteToMetrics(met)
209		met.Dump(soongMetricsFile)
210		if !config.SkipMetricsUpload() {
211			build.UploadMetrics(buildCtx, config, c.simpleOutput, buildStarted, metricsFiles...)
212		}
213	}()
214
215	// This has to come after the metrics uploading function, so that
216	// build.trace.gz is closed and ready for upload.
217	defer trace.Close()
218
219	os.MkdirAll(logsDir, 0777)
220
221	log.SetOutput(filepath.Join(logsDir, c.logsPrefix+"soong.log"))
222
223	trace.SetOutput(filepath.Join(logsDir, c.logsPrefix+"build.trace"))
224
225	log.Verbose("Command Line: ")
226	for i, arg := range os.Args {
227		log.Verbosef("  [%d] %s", i, arg)
228	}
229
230	// We need to call preProductConfigSetup before we can do product config, which is how we get
231	// PRODUCT_CONFIG_RELEASE_MAPS set for the final product config for the build.
232	// When product config uses a declarative language, we won't need to rerun product config.
233	preProductConfigSetup(buildCtx, config)
234	if build.SetProductReleaseConfigMaps(buildCtx, config) {
235		log.Verbose("Product release config maps found\n")
236		config = freshConfig()
237	}
238
239	c.run(buildCtx, config, args)
240}
241
242// This function must not modify config, since product config may cause us to recreate the config,
243// and we won't call this function a second time.
244func preProductConfigSetup(buildCtx build.Context, config build.Config) {
245	log := buildCtx.ContextImpl.Logger
246	logsPrefix := config.GetLogsPrefix()
247	build.SetupOutDir(buildCtx, config)
248	logsDir := config.LogsDir()
249
250	// Common list of metric file definition.
251	buildErrorFile := filepath.Join(logsDir, logsPrefix+"build_error")
252	rbeMetricsFile := filepath.Join(logsDir, logsPrefix+"rbe_metrics.pb")
253	soongMetricsFile := filepath.Join(logsDir, logsPrefix+"soong_metrics")
254	bp2buildMetricsFile := filepath.Join(logsDir, logsPrefix+"bp2build_metrics.pb")
255	soongBuildMetricsFile := filepath.Join(logsDir, logsPrefix+"soong_build_metrics.pb")
256
257	//Delete the stale metrics files
258	staleFileSlice := []string{buildErrorFile, rbeMetricsFile, soongMetricsFile, bp2buildMetricsFile, soongBuildMetricsFile}
259	if err := deleteStaleMetrics(staleFileSlice); err != nil {
260		log.Fatalln(err)
261	}
262
263	build.PrintOutDirWarning(buildCtx, config)
264
265	stat := buildCtx.Status
266	stat.AddOutput(status.NewVerboseLog(log, filepath.Join(logsDir, logsPrefix+"verbose.log")))
267	stat.AddOutput(status.NewErrorLog(log, filepath.Join(logsDir, logsPrefix+"error.log")))
268	stat.AddOutput(status.NewProtoErrorLog(log, buildErrorFile))
269	stat.AddOutput(status.NewCriticalPathLogger(log, buildCtx.CriticalPath))
270	stat.AddOutput(status.NewBuildProgressLog(log, filepath.Join(logsDir, logsPrefix+"build_progress.pb")))
271
272	buildCtx.Verbosef("Detected %.3v GB total RAM", float32(config.TotalRAM())/(1024*1024*1024))
273	buildCtx.Verbosef("Parallelism (local/remote/highmem): %v/%v/%v",
274		config.Parallel(), config.RemoteParallel(), config.HighmemParallel())
275
276	setMaxFiles(buildCtx)
277
278	defer build.CheckProdCreds(buildCtx, config)
279
280	// Read the time at the starting point.
281	if start, ok := os.LookupEnv("TRACE_BEGIN_SOONG"); ok {
282		// soong_ui.bash uses the date command's %N (nanosec) flag when getting the start time,
283		// which Darwin doesn't support. Check if it was executed properly before parsing the value.
284		if !strings.HasSuffix(start, "N") {
285			if start_time, err := strconv.ParseUint(start, 10, 64); err == nil {
286				log.Verbosef("Took %dms to start up.",
287					time.Since(time.Unix(0, int64(start_time))).Nanoseconds()/time.Millisecond.Nanoseconds())
288				buildCtx.CompleteTrace(metrics.RunSetupTool, "startup", start_time, uint64(time.Now().UnixNano()))
289			}
290		}
291
292		if executable, err := os.Executable(); err == nil {
293			buildCtx.ContextImpl.Tracer.ImportMicrofactoryLog(filepath.Join(filepath.Dir(executable), "."+filepath.Base(executable)+".trace"))
294		}
295	}
296
297	// Create a source finder.
298	f := build.NewSourceFinder(buildCtx, config)
299	defer f.Shutdown()
300	build.FindSources(buildCtx, config, f)
301}
302
303func dumpVar(ctx build.Context, config build.Config, args []string) {
304	flags := flag.NewFlagSet("dumpvar", flag.ExitOnError)
305	flags.SetOutput(ctx.Writer)
306
307	flags.Usage = func() {
308		fmt.Fprintf(ctx.Writer, "usage: %s --dumpvar-mode [--abs] <VAR>\n\n", os.Args[0])
309		fmt.Fprintln(ctx.Writer, "In dumpvar mode, print the value of the legacy make variable VAR to stdout")
310		fmt.Fprintln(ctx.Writer, "")
311
312		fmt.Fprintln(ctx.Writer, "'report_config' is a special case that prints the human-readable config banner")
313		fmt.Fprintln(ctx.Writer, "from the beginning of the build.")
314		fmt.Fprintln(ctx.Writer, "")
315		flags.PrintDefaults()
316	}
317	abs := flags.Bool("abs", false, "Print the absolute path of the value")
318	flags.Parse(args)
319
320	if flags.NArg() != 1 {
321		flags.Usage()
322		ctx.Fatalf("Invalid usage")
323	}
324
325	varName := flags.Arg(0)
326	if varName == "report_config" {
327		varData, err := build.DumpMakeVars(ctx, config, nil, build.BannerVars)
328		if err != nil {
329			ctx.Fatal(err)
330		}
331
332		fmt.Println(build.Banner(varData))
333	} else {
334		varData, err := build.DumpMakeVars(ctx, config, nil, []string{varName})
335		if err != nil {
336			ctx.Fatal(err)
337		}
338
339		if *abs {
340			var res []string
341			for _, path := range strings.Fields(varData[varName]) {
342				if abs, err := filepath.Abs(path); err == nil {
343					res = append(res, abs)
344				} else {
345					ctx.Fatalln("Failed to get absolute path of", path, err)
346				}
347			}
348			fmt.Println(strings.Join(res, " "))
349		} else {
350			fmt.Println(varData[varName])
351		}
352	}
353}
354
355func dumpVars(ctx build.Context, config build.Config, args []string) {
356
357	flags := flag.NewFlagSet("dumpvars", flag.ExitOnError)
358	flags.SetOutput(ctx.Writer)
359
360	flags.Usage = func() {
361		fmt.Fprintf(ctx.Writer, "usage: %s --dumpvars-mode [--vars=\"VAR VAR ...\"]\n\n", os.Args[0])
362		fmt.Fprintln(ctx.Writer, "In dumpvars mode, dump the values of one or more legacy make variables, in")
363		fmt.Fprintln(ctx.Writer, "shell syntax. The resulting output may be sourced directly into a shell to")
364		fmt.Fprintln(ctx.Writer, "set corresponding shell variables.")
365		fmt.Fprintln(ctx.Writer, "")
366
367		fmt.Fprintln(ctx.Writer, "'report_config' is a special case that dumps a variable containing the")
368		fmt.Fprintln(ctx.Writer, "human-readable config banner from the beginning of the build.")
369		fmt.Fprintln(ctx.Writer, "")
370		flags.PrintDefaults()
371	}
372
373	varsStr := flags.String("vars", "", "Space-separated list of variables to dump")
374	absVarsStr := flags.String("abs-vars", "", "Space-separated list of variables to dump (using absolute paths)")
375
376	varPrefix := flags.String("var-prefix", "", "String to prepend to all variable names when dumping")
377	absVarPrefix := flags.String("abs-var-prefix", "", "String to prepent to all absolute path variable names when dumping")
378
379	flags.Parse(args)
380
381	if flags.NArg() != 0 {
382		flags.Usage()
383		ctx.Fatalf("Invalid usage")
384	}
385
386	vars := strings.Fields(*varsStr)
387	absVars := strings.Fields(*absVarsStr)
388
389	allVars := append([]string{}, vars...)
390	allVars = append(allVars, absVars...)
391
392	if i := indexList("report_config", allVars); i != -1 {
393		allVars = append(allVars[:i], allVars[i+1:]...)
394		allVars = append(allVars, build.BannerVars...)
395	}
396
397	if len(allVars) == 0 {
398		return
399	}
400
401	varData, err := build.DumpMakeVars(ctx, config, nil, allVars)
402	if err != nil {
403		ctx.Fatal(err)
404	}
405
406	for _, name := range vars {
407		if name == "report_config" {
408			fmt.Printf("%sreport_config='%s'\n", *varPrefix, build.Banner(varData))
409		} else {
410			fmt.Printf("%s%s='%s'\n", *varPrefix, name, varData[name])
411		}
412	}
413	for _, name := range absVars {
414		var res []string
415		for _, path := range strings.Fields(varData[name]) {
416			abs, err := filepath.Abs(path)
417			if err != nil {
418				ctx.Fatalln("Failed to get absolute path of", path, err)
419			}
420			res = append(res, abs)
421		}
422		fmt.Printf("%s%s='%s'\n", *absVarPrefix, name, strings.Join(res, " "))
423	}
424}
425
426func stdio() terminal.StdioInterface {
427	return terminal.StdioImpl{}
428}
429
430// dumpvar and dumpvars use stdout to output variable values, so use stderr instead of stdout when
431// reporting events to keep stdout clean from noise.
432func customStdio() terminal.StdioInterface {
433	return terminal.NewCustomStdio(os.Stdin, os.Stderr, os.Stderr)
434}
435
436// dumpVarConfig does not require any arguments to be parsed by the NewConfig.
437func dumpVarConfig(ctx build.Context, args ...string) build.Config {
438	return build.NewConfig(ctx)
439}
440
441func buildActionConfig(ctx build.Context, args ...string) build.Config {
442	flags := flag.NewFlagSet("build-mode", flag.ContinueOnError)
443	flags.SetOutput(ctx.Writer)
444
445	flags.Usage = func() {
446		fmt.Fprintf(ctx.Writer, "usage: %s --build-mode --dir=<path> <build action> [<build arg 1> <build arg 2> ...]\n\n", os.Args[0])
447		fmt.Fprintln(ctx.Writer, "In build mode, build the set of modules based on the specified build")
448		fmt.Fprintln(ctx.Writer, "action. The --dir flag is required to determine what is needed to")
449		fmt.Fprintln(ctx.Writer, "build in the source tree based on the build action. See below for")
450		fmt.Fprintln(ctx.Writer, "the list of acceptable build action flags.")
451		fmt.Fprintln(ctx.Writer, "")
452		flags.PrintDefaults()
453	}
454
455	buildActionFlags := []struct {
456		name        string
457		description string
458		action      build.BuildAction
459		set         bool
460	}{{
461		name:        "all-modules",
462		description: "Build action: build from the top of the source tree.",
463		action:      build.BUILD_MODULES,
464	}, {
465		// This is redirecting to mma build command behaviour. Once it has soaked for a
466		// while, the build command is deleted from here once it has been removed from the
467		// envsetup.sh.
468		name:        "modules-in-a-dir-no-deps",
469		description: "Build action: builds all of the modules in the current directory without their dependencies.",
470		action:      build.BUILD_MODULES_IN_A_DIRECTORY,
471	}, {
472		// This is redirecting to mmma build command behaviour. Once it has soaked for a
473		// while, the build command is deleted from here once it has been removed from the
474		// envsetup.sh.
475		name:        "modules-in-dirs-no-deps",
476		description: "Build action: builds all of the modules in the supplied directories without their dependencies.",
477		action:      build.BUILD_MODULES_IN_DIRECTORIES,
478	}, {
479		name:        "modules-in-a-dir",
480		description: "Build action: builds all of the modules in the current directory and their dependencies.",
481		action:      build.BUILD_MODULES_IN_A_DIRECTORY,
482	}, {
483		name:        "modules-in-dirs",
484		description: "Build action: builds all of the modules in the supplied directories and their dependencies.",
485		action:      build.BUILD_MODULES_IN_DIRECTORIES,
486	}}
487	for i, flag := range buildActionFlags {
488		flags.BoolVar(&buildActionFlags[i].set, flag.name, false, flag.description)
489	}
490	dir := flags.String("dir", "", "Directory of the executed build command.")
491
492	// Only interested in the first two args which defines the build action and the directory.
493	// The remaining arguments are passed down to the config.
494	const numBuildActionFlags = 2
495	if len(args) < numBuildActionFlags {
496		flags.Usage()
497		ctx.Fatalln("Improper build action arguments: too few arguments")
498	}
499	parseError := flags.Parse(args[0:numBuildActionFlags])
500
501	// The next block of code is to validate that exactly one build action is set and the dir flag
502	// is specified.
503	buildActionFound := false
504	var buildAction build.BuildAction
505	for _, f := range buildActionFlags {
506		if f.set {
507			if buildActionFound {
508				if parseError == nil {
509					//otherwise Parse() already called Usage()
510					flags.Usage()
511				}
512				ctx.Fatalf("Build action already specified, omit: --%s\n", f.name)
513			}
514			buildActionFound = true
515			buildAction = f.action
516		}
517	}
518	if !buildActionFound {
519		if parseError == nil {
520			//otherwise Parse() already called Usage()
521			flags.Usage()
522		}
523		ctx.Fatalln("Build action not defined.")
524	}
525	if *dir == "" {
526		ctx.Fatalln("-dir not specified.")
527	}
528
529	// Remove the build action flags from the args as they are not recognized by the config.
530	args = args[numBuildActionFlags:]
531	return build.NewBuildActionConfig(buildAction, *dir, ctx, args...)
532}
533
534func runMake(ctx build.Context, config build.Config, _ []string) {
535	logsDir := config.LogsDir()
536	if config.IsVerbose() {
537		writer := ctx.Writer
538		fmt.Fprintln(writer, "! The argument `showcommands` is no longer supported.")
539		fmt.Fprintln(writer, "! Instead, the verbose log is always written to a compressed file in the output dir:")
540		fmt.Fprintln(writer, "!")
541		fmt.Fprintf(writer, "!   gzip -cd %s/verbose.log.gz | less -R\n", logsDir)
542		fmt.Fprintln(writer, "!")
543		fmt.Fprintln(writer, "! Older versions are saved in verbose.log.#.gz files")
544		fmt.Fprintln(writer, "")
545		ctx.Fatal("Invalid argument")
546	}
547
548	if _, ok := config.Environment().Get("ONE_SHOT_MAKEFILE"); ok {
549		writer := ctx.Writer
550		fmt.Fprintln(writer, "! The variable `ONE_SHOT_MAKEFILE` is obsolete.")
551		fmt.Fprintln(writer, "!")
552		fmt.Fprintln(writer, "! If you're using `mm`, you'll need to run `source build/envsetup.sh` to update.")
553		fmt.Fprintln(writer, "!")
554		fmt.Fprintln(writer, "! Otherwise, either specify a module name with m, or use mma / MODULES-IN-...")
555		fmt.Fprintln(writer, "")
556		ctx.Fatal("Invalid environment")
557	}
558
559	build.Build(ctx, config)
560}
561
562// getCommand finds the appropriate command based on args[1] flag. args[0]
563// is the soong_ui filename.
564func getCommand(args []string) (*command, []string, error) {
565	listFlags := func() []string {
566		flags := make([]string, len(commands))
567		for i, c := range commands {
568			flags[i] = c.flag
569		}
570		return flags
571	}
572
573	if len(args) < 2 {
574		return nil, nil, fmt.Errorf("Too few arguments: %q\nUse one of these: %q", args, listFlags())
575	}
576
577	for _, c := range commands {
578		if c.flag == args[1] {
579			return &c, args[2:], nil
580		}
581	}
582	return nil, nil, fmt.Errorf("Command not found: %q\nDid you mean one of these: %q", args[1], listFlags())
583}
584
585func setMaxFiles(ctx build.Context) {
586	var limits syscall.Rlimit
587
588	err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limits)
589	if err != nil {
590		ctx.Println("Failed to get file limit:", err)
591		return
592	}
593
594	ctx.Verbosef("Current file limits: %d soft, %d hard", limits.Cur, limits.Max)
595
596	// Go 1.21 modifies the file limit but restores the original when
597	// execing subprocesses if it hasn't be overridden.  Call Setrlimit
598	// here even if it doesn't appear to be necessary so that the
599	// syscall package considers it set.
600
601	limits.Cur = limits.Max
602	err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limits)
603	if err != nil {
604		ctx.Println("Failed to increase file limit:", err)
605	}
606}
607