xref: /aosp_15_r20/build/soong/cmd/soong_ui/main.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
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	soongBuildMetricsFile := filepath.Join(logsDir, logsPrefix+"soong_build_metrics.pb")
255
256	//Delete the stale metrics files
257	staleFileSlice := []string{buildErrorFile, rbeMetricsFile, soongMetricsFile, soongBuildMetricsFile}
258	if err := deleteStaleMetrics(staleFileSlice); err != nil {
259		log.Fatalln(err)
260	}
261
262	build.PrintOutDirWarning(buildCtx, config)
263
264	stat := buildCtx.Status
265	stat.AddOutput(status.NewVerboseLog(log, filepath.Join(logsDir, logsPrefix+"verbose.log")))
266	stat.AddOutput(status.NewErrorLog(log, filepath.Join(logsDir, logsPrefix+"error.log")))
267	stat.AddOutput(status.NewProtoErrorLog(log, buildErrorFile))
268	stat.AddOutput(status.NewCriticalPathLogger(log, buildCtx.CriticalPath))
269	stat.AddOutput(status.NewBuildProgressLog(log, filepath.Join(logsDir, logsPrefix+"build_progress.pb")))
270
271	buildCtx.Verbosef("Detected %.3v GB total RAM", float32(config.TotalRAM())/(1024*1024*1024))
272	buildCtx.Verbosef("Parallelism (local/remote/highmem): %v/%v/%v",
273		config.Parallel(), config.RemoteParallel(), config.HighmemParallel())
274
275	setMaxFiles(buildCtx)
276
277	defer build.CheckProdCreds(buildCtx, config)
278
279	// Read the time at the starting point.
280	if start, ok := os.LookupEnv("TRACE_BEGIN_SOONG"); ok {
281		// soong_ui.bash uses the date command's %N (nanosec) flag when getting the start time,
282		// which Darwin doesn't support. Check if it was executed properly before parsing the value.
283		if !strings.HasSuffix(start, "N") {
284			if start_time, err := strconv.ParseUint(start, 10, 64); err == nil {
285				log.Verbosef("Took %dms to start up.",
286					time.Since(time.Unix(0, int64(start_time))).Nanoseconds()/time.Millisecond.Nanoseconds())
287				buildCtx.CompleteTrace(metrics.RunSetupTool, "startup", start_time, uint64(time.Now().UnixNano()))
288			}
289		}
290
291		if executable, err := os.Executable(); err == nil {
292			buildCtx.ContextImpl.Tracer.ImportMicrofactoryLog(filepath.Join(filepath.Dir(executable), "."+filepath.Base(executable)+".trace"))
293		}
294	}
295
296	// Create a source finder.
297	f := build.NewSourceFinder(buildCtx, config)
298	defer f.Shutdown()
299	build.FindSources(buildCtx, config, f)
300}
301
302func dumpVar(ctx build.Context, config build.Config, args []string) {
303	flags := flag.NewFlagSet("dumpvar", flag.ExitOnError)
304	flags.SetOutput(ctx.Writer)
305
306	flags.Usage = func() {
307		fmt.Fprintf(ctx.Writer, "usage: %s --dumpvar-mode [--abs] <VAR>\n\n", os.Args[0])
308		fmt.Fprintln(ctx.Writer, "In dumpvar mode, print the value of the legacy make variable VAR to stdout")
309		fmt.Fprintln(ctx.Writer, "")
310
311		fmt.Fprintln(ctx.Writer, "'report_config' is a special case that prints the human-readable config banner")
312		fmt.Fprintln(ctx.Writer, "from the beginning of the build.")
313		fmt.Fprintln(ctx.Writer, "")
314		flags.PrintDefaults()
315	}
316	abs := flags.Bool("abs", false, "Print the absolute path of the value")
317	flags.Parse(args)
318
319	if flags.NArg() != 1 {
320		flags.Usage()
321		ctx.Fatalf("Invalid usage")
322	}
323
324	varName := flags.Arg(0)
325	if varName == "report_config" {
326		varData, err := build.DumpMakeVars(ctx, config, nil, build.BannerVars)
327		if err != nil {
328			ctx.Fatal(err)
329		}
330
331		fmt.Println(build.Banner(varData))
332	} else {
333		varData, err := build.DumpMakeVars(ctx, config, nil, []string{varName})
334		if err != nil {
335			ctx.Fatal(err)
336		}
337
338		if *abs {
339			var res []string
340			for _, path := range strings.Fields(varData[varName]) {
341				if abs, err := filepath.Abs(path); err == nil {
342					res = append(res, abs)
343				} else {
344					ctx.Fatalln("Failed to get absolute path of", path, err)
345				}
346			}
347			fmt.Println(strings.Join(res, " "))
348		} else {
349			fmt.Println(varData[varName])
350		}
351	}
352}
353
354func dumpVars(ctx build.Context, config build.Config, args []string) {
355
356	flags := flag.NewFlagSet("dumpvars", flag.ExitOnError)
357	flags.SetOutput(ctx.Writer)
358
359	flags.Usage = func() {
360		fmt.Fprintf(ctx.Writer, "usage: %s --dumpvars-mode [--vars=\"VAR VAR ...\"]\n\n", os.Args[0])
361		fmt.Fprintln(ctx.Writer, "In dumpvars mode, dump the values of one or more legacy make variables, in")
362		fmt.Fprintln(ctx.Writer, "shell syntax. The resulting output may be sourced directly into a shell to")
363		fmt.Fprintln(ctx.Writer, "set corresponding shell variables.")
364		fmt.Fprintln(ctx.Writer, "")
365
366		fmt.Fprintln(ctx.Writer, "'report_config' is a special case that dumps a variable containing the")
367		fmt.Fprintln(ctx.Writer, "human-readable config banner from the beginning of the build.")
368		fmt.Fprintln(ctx.Writer, "")
369		flags.PrintDefaults()
370	}
371
372	varsStr := flags.String("vars", "", "Space-separated list of variables to dump")
373	absVarsStr := flags.String("abs-vars", "", "Space-separated list of variables to dump (using absolute paths)")
374
375	varPrefix := flags.String("var-prefix", "", "String to prepend to all variable names when dumping")
376	absVarPrefix := flags.String("abs-var-prefix", "", "String to prepent to all absolute path variable names when dumping")
377
378	flags.Parse(args)
379
380	if flags.NArg() != 0 {
381		flags.Usage()
382		ctx.Fatalf("Invalid usage")
383	}
384
385	vars := strings.Fields(*varsStr)
386	absVars := strings.Fields(*absVarsStr)
387
388	allVars := append([]string{}, vars...)
389	allVars = append(allVars, absVars...)
390
391	if i := indexList("report_config", allVars); i != -1 {
392		allVars = append(allVars[:i], allVars[i+1:]...)
393		allVars = append(allVars, build.BannerVars...)
394	}
395
396	if len(allVars) == 0 {
397		return
398	}
399
400	varData, err := build.DumpMakeVars(ctx, config, nil, allVars)
401	if err != nil {
402		ctx.Fatal(err)
403	}
404
405	for _, name := range vars {
406		if name == "report_config" {
407			fmt.Printf("%sreport_config='%s'\n", *varPrefix, build.Banner(varData))
408		} else {
409			fmt.Printf("%s%s='%s'\n", *varPrefix, name, varData[name])
410		}
411	}
412	for _, name := range absVars {
413		var res []string
414		for _, path := range strings.Fields(varData[name]) {
415			abs, err := filepath.Abs(path)
416			if err != nil {
417				ctx.Fatalln("Failed to get absolute path of", path, err)
418			}
419			res = append(res, abs)
420		}
421		fmt.Printf("%s%s='%s'\n", *absVarPrefix, name, strings.Join(res, " "))
422	}
423}
424
425func stdio() terminal.StdioInterface {
426	return terminal.StdioImpl{}
427}
428
429// dumpvar and dumpvars use stdout to output variable values, so use stderr instead of stdout when
430// reporting events to keep stdout clean from noise.
431func customStdio() terminal.StdioInterface {
432	return terminal.NewCustomStdio(os.Stdin, os.Stderr, os.Stderr)
433}
434
435// dumpVarConfig does not require any arguments to be parsed by the NewConfig.
436func dumpVarConfig(ctx build.Context, args ...string) build.Config {
437	return build.NewConfig(ctx)
438}
439
440func buildActionConfig(ctx build.Context, args ...string) build.Config {
441	flags := flag.NewFlagSet("build-mode", flag.ContinueOnError)
442	flags.SetOutput(ctx.Writer)
443
444	flags.Usage = func() {
445		fmt.Fprintf(ctx.Writer, "usage: %s --build-mode --dir=<path> <build action> [<build arg 1> <build arg 2> ...]\n\n", os.Args[0])
446		fmt.Fprintln(ctx.Writer, "In build mode, build the set of modules based on the specified build")
447		fmt.Fprintln(ctx.Writer, "action. The --dir flag is required to determine what is needed to")
448		fmt.Fprintln(ctx.Writer, "build in the source tree based on the build action. See below for")
449		fmt.Fprintln(ctx.Writer, "the list of acceptable build action flags.")
450		fmt.Fprintln(ctx.Writer, "")
451		flags.PrintDefaults()
452	}
453
454	buildActionFlags := []struct {
455		name        string
456		description string
457		action      build.BuildAction
458		set         bool
459	}{{
460		name:        "all-modules",
461		description: "Build action: build from the top of the source tree.",
462		action:      build.BUILD_MODULES,
463	}, {
464		// This is redirecting to mma build command behaviour. Once it has soaked for a
465		// while, the build command is deleted from here once it has been removed from the
466		// envsetup.sh.
467		name:        "modules-in-a-dir-no-deps",
468		description: "Build action: builds all of the modules in the current directory without their dependencies.",
469		action:      build.BUILD_MODULES_IN_A_DIRECTORY,
470	}, {
471		// This is redirecting to mmma build command behaviour. Once it has soaked for a
472		// while, the build command is deleted from here once it has been removed from the
473		// envsetup.sh.
474		name:        "modules-in-dirs-no-deps",
475		description: "Build action: builds all of the modules in the supplied directories without their dependencies.",
476		action:      build.BUILD_MODULES_IN_DIRECTORIES,
477	}, {
478		name:        "modules-in-a-dir",
479		description: "Build action: builds all of the modules in the current directory and their dependencies.",
480		action:      build.BUILD_MODULES_IN_A_DIRECTORY,
481	}, {
482		name:        "modules-in-dirs",
483		description: "Build action: builds all of the modules in the supplied directories and their dependencies.",
484		action:      build.BUILD_MODULES_IN_DIRECTORIES,
485	}}
486	for i, flag := range buildActionFlags {
487		flags.BoolVar(&buildActionFlags[i].set, flag.name, false, flag.description)
488	}
489	dir := flags.String("dir", "", "Directory of the executed build command.")
490
491	// Only interested in the first two args which defines the build action and the directory.
492	// The remaining arguments are passed down to the config.
493	const numBuildActionFlags = 2
494	if len(args) < numBuildActionFlags {
495		flags.Usage()
496		ctx.Fatalln("Improper build action arguments: too few arguments")
497	}
498	parseError := flags.Parse(args[0:numBuildActionFlags])
499
500	// The next block of code is to validate that exactly one build action is set and the dir flag
501	// is specified.
502	buildActionFound := false
503	var buildAction build.BuildAction
504	for _, f := range buildActionFlags {
505		if f.set {
506			if buildActionFound {
507				if parseError == nil {
508					//otherwise Parse() already called Usage()
509					flags.Usage()
510				}
511				ctx.Fatalf("Build action already specified, omit: --%s\n", f.name)
512			}
513			buildActionFound = true
514			buildAction = f.action
515		}
516	}
517	if !buildActionFound {
518		if parseError == nil {
519			//otherwise Parse() already called Usage()
520			flags.Usage()
521		}
522		ctx.Fatalln("Build action not defined.")
523	}
524	if *dir == "" {
525		ctx.Fatalln("-dir not specified.")
526	}
527
528	// Remove the build action flags from the args as they are not recognized by the config.
529	args = args[numBuildActionFlags:]
530	return build.NewBuildActionConfig(buildAction, *dir, ctx, args...)
531}
532
533func runMake(ctx build.Context, config build.Config, _ []string) {
534	logsDir := config.LogsDir()
535	if config.IsVerbose() {
536		writer := ctx.Writer
537		fmt.Fprintln(writer, "! The argument `showcommands` is no longer supported.")
538		fmt.Fprintln(writer, "! Instead, the verbose log is always written to a compressed file in the output dir:")
539		fmt.Fprintln(writer, "!")
540		fmt.Fprintf(writer, "!   gzip -cd %s/verbose.log.gz | less -R\n", logsDir)
541		fmt.Fprintln(writer, "!")
542		fmt.Fprintln(writer, "! Older versions are saved in verbose.log.#.gz files")
543		fmt.Fprintln(writer, "")
544		ctx.Fatal("Invalid argument")
545	}
546
547	if _, ok := config.Environment().Get("ONE_SHOT_MAKEFILE"); ok {
548		writer := ctx.Writer
549		fmt.Fprintln(writer, "! The variable `ONE_SHOT_MAKEFILE` is obsolete.")
550		fmt.Fprintln(writer, "!")
551		fmt.Fprintln(writer, "! If you're using `mm`, you'll need to run `source build/envsetup.sh` to update.")
552		fmt.Fprintln(writer, "!")
553		fmt.Fprintln(writer, "! Otherwise, either specify a module name with m, or use mma / MODULES-IN-...")
554		fmt.Fprintln(writer, "")
555		ctx.Fatal("Invalid environment")
556	}
557
558	build.Build(ctx, config)
559}
560
561// getCommand finds the appropriate command based on args[1] flag. args[0]
562// is the soong_ui filename.
563func getCommand(args []string) (*command, []string, error) {
564	listFlags := func() []string {
565		flags := make([]string, len(commands))
566		for i, c := range commands {
567			flags[i] = c.flag
568		}
569		return flags
570	}
571
572	if len(args) < 2 {
573		return nil, nil, fmt.Errorf("Too few arguments: %q\nUse one of these: %q", args, listFlags())
574	}
575
576	for _, c := range commands {
577		if c.flag == args[1] {
578			return &c, args[2:], nil
579		}
580	}
581	return nil, nil, fmt.Errorf("Command not found: %q\nDid you mean one of these: %q", args[1], listFlags())
582}
583
584func setMaxFiles(ctx build.Context) {
585	var limits syscall.Rlimit
586
587	err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limits)
588	if err != nil {
589		ctx.Println("Failed to get file limit:", err)
590		return
591	}
592
593	ctx.Verbosef("Current file limits: %d soft, %d hard", limits.Cur, limits.Max)
594
595	// Go 1.21 modifies the file limit but restores the original when
596	// execing subprocesses if it hasn't be overridden.  Call Setrlimit
597	// here even if it doesn't appear to be necessary so that the
598	// syscall package considers it set.
599
600	limits.Cur = limits.Max
601	err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limits)
602	if err != nil {
603		ctx.Println("Failed to increase file limit:", err)
604	}
605}
606