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