| // Copyright 2017 Google Inc. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package build |
| |
| import ( |
| "android/soong/ui/metrics" |
| "android/soong/ui/status" |
| "crypto/md5" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "os/user" |
| "path/filepath" |
| "strings" |
| ) |
| |
| var spaceSlashReplacer = strings.NewReplacer("/", "_", " ", "_") |
| |
| const katiBuildSuffix = "" |
| const katiCleanspecSuffix = "-cleanspec" |
| const katiPackageSuffix = "-package" |
| |
| // genKatiSuffix creates a filename suffix for kati-generated files so that we |
| // can cache them based on their inputs. Such files include the generated Ninja |
| // files and env.sh environment variable setup files. |
| // |
| // The filename suffix should encode all common changes to Kati inputs. |
| // Currently that includes the TARGET_PRODUCT and kati-processed command line |
| // arguments. |
| func genKatiSuffix(ctx Context, config Config) { |
| // Construct the base suffix. |
| katiSuffix := "-" + config.TargetProduct() |
| |
| // Append kati arguments to the suffix. |
| if args := config.KatiArgs(); len(args) > 0 { |
| katiSuffix += "-" + spaceSlashReplacer.Replace(strings.Join(args, "_")) |
| } |
| |
| // If the suffix is too long, replace it with a md5 hash and write a |
| // file that contains the original suffix. |
| if len(katiSuffix) > 64 { |
| shortSuffix := "-" + fmt.Sprintf("%x", md5.Sum([]byte(katiSuffix))) |
| config.SetKatiSuffix(shortSuffix) |
| |
| ctx.Verbosef("Kati ninja suffix too long: %q", katiSuffix) |
| ctx.Verbosef("Replacing with: %q", shortSuffix) |
| |
| if err := ioutil.WriteFile(strings.TrimSuffix(config.KatiBuildNinjaFile(), "ninja")+"suf", []byte(katiSuffix), 0777); err != nil { |
| ctx.Println("Error writing suffix file:", err) |
| } |
| } else { |
| config.SetKatiSuffix(katiSuffix) |
| } |
| } |
| |
| func writeValueIfChanged(ctx Context, config Config, dir string, filename string, value string) { |
| filePath := filepath.Join(dir, filename) |
| previousValue := "" |
| rawPreviousValue, err := ioutil.ReadFile(filePath) |
| if err == nil { |
| previousValue = string(rawPreviousValue) |
| } |
| |
| if previousValue != value { |
| if err = ioutil.WriteFile(filePath, []byte(value), 0666); err != nil { |
| ctx.Fatalf("Failed to write: %v", err) |
| } |
| } |
| } |
| |
| // Base function to construct and run the Kati command line with additional |
| // arguments, and a custom function closure to mutate the environment Kati runs |
| // in. |
| func runKati(ctx Context, config Config, extraSuffix string, args []string, envFunc func(*Environment)) { |
| executable := config.PrebuiltBuildTool("ckati") |
| // cKati arguments. |
| args = append([]string{ |
| // Instead of executing commands directly, generate a Ninja file. |
| "--ninja", |
| // Generate Ninja files in the output directory. |
| "--ninja_dir=" + config.OutDir(), |
| // Filename suffix of the generated Ninja file. |
| "--ninja_suffix=" + config.KatiSuffix() + extraSuffix, |
| // Remove common parts at the beginning of a Ninja file, like build_dir, |
| // local_pool and _kati_always_build_. Allows Kati to be run multiple |
| // times, with generated Ninja files combined in a single invocation |
| // using 'include'. |
| "--no_ninja_prelude", |
| // Support declaring phony outputs in AOSP Ninja. |
| "--use_ninja_phony_output", |
| // Regenerate the Ninja file if environment inputs have changed. e.g. |
| // CLI flags, .mk file timestamps, env vars, $(wildcard ..) and some |
| // $(shell ..) results. |
| "--regen", |
| // Skip '-include' directives starting with the specified path. Used to |
| // ignore generated .mk files. |
| "--ignore_optional_include=" + filepath.Join(config.OutDir(), "%.P"), |
| // Detect the use of $(shell echo ...). |
| "--detect_android_echo", |
| // Colorful ANSI-based warning and error messages. |
| "--color_warnings", |
| // Generate all targets, not just the top level requested ones. |
| "--gen_all_targets", |
| // Use the built-in emulator of GNU find for better file finding |
| // performance. Used with $(shell find ...). |
| "--use_find_emulator", |
| // Fail when the find emulator encounters problems. |
| "--werror_find_emulator", |
| // Do not provide any built-in rules. |
| "--no_builtin_rules", |
| // Fail when suffix rules are used. |
| "--werror_suffix_rules", |
| // Fail when a real target depends on a phony target. |
| "--werror_real_to_phony", |
| // Makes real_to_phony checks assume that any top-level or leaf |
| // dependencies that does *not* have a '/' in it is a phony target. |
| "--top_level_phony", |
| // Fail when a phony target contains slashes. |
| "--werror_phony_looks_real", |
| // Fail when writing to a read-only directory. |
| "--werror_writable", |
| // Print Kati's internal statistics, such as the number of variables, |
| // implicit/explicit/suffix rules, and so on. |
| "--kati_stats", |
| }, args...) |
| |
| // Generate a minimal Ninja file. |
| // |
| // Used for build_test and multiproduct_kati, which runs Kati several |
| // hundred times for different configurations to test file generation logic. |
| // These can result in generating Ninja files reaching ~1GB or more, |
| // resulting in ~hundreds of GBs of writes. |
| // |
| // Since we don't care about executing the Ninja files in these test cases, |
| // generating the Ninja file content wastes time, so skip writing any |
| // information out with --empty_ninja_file. |
| // |
| // From https://github.com/google/kati/commit/87b8da7af2c8bea28b1d8ab17679453d859f96e5 |
| if config.EmptyNinjaFile() { |
| args = append(args, "--empty_ninja_file") |
| } |
| |
| // Apply 'local_pool' to to all rules that don't specify a pool. |
| if config.UseRemoteBuild() { |
| args = append(args, "--default_pool=local_pool") |
| } |
| |
| cmd := Command(ctx, config, "ckati", executable, args...) |
| |
| // Set up the nsjail sandbox. |
| cmd.Sandbox = katiSandbox |
| |
| // Set up stdout and stderr. |
| pipe, err := cmd.StdoutPipe() |
| if err != nil { |
| ctx.Fatalln("Error getting output pipe for ckati:", err) |
| } |
| cmd.Stderr = cmd.Stdout |
| |
| var username string |
| // Pass on various build environment metadata to Kati. |
| if usernameFromEnv, ok := cmd.Environment.Get("BUILD_USERNAME"); !ok { |
| username = "unknown" |
| if u, err := user.Current(); err == nil { |
| username = u.Username |
| } else { |
| ctx.Println("Failed to get current user:", err) |
| } |
| cmd.Environment.Set("BUILD_USERNAME", username) |
| } else { |
| username = usernameFromEnv |
| } |
| |
| hostname, ok := cmd.Environment.Get("BUILD_HOSTNAME") |
| // Unset BUILD_HOSTNAME during kati run to avoid kati rerun, kati will use BUILD_HOSTNAME from a file. |
| cmd.Environment.Unset("BUILD_HOSTNAME") |
| if !ok { |
| hostname, err = os.Hostname() |
| if err != nil { |
| ctx.Println("Failed to read hostname:", err) |
| hostname = "unknown" |
| } |
| } |
| writeValueIfChanged(ctx, config, config.SoongOutDir(), "build_hostname.txt", hostname) |
| _, ok = cmd.Environment.Get("BUILD_NUMBER") |
| // Unset BUILD_NUMBER during kati run to avoid kati rerun, kati will use BUILD_NUMBER from a file. |
| cmd.Environment.Unset("BUILD_NUMBER") |
| if ok { |
| cmd.Environment.Set("HAS_BUILD_NUMBER", "true") |
| } else { |
| cmd.Environment.Set("HAS_BUILD_NUMBER", "false") |
| } |
| |
| // Apply the caller's function closure to mutate the environment variables. |
| envFunc(cmd.Environment) |
| |
| cmd.StartOrFatal() |
| // Set up the ToolStatus command line reader for Kati for a consistent UI |
| // for the user. |
| status.KatiReader(ctx.Status.StartTool(), pipe) |
| cmd.WaitOrFatal() |
| } |
| |
| func runKatiBuild(ctx Context, config Config) { |
| ctx.BeginTrace(metrics.RunKati, "kati build") |
| defer ctx.EndTrace() |
| |
| args := []string{ |
| // Mark the output directory as writable. |
| "--writable", config.OutDir() + "/", |
| // Fail when encountering implicit rules. e.g. |
| // %.foo: %.bar |
| // cp $< $@ |
| "--werror_implicit_rules", |
| // Entry point for the Kati Ninja file generation. |
| "-f", "build/make/core/main.mk", |
| } |
| |
| if !config.BuildBrokenDupRules() { |
| // Fail when redefining / duplicating a target. |
| args = append(args, "--werror_overriding_commands") |
| } |
| |
| args = append(args, config.KatiArgs()...) |
| |
| args = append(args, |
| // Location of the Make vars .mk file generated by Soong. |
| "SOONG_MAKEVARS_MK="+config.SoongMakeVarsMk(), |
| // Location of the Android.mk file generated by Soong. This |
| // file contains Soong modules represented as Kati modules, |
| // allowing Kati modules to depend on Soong modules. |
| "SOONG_ANDROID_MK="+config.SoongAndroidMk(), |
| // Directory containing outputs for the target device. |
| "TARGET_DEVICE_DIR="+config.TargetDeviceDir(), |
| // Directory containing .mk files for packaging purposes, such as |
| // the dist.mk file, containing dist-for-goals data. |
| "KATI_PACKAGE_MK_DIR="+config.KatiPackageMkDir()) |
| |
| runKati(ctx, config, katiBuildSuffix, args, func(env *Environment) {}) |
| |
| // compress and dist the main build ninja file. |
| distGzipFile(ctx, config, config.KatiBuildNinjaFile()) |
| |
| // Cleanup steps. |
| cleanCopyHeaders(ctx, config) |
| cleanOldInstalledFiles(ctx, config) |
| } |
| |
| // Clean out obsolete header files on the disk that were *not copied* during the |
| // build with BUILD_COPY_HEADERS and LOCAL_COPY_HEADERS. |
| // |
| // These should be increasingly uncommon, as it's a deprecated feature and there |
| // isn't an equivalent feature in Soong. |
| func cleanCopyHeaders(ctx Context, config Config) { |
| ctx.BeginTrace("clean", "clean copy headers") |
| defer ctx.EndTrace() |
| |
| // Read and parse the list of copied headers from a file in the product |
| // output directory. |
| data, err := ioutil.ReadFile(filepath.Join(config.ProductOut(), ".copied_headers_list")) |
| if err != nil { |
| if os.IsNotExist(err) { |
| return |
| } |
| ctx.Fatalf("Failed to read copied headers list: %v", err) |
| } |
| |
| headers := strings.Fields(string(data)) |
| if len(headers) < 1 { |
| ctx.Fatal("Failed to parse copied headers list: %q", string(data)) |
| } |
| headerDir := headers[0] |
| headers = headers[1:] |
| |
| // Walk the tree and remove any headers that are not in the list of copied |
| // headers in the current build. |
| filepath.Walk(headerDir, |
| func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| return nil |
| } |
| if info.IsDir() { |
| return nil |
| } |
| if !inList(path, headers) { |
| ctx.Printf("Removing obsolete header %q", path) |
| if err := os.Remove(path); err != nil { |
| ctx.Fatalf("Failed to remove obsolete header %q: %v", path, err) |
| } |
| } |
| return nil |
| }) |
| } |
| |
| // Clean out any previously installed files from the disk that are not installed |
| // in the current build. |
| func cleanOldInstalledFiles(ctx Context, config Config) { |
| ctx.BeginTrace("clean", "clean old installed files") |
| defer ctx.EndTrace() |
| |
| // We shouldn't be removing files from one side of the two-step asan builds |
| var suffix string |
| if v, ok := config.Environment().Get("SANITIZE_TARGET"); ok { |
| if sanitize := strings.Fields(v); inList("address", sanitize) { |
| suffix = "_asan" |
| } |
| } |
| |
| cleanOldFiles(ctx, config.ProductOut(), ".installable_files"+suffix) |
| |
| cleanOldFiles(ctx, config.HostOut(), ".installable_test_files") |
| } |
| |
| // Generate the Ninja file containing the packaging command lines for the dist |
| // dir. |
| func runKatiPackage(ctx Context, config Config) { |
| ctx.BeginTrace(metrics.RunKati, "kati package") |
| defer ctx.EndTrace() |
| |
| args := []string{ |
| // Mark the dist dir as writable. |
| "--writable", config.DistDir() + "/", |
| // Fail when encountering implicit rules. e.g. |
| "--werror_implicit_rules", |
| // Fail when redefining / duplicating a target. |
| "--werror_overriding_commands", |
| // Entry point. |
| "-f", "build/make/packaging/main.mk", |
| // Directory containing .mk files for packaging purposes, such as |
| // the dist.mk file, containing dist-for-goals data. |
| "KATI_PACKAGE_MK_DIR=" + config.KatiPackageMkDir(), |
| } |
| |
| // Run Kati against a restricted set of environment variables. |
| runKati(ctx, config, katiPackageSuffix, args, func(env *Environment) { |
| env.Allow([]string{ |
| // Some generic basics |
| "LANG", |
| "LC_MESSAGES", |
| "PATH", |
| "PWD", |
| "TMPDIR", |
| |
| // Tool configs |
| "ASAN_SYMBOLIZER_PATH", |
| "JAVA_HOME", |
| "PYTHONDONTWRITEBYTECODE", |
| |
| // Build configuration |
| "ANDROID_BUILD_SHELL", |
| "DIST_DIR", |
| "OUT_DIR", |
| "FILE_NAME_TAG", |
| }...) |
| |
| if config.Dist() { |
| env.Set("DIST", "true") |
| env.Set("DIST_DIR", config.DistDir()) |
| } |
| }) |
| |
| // Compress and dist the packaging Ninja file. |
| distGzipFile(ctx, config, config.KatiPackageNinjaFile()) |
| } |
| |
| // Run Kati on the cleanspec files to clean the build. |
| func runKatiCleanSpec(ctx Context, config Config) { |
| ctx.BeginTrace(metrics.RunKati, "kati cleanspec") |
| defer ctx.EndTrace() |
| |
| runKati(ctx, config, katiCleanspecSuffix, []string{ |
| // Fail when encountering implicit rules. e.g. |
| "--werror_implicit_rules", |
| // Fail when redefining / duplicating a target. |
| "--werror_overriding_commands", |
| // Entry point. |
| "-f", "build/make/core/cleanbuild.mk", |
| "SOONG_MAKEVARS_MK=" + config.SoongMakeVarsMk(), |
| "TARGET_DEVICE_DIR=" + config.TargetDeviceDir(), |
| }, func(env *Environment) {}) |
| } |