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