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