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