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