| // 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)) | 
 | } |