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