|  | // 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 ( | 
|  | "bytes" | 
|  | "fmt" | 
|  | "io/ioutil" | 
|  | "os" | 
|  | "path/filepath" | 
|  | "sort" | 
|  | "strings" | 
|  |  | 
|  | "android/soong/ui/metrics" | 
|  | ) | 
|  |  | 
|  | // Given a series of glob patterns, remove matching files and directories from the filesystem. | 
|  | // For example, "malware*" would remove all files and directories in the current directory that begin with "malware". | 
|  | func removeGlobs(ctx Context, globs ...string) { | 
|  | for _, glob := range globs { | 
|  | // Find files and directories that match this glob pattern. | 
|  | files, err := filepath.Glob(glob) | 
|  | if err != nil { | 
|  | // Only possible error is ErrBadPattern | 
|  | panic(fmt.Errorf("%q: %s", glob, err)) | 
|  | } | 
|  |  | 
|  | for _, file := range files { | 
|  | err = os.RemoveAll(file) | 
|  | if err != nil { | 
|  | ctx.Fatalf("Failed to remove file %q: %v", file, err) | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // Based on https://stackoverflow.com/questions/28969455/how-to-properly-instantiate-os-filemode | 
|  | // Because Go doesn't provide a nice way to set bits on a filemode | 
|  | const ( | 
|  | FILEMODE_READ         = 04 | 
|  | FILEMODE_WRITE        = 02 | 
|  | FILEMODE_EXECUTE      = 01 | 
|  | FILEMODE_USER_SHIFT   = 6 | 
|  | FILEMODE_USER_READ    = FILEMODE_READ << FILEMODE_USER_SHIFT | 
|  | FILEMODE_USER_WRITE   = FILEMODE_WRITE << FILEMODE_USER_SHIFT | 
|  | FILEMODE_USER_EXECUTE = FILEMODE_EXECUTE << FILEMODE_USER_SHIFT | 
|  | ) | 
|  |  | 
|  | // Remove everything under the out directory. Don't remove the out directory | 
|  | // itself in case it's a symlink. | 
|  | func clean(ctx Context, config Config) { | 
|  | removeGlobs(ctx, filepath.Join(config.OutDir(), "*")) | 
|  | ctx.Println("Entire build directory removed.") | 
|  | } | 
|  |  | 
|  | // Remove everything in the data directory. | 
|  | func dataClean(ctx Context, config Config) { | 
|  | removeGlobs(ctx, filepath.Join(config.ProductOut(), "data", "*")) | 
|  | ctx.Println("Entire data directory removed.") | 
|  | } | 
|  |  | 
|  | // installClean deletes all of the installed files -- the intent is to remove | 
|  | // files that may no longer be installed, either because the user previously | 
|  | // installed them, or they were previously installed by default but no longer | 
|  | // are. | 
|  | // | 
|  | // This is faster than a full clean, since we're not deleting the | 
|  | // intermediates.  Instead of recompiling, we can just copy the results. | 
|  | func installClean(ctx Context, config Config) { | 
|  | dataClean(ctx, config) | 
|  |  | 
|  | if hostCrossOutPath := config.hostCrossOut(); hostCrossOutPath != "" { | 
|  | hostCrossOut := func(path string) string { | 
|  | return filepath.Join(hostCrossOutPath, path) | 
|  | } | 
|  | removeGlobs(ctx, | 
|  | hostCrossOut("bin"), | 
|  | hostCrossOut("coverage"), | 
|  | hostCrossOut("lib*"), | 
|  | hostCrossOut("nativetest*")) | 
|  | } | 
|  |  | 
|  | hostOutPath := config.HostOut() | 
|  | hostOut := func(path string) string { | 
|  | return filepath.Join(hostOutPath, path) | 
|  | } | 
|  |  | 
|  | hostCommonOut := func(path string) string { | 
|  | return filepath.Join(config.hostOutRoot(), "common", path) | 
|  | } | 
|  |  | 
|  | productOutPath := config.ProductOut() | 
|  | productOut := func(path string) string { | 
|  | return filepath.Join(productOutPath, path) | 
|  | } | 
|  |  | 
|  | // Host bin, frameworks, and lib* are intentionally omitted, since | 
|  | // otherwise we'd have to rebuild any generated files created with | 
|  | // those tools. | 
|  | removeGlobs(ctx, | 
|  | hostOut("apex"), | 
|  | hostOut("obj/NOTICE_FILES"), | 
|  | hostOut("obj/PACKAGING"), | 
|  | hostOut("coverage"), | 
|  | hostOut("cts"), | 
|  | hostOut("nativetest*"), | 
|  | hostOut("sdk"), | 
|  | hostOut("sdk_addon"), | 
|  | hostOut("testcases"), | 
|  | hostOut("vts"), | 
|  | hostOut("vts10"), | 
|  | hostOut("vts-core"), | 
|  | hostCommonOut("obj/PACKAGING"), | 
|  | productOut("*.img"), | 
|  | productOut("*.zip"), | 
|  | productOut("android-info.txt"), | 
|  | productOut("misc_info.txt"), | 
|  | productOut("apex"), | 
|  | productOut("kernel"), | 
|  | productOut("kernel-*"), | 
|  | productOut("data"), | 
|  | productOut("skin"), | 
|  | productOut("obj/NOTICE_FILES"), | 
|  | productOut("obj/PACKAGING"), | 
|  | productOut("ramdisk"), | 
|  | productOut("ramdisk_16k"), | 
|  | productOut("debug_ramdisk"), | 
|  | productOut("vendor_ramdisk"), | 
|  | productOut("vendor_debug_ramdisk"), | 
|  | productOut("vendor_kernel_ramdisk"), | 
|  | productOut("test_harness_ramdisk"), | 
|  | productOut("recovery"), | 
|  | productOut("root"), | 
|  | productOut("system"), | 
|  | productOut("system_dlkm"), | 
|  | productOut("system_other"), | 
|  | productOut("vendor"), | 
|  | productOut("vendor_dlkm"), | 
|  | productOut("product"), | 
|  | productOut("system_ext"), | 
|  | productOut("oem"), | 
|  | productOut("obj/FAKE"), | 
|  | productOut("breakpad"), | 
|  | productOut("cache"), | 
|  | productOut("coverage"), | 
|  | productOut("installer"), | 
|  | productOut("odm"), | 
|  | productOut("odm_dlkm"), | 
|  | productOut("sysloader"), | 
|  | productOut("testcases"), | 
|  | productOut("symbols")) | 
|  | } | 
|  |  | 
|  | // Since products and build variants (unfortunately) shared the same | 
|  | // PRODUCT_OUT staging directory, things can get out of sync if different | 
|  | // build configurations are built in the same tree. This function will | 
|  | // notice when the configuration has changed and call installClean to | 
|  | // remove the files necessary to keep things consistent. | 
|  | func installCleanIfNecessary(ctx Context, config Config) { | 
|  | configFile := config.DevicePreviousProductConfig() | 
|  | prefix := "PREVIOUS_BUILD_CONFIG := " | 
|  | suffix := "\n" | 
|  | currentConfig := prefix + config.TargetProduct() + "-" + config.TargetBuildVariant() + suffix | 
|  |  | 
|  | ensureDirectoriesExist(ctx, filepath.Dir(configFile)) | 
|  |  | 
|  | writeConfig := func() { | 
|  | err := ioutil.WriteFile(configFile, []byte(currentConfig), 0666) // a+rw | 
|  | if err != nil { | 
|  | ctx.Fatalln("Failed to write product config:", err) | 
|  | } | 
|  | } | 
|  |  | 
|  | previousConfigBytes, err := ioutil.ReadFile(configFile) | 
|  | if err != nil { | 
|  | if os.IsNotExist(err) { | 
|  | // Just write the new config file, no old config file to worry about. | 
|  | writeConfig() | 
|  | return | 
|  | } else { | 
|  | ctx.Fatalln("Failed to read previous product config:", err) | 
|  | } | 
|  | } | 
|  |  | 
|  | previousConfig := string(previousConfigBytes) | 
|  | if previousConfig == currentConfig { | 
|  | // Same config as before - nothing to clean. | 
|  | return | 
|  | } | 
|  |  | 
|  | if config.Environment().IsEnvTrue("DISABLE_AUTO_INSTALLCLEAN") { | 
|  | ctx.Println("DISABLE_AUTO_INSTALLCLEAN is set and true; skipping auto-clean. Your tree may be in an inconsistent state.") | 
|  | return | 
|  | } | 
|  |  | 
|  | ctx.BeginTrace(metrics.PrimaryNinja, "installclean") | 
|  | defer ctx.EndTrace() | 
|  |  | 
|  | previousProductAndVariant := strings.TrimPrefix(strings.TrimSuffix(previousConfig, suffix), prefix) | 
|  | currentProductAndVariant := strings.TrimPrefix(strings.TrimSuffix(currentConfig, suffix), prefix) | 
|  |  | 
|  | ctx.Printf("Build configuration changed: %q -> %q, forcing installclean\n", previousProductAndVariant, currentProductAndVariant) | 
|  |  | 
|  | installClean(ctx, config) | 
|  |  | 
|  | writeConfig() | 
|  | } | 
|  |  | 
|  | // cleanOldFiles takes an input file (with all paths relative to basePath), and removes files from | 
|  | // the filesystem if they were removed from the input file since the last execution. | 
|  | func cleanOldFiles(ctx Context, basePath, newFile string) { | 
|  | newFile = filepath.Join(basePath, newFile) | 
|  | oldFile := newFile + ".previous" | 
|  |  | 
|  | if _, err := os.Stat(newFile); os.IsNotExist(err) { | 
|  | // If the file doesn't exist, assume no installed files exist either | 
|  | return | 
|  | } else if err != nil { | 
|  | ctx.Fatalf("Expected %q to be readable", newFile) | 
|  | } | 
|  |  | 
|  | if _, err := os.Stat(oldFile); os.IsNotExist(err) { | 
|  | if err := os.Rename(newFile, oldFile); err != nil { | 
|  | ctx.Fatalf("Failed to rename file list (%q->%q): %v", newFile, oldFile, err) | 
|  | } | 
|  | return | 
|  | } | 
|  |  | 
|  | var newData, oldData []byte | 
|  | if data, err := ioutil.ReadFile(newFile); err == nil { | 
|  | newData = data | 
|  | } else { | 
|  | ctx.Fatalf("Failed to read list of installable files (%q): %v", newFile, err) | 
|  | } | 
|  | if data, err := ioutil.ReadFile(oldFile); err == nil { | 
|  | oldData = data | 
|  | } else { | 
|  | ctx.Fatalf("Failed to read list of installable files (%q): %v", oldFile, err) | 
|  | } | 
|  |  | 
|  | // Common case: nothing has changed | 
|  | if bytes.Equal(newData, oldData) { | 
|  | return | 
|  | } | 
|  |  | 
|  | var newPaths, oldPaths []string | 
|  | newPaths = strings.Fields(string(newData)) | 
|  | oldPaths = strings.Fields(string(oldData)) | 
|  |  | 
|  | // These should be mostly sorted by make already, but better make sure Go concurs | 
|  | sort.Strings(newPaths) | 
|  | sort.Strings(oldPaths) | 
|  |  | 
|  | for len(oldPaths) > 0 { | 
|  | if len(newPaths) > 0 { | 
|  | if oldPaths[0] == newPaths[0] { | 
|  | // Same file; continue | 
|  | newPaths = newPaths[1:] | 
|  | oldPaths = oldPaths[1:] | 
|  | continue | 
|  | } else if oldPaths[0] > newPaths[0] { | 
|  | // New file; ignore | 
|  | newPaths = newPaths[1:] | 
|  | continue | 
|  | } | 
|  | } | 
|  |  | 
|  | // File only exists in the old list; remove if it exists | 
|  | oldPath := filepath.Join(basePath, oldPaths[0]) | 
|  | oldPaths = oldPaths[1:] | 
|  |  | 
|  | if oldFile, err := os.Stat(oldPath); err == nil { | 
|  | if oldFile.IsDir() { | 
|  | if err := os.Remove(oldPath); err == nil { | 
|  | ctx.Println("Removed directory that is no longer installed: ", oldPath) | 
|  | cleanEmptyDirs(ctx, filepath.Dir(oldPath)) | 
|  | } else { | 
|  | ctx.Println("Failed to remove directory that is no longer installed (%q): %v", oldPath, err) | 
|  | ctx.Println("It's recommended to run `m installclean`") | 
|  | } | 
|  | } else { | 
|  | // Removing a file, not a directory. | 
|  | if err := os.Remove(oldPath); err == nil { | 
|  | ctx.Println("Removed file that is no longer installed: ", oldPath) | 
|  | cleanEmptyDirs(ctx, filepath.Dir(oldPath)) | 
|  | } else if !os.IsNotExist(err) { | 
|  | ctx.Fatalf("Failed to remove file that is no longer installed (%q): %v", oldPath, err) | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // Use the new list as the base for the next build | 
|  | os.Rename(newFile, oldFile) | 
|  | } | 
|  |  | 
|  | // cleanEmptyDirs will delete a directory if it contains no files. | 
|  | // If a deletion occurs, then it also recurses upwards to try and delete empty parent directories. | 
|  | func cleanEmptyDirs(ctx Context, dir string) { | 
|  | files, err := ioutil.ReadDir(dir) | 
|  | if err != nil { | 
|  | ctx.Println("Could not read directory while trying to clean empty dirs: ", dir) | 
|  | return | 
|  | } | 
|  | if len(files) > 0 { | 
|  | // Directory is not empty. | 
|  | return | 
|  | } | 
|  |  | 
|  | if err := os.Remove(dir); err == nil { | 
|  | ctx.Println("Removed empty directory (may no longer be installed?): ", dir) | 
|  | } else { | 
|  | ctx.Fatalf("Failed to remove empty directory (which may no longer be installed?) %q: (%v)", dir, err) | 
|  | } | 
|  |  | 
|  | // Try and delete empty parent directories too. | 
|  | cleanEmptyDirs(ctx, filepath.Dir(dir)) | 
|  | } |