xref: /aosp_15_r20/build/soong/ui/build/ninja.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 build
16
17import (
18	"fmt"
19	"os"
20	"path/filepath"
21	"sort"
22	"strconv"
23	"strings"
24	"time"
25
26	"android/soong/shared"
27	"android/soong/ui/metrics"
28	"android/soong/ui/status"
29)
30
31const (
32	// File containing the environment state when ninja is executed
33	ninjaEnvFileName        = "ninja.environment"
34	ninjaLogFileName        = ".ninja_log"
35	ninjaWeightListFileName = ".ninja_weight_list"
36)
37
38// Constructs and runs the Ninja command line with a restricted set of
39// environment variables. It's important to restrict the environment Ninja runs
40// for hermeticity reasons, and to avoid spurious rebuilds.
41func runNinjaForBuild(ctx Context, config Config) {
42	ctx.BeginTrace(metrics.PrimaryNinja, "ninja")
43	defer ctx.EndTrace()
44
45	// Sets up the FIFO status updater that reads the Ninja protobuf output, and
46	// translates it to the soong_ui status output, displaying real-time
47	// progress of the build.
48	fifo := filepath.Join(config.OutDir(), ".ninja_fifo")
49	nr := status.NewNinjaReader(ctx, ctx.Status.StartTool(), fifo)
50	defer nr.Close()
51
52	var executable string
53	var args []string
54	switch config.ninjaCommand {
55	case NINJA_N2:
56		executable = config.N2Bin()
57		args = []string{
58			"-d", "trace",
59			// TODO: implement these features, or remove them.
60			//"-d", "keepdepfile",
61			//"-d", "keeprsp",
62			//"-d", "stats",
63			"--frontend-file", fifo,
64		}
65	case NINJA_SISO:
66		executable = config.SisoBin()
67		args = []string{
68			"ninja",
69			"--log_dir", config.SoongOutDir(),
70			// TODO: implement these features, or remove them.
71			//"-d", "trace",
72			//"-d", "keepdepfile",
73			//"-d", "keeprsp",
74			//"-d", "stats",
75			//"--frontend-file", fifo,
76		}
77	default:
78		// NINJA_NINJA is the default.
79		executable = config.NinjaBin()
80		args = []string{
81			"-d", "keepdepfile",
82			"-d", "keeprsp",
83			"-d", "stats",
84			"--frontend_file", fifo,
85			"-o", "usesphonyoutputs=yes",
86			"-w", "dupbuild=err",
87			"-w", "missingdepfile=err",
88		}
89	}
90	args = append(args, config.NinjaArgs()...)
91
92	var parallel int
93	if config.UseRemoteBuild() {
94		parallel = config.RemoteParallel()
95	} else {
96		parallel = config.Parallel()
97	}
98	args = append(args, "-j", strconv.Itoa(parallel))
99	if config.keepGoing != 1 {
100		args = append(args, "-k", strconv.Itoa(config.keepGoing))
101	}
102
103	args = append(args, "-f", config.CombinedNinjaFile())
104
105	if !config.BuildBrokenMissingOutputs() {
106		// Missing outputs will be treated as errors.
107		// BUILD_BROKEN_MISSING_OUTPUTS can be used to bypass this check.
108		if config.ninjaCommand != NINJA_N2 {
109			args = append(args,
110				"-w", "missingoutfile=err",
111			)
112		}
113	}
114
115	cmd := Command(ctx, config, "ninja", executable, args...)
116
117	// Set up the nsjail sandbox Ninja runs in.
118	cmd.Sandbox = ninjaSandbox
119	if config.HasKatiSuffix() {
120		// Reads and executes a shell script from Kati that sets/unsets the
121		// environment Ninja runs in.
122		cmd.Environment.AppendFromKati(config.KatiEnvFile())
123	}
124
125	// TODO(b/346806126): implement this for the other ninjaCommand values.
126	if config.ninjaCommand == NINJA_NINJA {
127		switch config.NinjaWeightListSource() {
128		case NINJA_LOG:
129			cmd.Args = append(cmd.Args, "-o", "usesninjalogasweightlist=yes")
130		case EVENLY_DISTRIBUTED:
131			// pass empty weight list means ninja considers every tasks's weight as 1(default value).
132			cmd.Args = append(cmd.Args, "-o", "usesweightlist=/dev/null")
133		case EXTERNAL_FILE:
134			fallthrough
135		case HINT_FROM_SOONG:
136			// The weight list is already copied/generated.
137			ninjaWeightListPath := filepath.Join(config.OutDir(), ninjaWeightListFileName)
138			cmd.Args = append(cmd.Args, "-o", "usesweightlist="+ninjaWeightListPath)
139		}
140	}
141
142	// Allow both NINJA_ARGS and NINJA_EXTRA_ARGS, since both have been
143	// used in the past to specify extra ninja arguments.
144	if extra, ok := cmd.Environment.Get("NINJA_ARGS"); ok {
145		cmd.Args = append(cmd.Args, strings.Fields(extra)...)
146	}
147	if extra, ok := cmd.Environment.Get("NINJA_EXTRA_ARGS"); ok {
148		cmd.Args = append(cmd.Args, strings.Fields(extra)...)
149	}
150
151	ninjaHeartbeatDuration := time.Minute * 5
152	// Get the ninja heartbeat interval from the environment before it's filtered away later.
153	if overrideText, ok := cmd.Environment.Get("NINJA_HEARTBEAT_INTERVAL"); ok {
154		// For example, "1m"
155		overrideDuration, err := time.ParseDuration(overrideText)
156		if err == nil && overrideDuration.Seconds() > 0 {
157			ninjaHeartbeatDuration = overrideDuration
158		}
159	}
160
161	// Filter the environment, as ninja does not rebuild files when environment
162	// variables change.
163	//
164	// Anything listed here must not change the output of rules/actions when the
165	// value changes, otherwise incremental builds may be unsafe. Vars
166	// explicitly set to stable values elsewhere in soong_ui are fine.
167	//
168	// For the majority of cases, either Soong or the makefiles should be
169	// replicating any necessary environment variables in the command line of
170	// each action that needs it.
171	if cmd.Environment.IsEnvTrue("ALLOW_NINJA_ENV") {
172		ctx.Println("Allowing all environment variables during ninja; incremental builds may be unsafe.")
173	} else {
174		cmd.Environment.Allow(append([]string{
175			// Set the path to a symbolizer (e.g. llvm-symbolizer) so ASAN-based
176			// tools can symbolize crashes.
177			"ASAN_SYMBOLIZER_PATH",
178			"HOME",
179			"JAVA_HOME",
180			"LANG",
181			"LC_MESSAGES",
182			"OUT_DIR",
183			"PATH",
184			"PWD",
185			// https://docs.python.org/3/using/cmdline.html#envvar-PYTHONDONTWRITEBYTECODE
186			"PYTHONDONTWRITEBYTECODE",
187			"TMPDIR",
188			"USER",
189
190			// TODO: remove these carefully
191			// Options for the address sanitizer.
192			"ASAN_OPTIONS",
193			// The list of Android app modules to be built in an unbundled manner.
194			"TARGET_BUILD_APPS",
195			// The variant of the product being built. e.g. eng, userdebug, debug.
196			"TARGET_BUILD_VARIANT",
197			// The product name of the product being built, e.g. aosp_arm, aosp_flame.
198			"TARGET_PRODUCT",
199			// b/147197813 - used by art-check-debug-apex-gen
200			"EMMA_INSTRUMENT_FRAMEWORK",
201
202			// RBE client
203			"RBE_compare",
204			"RBE_num_local_reruns",
205			"RBE_num_remote_reruns",
206			"RBE_exec_root",
207			"RBE_exec_strategy",
208			"RBE_invocation_id",
209			"RBE_log_dir",
210			"RBE_num_retries_if_mismatched",
211			"RBE_platform",
212			"RBE_remote_accept_cache",
213			"RBE_remote_update_cache",
214			"RBE_server_address",
215			// TODO: remove old FLAG_ variables.
216			"FLAG_compare",
217			"FLAG_exec_root",
218			"FLAG_exec_strategy",
219			"FLAG_invocation_id",
220			"FLAG_log_dir",
221			"FLAG_platform",
222			"FLAG_remote_accept_cache",
223			"FLAG_remote_update_cache",
224			"FLAG_server_address",
225
226			// ccache settings
227			"CCACHE_COMPILERCHECK",
228			"CCACHE_SLOPPINESS",
229			"CCACHE_BASEDIR",
230			"CCACHE_CPP2",
231			"CCACHE_DIR",
232
233			// LLVM compiler wrapper options
234			"TOOLCHAIN_RUSAGE_OUTPUT",
235
236			// We don't want this build broken flag to cause reanalysis, so allow it through to the
237			// actions.
238			"BUILD_BROKEN_INCORRECT_PARTITION_IMAGES",
239			// Do not do reanalysis just because we changed ninja commands.
240			"SOONG_NINJA",
241			"SOONG_USE_N2",
242			"RUST_BACKTRACE",
243			"RUST_LOG",
244
245			// SOONG_USE_PARTIAL_COMPILE only determines which half of the rule we execute.
246			"SOONG_USE_PARTIAL_COMPILE",
247		}, config.BuildBrokenNinjaUsesEnvVars()...)...)
248	}
249
250	cmd.Environment.Set("DIST_DIR", config.DistDir())
251	cmd.Environment.Set("SHELL", "/bin/bash")
252	switch config.ninjaCommand {
253	case NINJA_N2:
254		cmd.Environment.Set("RUST_BACKTRACE", "1")
255	default:
256		// Only set RUST_BACKTRACE for n2.
257	}
258
259	// Print the environment variables that Ninja is operating in.
260	ctx.Verboseln("Ninja environment: ")
261	envVars := cmd.Environment.Environ()
262	sort.Strings(envVars)
263	for _, envVar := range envVars {
264		ctx.Verbosef("  %s", envVar)
265	}
266
267	// Write the env vars available during ninja execution to a file
268	ninjaEnvVars := cmd.Environment.AsMap()
269	data, err := shared.EnvFileContents(ninjaEnvVars)
270	if err != nil {
271		ctx.Panicf("Could not parse environment variables for ninja run %s", err)
272	}
273	// Write the file in every single run. This is fine because
274	// 1. It is not a dep of Soong analysis, so will not retrigger Soong analysis.
275	// 2. Is is fairly lightweight (~1Kb)
276	ninjaEnvVarsFile := shared.JoinPath(config.SoongOutDir(), ninjaEnvFileName)
277	err = os.WriteFile(ninjaEnvVarsFile, data, 0666)
278	if err != nil {
279		ctx.Panicf("Could not write ninja environment file %s", err)
280	}
281
282	// Poll the Ninja log for updates regularly based on the heartbeat
283	// frequency. If it isn't updated enough, then we want to surface the
284	// possibility that Ninja is stuck, to the user.
285	done := make(chan struct{})
286	defer close(done)
287	ticker := time.NewTicker(ninjaHeartbeatDuration)
288	defer ticker.Stop()
289	ninjaChecker := &ninjaStucknessChecker{
290		logPath: filepath.Join(config.OutDir(), ninjaLogFileName),
291	}
292	go func() {
293		for {
294			select {
295			case <-ticker.C:
296				ninjaChecker.check(ctx, config)
297			case <-done:
298				return
299			}
300		}
301	}()
302
303	ctx.Status.Status("Starting ninja...")
304	cmd.RunAndStreamOrFatal()
305}
306
307// A simple struct for checking if Ninja gets stuck, using timestamps.
308type ninjaStucknessChecker struct {
309	logPath     string
310	prevModTime time.Time
311}
312
313// Check that a file has been modified since the last time it was checked. If
314// the mod time hasn't changed, then assume that Ninja got stuck, and print
315// diagnostics for debugging.
316func (c *ninjaStucknessChecker) check(ctx Context, config Config) {
317	info, err := os.Stat(c.logPath)
318	var newModTime time.Time
319	if err == nil {
320		newModTime = info.ModTime()
321	}
322	if newModTime == c.prevModTime {
323		// The Ninja file hasn't been modified since the last time it was
324		// checked, so Ninja could be stuck. Output some diagnostics.
325		ctx.Verbosef("ninja may be stuck; last update to %v was %v. dumping process tree...", c.logPath, newModTime)
326		ctx.Printf("ninja may be stuck, check %v for list of running processes.",
327			filepath.Join(config.LogsDir(), config.logsPrefix+"soong.log"))
328
329		// The "pstree" command doesn't exist on Mac, but "pstree" on Linux
330		// gives more convenient output than "ps" So, we try pstree first, and
331		// ps second
332		commandText := fmt.Sprintf("pstree -palT %v || ps -ef", os.Getpid())
333
334		cmd := Command(ctx, config, "dump process tree", "bash", "-c", commandText)
335		output := cmd.CombinedOutputOrFatal()
336		ctx.Verbose(string(output))
337
338		ctx.Verbosef("done\n")
339	}
340	c.prevModTime = newModTime
341}
342