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	"bytes"
19	"fmt"
20	"io/ioutil"
21	"os"
22	"path/filepath"
23	"sort"
24	"strings"
25
26	"android/soong/ui/metrics"
27)
28
29// Given a series of glob patterns, remove matching files and directories from the filesystem.
30// For example, "malware*" would remove all files and directories in the current directory that begin with "malware".
31func removeGlobs(ctx Context, globs ...string) {
32	for _, glob := range globs {
33		// Find files and directories that match this glob pattern.
34		files, err := filepath.Glob(glob)
35		if err != nil {
36			// Only possible error is ErrBadPattern
37			panic(fmt.Errorf("%q: %s", glob, err))
38		}
39
40		for _, file := range files {
41			err = os.RemoveAll(file)
42			if err != nil {
43				ctx.Fatalf("Failed to remove file %q: %v", file, err)
44			}
45		}
46	}
47}
48
49// Remove everything under the out directory. Don't remove the out directory
50// itself in case it's a symlink.
51func clean(ctx Context, config Config) {
52	removeGlobs(ctx, filepath.Join(config.OutDir(), "*"))
53	ctx.Println("Entire build directory removed.")
54}
55
56// Remove everything in the data directory.
57func dataClean(ctx Context, config Config) {
58	removeGlobs(ctx, filepath.Join(config.ProductOut(), "data", "*"))
59	ctx.Println("Entire data directory removed.")
60}
61
62// installClean deletes all of the installed files -- the intent is to remove
63// files that may no longer be installed, either because the user previously
64// installed them, or they were previously installed by default but no longer
65// are.
66//
67// This is faster than a full clean, since we're not deleting the
68// intermediates.  Instead of recompiling, we can just copy the results.
69func installClean(ctx Context, config Config) {
70	dataClean(ctx, config)
71
72	if hostCrossOutPath := config.hostCrossOut(); hostCrossOutPath != "" {
73		hostCrossOut := func(path string) string {
74			return filepath.Join(hostCrossOutPath, path)
75		}
76		removeGlobs(ctx,
77			hostCrossOut("bin"),
78			hostCrossOut("coverage"),
79			hostCrossOut("lib*"),
80			hostCrossOut("nativetest*"))
81	}
82
83	hostOutPath := config.HostOut()
84	hostOut := func(path string) string {
85		return filepath.Join(hostOutPath, path)
86	}
87
88	hostCommonOut := func(path string) string {
89		return filepath.Join(config.hostOutRoot(), "common", path)
90	}
91
92	productOutPath := config.ProductOut()
93	productOut := func(path string) string {
94		return filepath.Join(productOutPath, path)
95	}
96
97	// Host bin, frameworks, and lib* are intentionally omitted, since
98	// otherwise we'd have to rebuild any generated files created with
99	// those tools.
100	removeGlobs(ctx,
101		hostOut("apex"),
102		hostOut("obj/NOTICE_FILES"),
103		hostOut("obj/PACKAGING"),
104		hostOut("coverage"),
105		hostOut("cts"),
106		hostOut("nativetest*"),
107		hostOut("sdk"),
108		hostOut("sdk_addon"),
109		hostOut("testcases"),
110		hostOut("vts"),
111		hostOut("vts10"),
112		hostOut("vts-core"),
113		hostCommonOut("obj/PACKAGING"),
114		productOut("*.img"),
115		productOut("*.zip"),
116		productOut("android-info.txt"),
117		productOut("misc_info.txt"),
118		productOut("apex"),
119		productOut("kernel"),
120		productOut("kernel-*"),
121		productOut("data"),
122		productOut("skin"),
123		productOut("obj/NOTICE_FILES"),
124		productOut("obj/PACKAGING"),
125		productOut("ramdisk"),
126		productOut("debug_ramdisk"),
127		productOut("vendor_ramdisk"),
128		productOut("vendor_debug_ramdisk"),
129		productOut("test_harness_ramdisk"),
130		productOut("recovery"),
131		productOut("root"),
132		productOut("system"),
133		productOut("system_other"),
134		productOut("vendor"),
135		productOut("vendor_dlkm"),
136		productOut("product"),
137		productOut("system_ext"),
138		productOut("oem"),
139		productOut("obj/FAKE"),
140		productOut("breakpad"),
141		productOut("cache"),
142		productOut("coverage"),
143		productOut("installer"),
144		productOut("odm"),
145		productOut("odm_dlkm"),
146		productOut("sysloader"),
147		productOut("testcases"),
148		productOut("symbols"))
149}
150
151// Since products and build variants (unfortunately) shared the same
152// PRODUCT_OUT staging directory, things can get out of sync if different
153// build configurations are built in the same tree. This function will
154// notice when the configuration has changed and call installClean to
155// remove the files necessary to keep things consistent.
156func installCleanIfNecessary(ctx Context, config Config) {
157	configFile := config.DevicePreviousProductConfig()
158	prefix := "PREVIOUS_BUILD_CONFIG := "
159	suffix := "\n"
160	currentConfig := prefix + config.TargetProduct() + "-" + config.TargetBuildVariant() + suffix
161
162	ensureDirectoriesExist(ctx, filepath.Dir(configFile))
163
164	writeConfig := func() {
165		err := ioutil.WriteFile(configFile, []byte(currentConfig), 0666) // a+rw
166		if err != nil {
167			ctx.Fatalln("Failed to write product config:", err)
168		}
169	}
170
171	previousConfigBytes, err := ioutil.ReadFile(configFile)
172	if err != nil {
173		if os.IsNotExist(err) {
174			// Just write the new config file, no old config file to worry about.
175			writeConfig()
176			return
177		} else {
178			ctx.Fatalln("Failed to read previous product config:", err)
179		}
180	}
181
182	previousConfig := string(previousConfigBytes)
183	if previousConfig == currentConfig {
184		// Same config as before - nothing to clean.
185		return
186	}
187
188	if config.Environment().IsEnvTrue("DISABLE_AUTO_INSTALLCLEAN") {
189		ctx.Println("DISABLE_AUTO_INSTALLCLEAN is set and true; skipping auto-clean. Your tree may be in an inconsistent state.")
190		return
191	}
192
193	ctx.BeginTrace(metrics.PrimaryNinja, "installclean")
194	defer ctx.EndTrace()
195
196	previousProductAndVariant := strings.TrimPrefix(strings.TrimSuffix(previousConfig, suffix), prefix)
197	currentProductAndVariant := strings.TrimPrefix(strings.TrimSuffix(currentConfig, suffix), prefix)
198
199	ctx.Printf("Build configuration changed: %q -> %q, forcing installclean\n", previousProductAndVariant, currentProductAndVariant)
200
201	installClean(ctx, config)
202
203	writeConfig()
204}
205
206// cleanOldFiles takes an input file (with all paths relative to basePath), and removes files from
207// the filesystem if they were removed from the input file since the last execution.
208func cleanOldFiles(ctx Context, basePath, newFile string) {
209	newFile = filepath.Join(basePath, newFile)
210	oldFile := newFile + ".previous"
211
212	if _, err := os.Stat(newFile); err != nil {
213		ctx.Fatalf("Expected %q to be readable", newFile)
214	}
215
216	if _, err := os.Stat(oldFile); os.IsNotExist(err) {
217		if err := os.Rename(newFile, oldFile); err != nil {
218			ctx.Fatalf("Failed to rename file list (%q->%q): %v", newFile, oldFile, err)
219		}
220		return
221	}
222
223	var newData, oldData []byte
224	if data, err := ioutil.ReadFile(newFile); err == nil {
225		newData = data
226	} else {
227		ctx.Fatalf("Failed to read list of installable files (%q): %v", newFile, err)
228	}
229	if data, err := ioutil.ReadFile(oldFile); err == nil {
230		oldData = data
231	} else {
232		ctx.Fatalf("Failed to read list of installable files (%q): %v", oldFile, err)
233	}
234
235	// Common case: nothing has changed
236	if bytes.Equal(newData, oldData) {
237		return
238	}
239
240	var newPaths, oldPaths []string
241	newPaths = strings.Fields(string(newData))
242	oldPaths = strings.Fields(string(oldData))
243
244	// These should be mostly sorted by make already, but better make sure Go concurs
245	sort.Strings(newPaths)
246	sort.Strings(oldPaths)
247
248	for len(oldPaths) > 0 {
249		if len(newPaths) > 0 {
250			if oldPaths[0] == newPaths[0] {
251				// Same file; continue
252				newPaths = newPaths[1:]
253				oldPaths = oldPaths[1:]
254				continue
255			} else if oldPaths[0] > newPaths[0] {
256				// New file; ignore
257				newPaths = newPaths[1:]
258				continue
259			}
260		}
261
262		// File only exists in the old list; remove if it exists
263		oldPath := filepath.Join(basePath, oldPaths[0])
264		oldPaths = oldPaths[1:]
265
266		if oldFile, err := os.Stat(oldPath); err == nil {
267			if oldFile.IsDir() {
268				if err := os.Remove(oldPath); err == nil {
269					ctx.Println("Removed directory that is no longer installed: ", oldPath)
270					cleanEmptyDirs(ctx, filepath.Dir(oldPath))
271				} else {
272					ctx.Println("Failed to remove directory that is no longer installed (%q): %v", oldPath, err)
273					ctx.Println("It's recommended to run `m installclean`")
274				}
275			} else {
276				// Removing a file, not a directory.
277				if err := os.Remove(oldPath); err == nil {
278					ctx.Println("Removed file that is no longer installed: ", oldPath)
279					cleanEmptyDirs(ctx, filepath.Dir(oldPath))
280				} else if !os.IsNotExist(err) {
281					ctx.Fatalf("Failed to remove file that is no longer installed (%q): %v", oldPath, err)
282				}
283			}
284		}
285	}
286
287	// Use the new list as the base for the next build
288	os.Rename(newFile, oldFile)
289}
290
291// cleanEmptyDirs will delete a directory if it contains no files.
292// If a deletion occurs, then it also recurses upwards to try and delete empty parent directories.
293func cleanEmptyDirs(ctx Context, dir string) {
294	files, err := ioutil.ReadDir(dir)
295	if err != nil {
296		ctx.Println("Could not read directory while trying to clean empty dirs: ", dir)
297		return
298	}
299	if len(files) > 0 {
300		// Directory is not empty.
301		return
302	}
303
304	if err := os.Remove(dir); err == nil {
305		ctx.Println("Removed empty directory (may no longer be installed?): ", dir)
306	} else {
307		ctx.Fatalf("Failed to remove empty directory (which may no longer be installed?) %q: (%v)", dir, err)
308	}
309
310	// Try and delete empty parent directories too.
311	cleanEmptyDirs(ctx, filepath.Dir(dir))
312}
313