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 "android/soong/ui/metrics" 19 "android/soong/ui/status" 20 "crypto/md5" 21 "fmt" 22 "io/ioutil" 23 "os" 24 "os/user" 25 "path/filepath" 26 "strings" 27) 28 29var spaceSlashReplacer = strings.NewReplacer("/", "_", " ", "_") 30 31const katiBuildSuffix = "" 32const katiCleanspecSuffix = "-cleanspec" 33const katiPackageSuffix = "-package" 34const katiSoongOnlyPackageSuffix = "-soong-only-package" 35 36// genKatiSuffix creates a filename suffix for kati-generated files so that we 37// can cache them based on their inputs. Such files include the generated Ninja 38// files and env.sh environment variable setup files. 39// 40// The filename suffix should encode all common changes to Kati inputs. 41// Currently that includes the TARGET_PRODUCT and kati-processed command line 42// arguments. 43func genKatiSuffix(ctx Context, config Config) { 44 targetProduct := "unknown" 45 if p, err := config.TargetProductOrErr(); err == nil { 46 targetProduct = p 47 } 48 // Construct the base suffix. 49 katiSuffix := "-" + targetProduct + config.CoverageSuffix() 50 51 // Append kati arguments to the suffix. 52 if args := config.KatiArgs(); len(args) > 0 { 53 katiSuffix += "-" + spaceSlashReplacer.Replace(strings.Join(args, "_")) 54 } 55 56 // If the suffix is too long, replace it with a md5 hash and write a 57 // file that contains the original suffix. 58 if len(katiSuffix) > 64 { 59 shortSuffix := "-" + fmt.Sprintf("%x", md5.Sum([]byte(katiSuffix))) 60 config.SetKatiSuffix(shortSuffix) 61 62 ctx.Verbosef("Kati ninja suffix too long: %q", katiSuffix) 63 ctx.Verbosef("Replacing with: %q", shortSuffix) 64 65 if err := ioutil.WriteFile(strings.TrimSuffix(config.KatiBuildNinjaFile(), "ninja")+"suf", []byte(katiSuffix), 0777); err != nil { 66 ctx.Println("Error writing suffix file:", err) 67 } 68 } else { 69 config.SetKatiSuffix(katiSuffix) 70 } 71} 72 73func writeValueIfChanged(ctx Context, config Config, dir string, filename string, value string) { 74 filePath := filepath.Join(dir, filename) 75 previousValue := "" 76 rawPreviousValue, err := os.ReadFile(filePath) 77 if err == nil { 78 previousValue = string(rawPreviousValue) 79 } 80 81 if previousValue != value { 82 if err = os.WriteFile(filePath, []byte(value), 0666); err != nil { 83 ctx.Fatalf("Failed to write: %v", err) 84 } 85 } 86} 87 88// Base function to construct and run the Kati command line with additional 89// arguments, and a custom function closure to mutate the environment Kati runs 90// in. 91func runKati(ctx Context, config Config, extraSuffix string, args []string, envFunc func(*Environment)) { 92 executable := config.KatiBin() 93 // cKati arguments. 94 args = append([]string{ 95 // Instead of executing commands directly, generate a Ninja file. 96 "--ninja", 97 // Generate Ninja files in the output directory. 98 "--ninja_dir=" + config.OutDir(), 99 // Filename suffix of the generated Ninja file. 100 "--ninja_suffix=" + config.KatiSuffix() + extraSuffix, 101 // Remove common parts at the beginning of a Ninja file, like build_dir, 102 // local_pool and _kati_always_build_. Allows Kati to be run multiple 103 // times, with generated Ninja files combined in a single invocation 104 // using 'include'. 105 "--no_ninja_prelude", 106 // Support declaring phony outputs in AOSP Ninja. 107 "--use_ninja_phony_output", 108 // Regenerate the Ninja file if environment inputs have changed. e.g. 109 // CLI flags, .mk file timestamps, env vars, $(wildcard ..) and some 110 // $(shell ..) results. 111 "--regen", 112 // Skip '-include' directives starting with the specified path. Used to 113 // ignore generated .mk files. 114 "--ignore_optional_include=" + filepath.Join(config.OutDir(), "%.P"), 115 // Detect the use of $(shell echo ...). 116 "--detect_android_echo", 117 // Colorful ANSI-based warning and error messages. 118 "--color_warnings", 119 // Generate all targets, not just the top level requested ones. 120 "--gen_all_targets", 121 // Use the built-in emulator of GNU find for better file finding 122 // performance. Used with $(shell find ...). 123 "--use_find_emulator", 124 // Fail when the find emulator encounters problems. 125 "--werror_find_emulator", 126 // Do not provide any built-in rules. 127 "--no_builtin_rules", 128 // Fail when suffix rules are used. 129 "--werror_suffix_rules", 130 // Fail when a real target depends on a phony target. 131 "--werror_real_to_phony", 132 // Makes real_to_phony checks assume that any top-level or leaf 133 // dependencies that does *not* have a '/' in it is a phony target. 134 "--top_level_phony", 135 // Fail when a phony target contains slashes. 136 "--werror_phony_looks_real", 137 // Fail when writing to a read-only directory. 138 "--werror_writable", 139 // Print Kati's internal statistics, such as the number of variables, 140 // implicit/explicit/suffix rules, and so on. 141 "--kati_stats", 142 }, args...) 143 144 // Generate a minimal Ninja file. 145 // 146 // Used for build_test and multiproduct_kati, which runs Kati several 147 // hundred times for different configurations to test file generation logic. 148 // These can result in generating Ninja files reaching ~1GB or more, 149 // resulting in ~hundreds of GBs of writes. 150 // 151 // Since we don't care about executing the Ninja files in these test cases, 152 // generating the Ninja file content wastes time, so skip writing any 153 // information out with --empty_ninja_file. 154 // 155 // From https://github.com/google/kati/commit/87b8da7af2c8bea28b1d8ab17679453d859f96e5 156 if config.EmptyNinjaFile() { 157 args = append(args, "--empty_ninja_file") 158 } 159 160 // Apply 'local_pool' to to all rules that don't specify a pool. 161 if config.UseRemoteBuild() { 162 args = append(args, "--default_pool=local_pool") 163 } 164 165 cmd := Command(ctx, config, "ckati", executable, args...) 166 167 // Set up the nsjail sandbox. 168 cmd.Sandbox = katiSandbox 169 170 // Set up stdout and stderr. 171 pipe, err := cmd.StdoutPipe() 172 if err != nil { 173 ctx.Fatalln("Error getting output pipe for ckati:", err) 174 } 175 cmd.Stderr = cmd.Stdout 176 177 var username string 178 // Pass on various build environment metadata to Kati. 179 if usernameFromEnv, ok := cmd.Environment.Get("BUILD_USERNAME"); !ok { 180 username = "unknown" 181 if u, err := user.Current(); err == nil { 182 username = u.Username 183 } else { 184 ctx.Println("Failed to get current user:", err) 185 } 186 cmd.Environment.Set("BUILD_USERNAME", username) 187 } else { 188 username = usernameFromEnv 189 } 190 191 // SOONG_USE_PARTIAL_COMPILE may be used in makefiles, but both cases must be supported. 192 // 193 // In general, the partial compile features will be implemented in Soong-based rules. We 194 // also allow them to be used in makefiles. Clear the environment variable when calling 195 // kati so that we avoid reanalysis when the user changes it. We will pass it to Ninja. 196 // As a result, rules where we want to allow the developer to toggle the feature ("use 197 // the partial compile feature" vs "legacy, aka full compile behavior") need to use this 198 // in the rule, since changing it will not cause reanalysis. 199 // 200 // Shell syntax in the rule might look something like this: 201 // if [[ -n ${SOONG_USE_PARTIAL_COMPILE} ]]; then 202 // # partial compile behavior 203 // else 204 // # legacy behavior 205 // fi 206 cmd.Environment.Unset("SOONG_USE_PARTIAL_COMPILE") 207 208 // Unset BUILD_HOSTNAME during kati run to avoid kati rerun, kati will use BUILD_HOSTNAME from a file. 209 cmd.Environment.Unset("BUILD_HOSTNAME") 210 211 _, ok := cmd.Environment.Get("BUILD_NUMBER") 212 // Unset BUILD_NUMBER during kati run to avoid kati rerun, kati will use BUILD_NUMBER from a file. 213 cmd.Environment.Unset("BUILD_NUMBER") 214 if ok { 215 cmd.Environment.Set("HAS_BUILD_NUMBER", "true") 216 } else { 217 cmd.Environment.Set("HAS_BUILD_NUMBER", "false") 218 } 219 220 // Apply the caller's function closure to mutate the environment variables. 221 envFunc(cmd.Environment) 222 223 cmd.StartOrFatal() 224 // Set up the ToolStatus command line reader for Kati for a consistent UI 225 // for the user. 226 status.KatiReader(ctx.Status.StartTool(), pipe) 227 cmd.WaitOrFatal() 228} 229 230func runKatiBuild(ctx Context, config Config) { 231 ctx.BeginTrace(metrics.RunKati, "kati build") 232 defer ctx.EndTrace() 233 234 args := []string{ 235 // Mark the output directory as writable. 236 "--writable", config.OutDir() + "/", 237 // Fail when encountering implicit rules. e.g. 238 // %.foo: %.bar 239 // cp $< $@ 240 "--werror_implicit_rules", 241 // Entry point for the Kati Ninja file generation. 242 "-f", "build/make/core/main.mk", 243 } 244 245 if !config.BuildBrokenDupRules() { 246 // Fail when redefining / duplicating a target. 247 args = append(args, "--werror_overriding_commands") 248 } 249 250 args = append(args, config.KatiArgs()...) 251 252 args = append(args, 253 // Location of the Make vars .mk file generated by Soong. 254 "SOONG_MAKEVARS_MK="+config.SoongMakeVarsMk(), 255 // Location of the Android.mk file generated by Soong. This 256 // file contains Soong modules represented as Kati modules, 257 // allowing Kati modules to depend on Soong modules. 258 "SOONG_ANDROID_MK="+config.SoongAndroidMk(), 259 // Directory containing outputs for the target device. 260 "TARGET_DEVICE_DIR="+config.TargetDeviceDir(), 261 // Directory containing .mk files for packaging purposes, such as 262 // the dist.mk file, containing dist-for-goals data. 263 "KATI_PACKAGE_MK_DIR="+config.KatiPackageMkDir()) 264 265 runKati(ctx, config, katiBuildSuffix, args, func(env *Environment) {}) 266 267 // compress and dist the main build ninja file. 268 distGzipFile(ctx, config, config.KatiBuildNinjaFile()) 269 270 // Cleanup steps. 271 cleanCopyHeaders(ctx, config) 272 cleanOldInstalledFiles(ctx, config) 273} 274 275// Clean out obsolete header files on the disk that were *not copied* during the 276// build with BUILD_COPY_HEADERS and LOCAL_COPY_HEADERS. 277// 278// These should be increasingly uncommon, as it's a deprecated feature and there 279// isn't an equivalent feature in Soong. 280func cleanCopyHeaders(ctx Context, config Config) { 281 ctx.BeginTrace("clean", "clean copy headers") 282 defer ctx.EndTrace() 283 284 // Read and parse the list of copied headers from a file in the product 285 // output directory. 286 data, err := ioutil.ReadFile(filepath.Join(config.ProductOut(), ".copied_headers_list")) 287 if err != nil { 288 if os.IsNotExist(err) { 289 return 290 } 291 ctx.Fatalf("Failed to read copied headers list: %v", err) 292 } 293 294 headers := strings.Fields(string(data)) 295 if len(headers) < 1 { 296 ctx.Fatal("Failed to parse copied headers list: %q", string(data)) 297 } 298 headerDir := headers[0] 299 headers = headers[1:] 300 301 // Walk the tree and remove any headers that are not in the list of copied 302 // headers in the current build. 303 filepath.Walk(headerDir, 304 func(path string, info os.FileInfo, err error) error { 305 if err != nil { 306 return nil 307 } 308 if info.IsDir() { 309 return nil 310 } 311 if !inList(path, headers) { 312 ctx.Printf("Removing obsolete header %q", path) 313 if err := os.Remove(path); err != nil { 314 ctx.Fatalf("Failed to remove obsolete header %q: %v", path, err) 315 } 316 } 317 return nil 318 }) 319} 320 321// Clean out any previously installed files from the disk that are not installed 322// in the current build. 323func cleanOldInstalledFiles(ctx Context, config Config) { 324 ctx.BeginTrace("clean", "clean old installed files") 325 defer ctx.EndTrace() 326 327 // We shouldn't be removing files from one side of the two-step asan builds 328 var suffix string 329 if v, ok := config.Environment().Get("SANITIZE_TARGET"); ok { 330 if sanitize := strings.Fields(v); inList("address", sanitize) { 331 suffix = "_asan" 332 } 333 } 334 335 cleanOldFiles(ctx, config.ProductOut(), ".installable_files"+suffix) 336 337 cleanOldFiles(ctx, config.HostOut(), ".installable_test_files") 338} 339 340// Generate the Ninja file containing the packaging command lines for the dist 341// dir. 342func runKatiPackage(ctx Context, config Config, soongOnly bool) { 343 ctx.BeginTrace(metrics.RunKati, "kati package") 344 defer ctx.EndTrace() 345 346 entryPoint := "build/make/packaging/main.mk" 347 suffix := katiPackageSuffix 348 ninjaFile := config.KatiPackageNinjaFile() 349 if soongOnly { 350 entryPoint = "build/make/packaging/main_soong_only.mk" 351 suffix = katiSoongOnlyPackageSuffix 352 ninjaFile = config.KatiSoongOnlyPackageNinjaFile() 353 } 354 355 args := []string{ 356 // Mark the dist dir as writable. 357 "--writable", config.DistDir() + "/", 358 // Fail when encountering implicit rules. e.g. 359 "--werror_implicit_rules", 360 // Fail when redefining / duplicating a target. 361 "--werror_overriding_commands", 362 // Entry point. 363 "-f", entryPoint, 364 // Directory containing .mk files for packaging purposes, such as 365 // the dist.mk file, containing dist-for-goals data. 366 "KATI_PACKAGE_MK_DIR=" + config.KatiPackageMkDir(), 367 } 368 369 // Run Kati against a restricted set of environment variables. 370 runKati(ctx, config, suffix, args, func(env *Environment) { 371 env.Allow([]string{ 372 // Some generic basics 373 "LANG", 374 "LC_MESSAGES", 375 "PATH", 376 "PWD", 377 "TMPDIR", 378 379 // Tool configs 380 "ASAN_SYMBOLIZER_PATH", 381 "JAVA_HOME", 382 "PYTHONDONTWRITEBYTECODE", 383 384 // Build configuration 385 "ANDROID_BUILD_SHELL", 386 "DIST_DIR", 387 "OUT_DIR", 388 "FILE_NAME_TAG", 389 }...) 390 391 if config.Dist() { 392 env.Set("DIST", "true") 393 env.Set("DIST_DIR", config.DistDir()) 394 } 395 }) 396 397 // Compress and dist the packaging Ninja file. 398 distGzipFile(ctx, config, ninjaFile) 399} 400 401// Run Kati on the cleanspec files to clean the build. 402func runKatiCleanSpec(ctx Context, config Config) { 403 ctx.BeginTrace(metrics.RunKati, "kati cleanspec") 404 defer ctx.EndTrace() 405 406 runKati(ctx, config, katiCleanspecSuffix, []string{ 407 // Fail when encountering implicit rules. e.g. 408 "--werror_implicit_rules", 409 // Fail when redefining / duplicating a target. 410 "--werror_overriding_commands", 411 // Entry point. 412 "-f", "build/make/core/cleanbuild.mk", 413 "SOONG_MAKEVARS_MK=" + config.SoongMakeVarsMk(), 414 "TARGET_DEVICE_DIR=" + config.TargetDeviceDir(), 415 }, func(env *Environment) {}) 416} 417