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