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