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