1// Copyright 2012 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package clean implements the “go clean” command.
6package clean
7
8import (
9	"context"
10	"fmt"
11	"io"
12	"os"
13	"path/filepath"
14	"runtime"
15	"strconv"
16	"strings"
17	"time"
18
19	"cmd/go/internal/base"
20	"cmd/go/internal/cache"
21	"cmd/go/internal/cfg"
22	"cmd/go/internal/load"
23	"cmd/go/internal/lockedfile"
24	"cmd/go/internal/modfetch"
25	"cmd/go/internal/modload"
26	"cmd/go/internal/str"
27	"cmd/go/internal/work"
28)
29
30var CmdClean = &base.Command{
31	UsageLine: "go clean [-i] [-r] [-cache] [-testcache] [-modcache] [-fuzzcache] [build flags] [packages]",
32	Short:     "remove object files and cached files",
33	Long: `
34Clean removes object files from package source directories.
35The go command builds most objects in a temporary directory,
36so go clean is mainly concerned with object files left by other
37tools or by manual invocations of go build.
38
39If a package argument is given or the -i or -r flag is set,
40clean removes the following files from each of the
41source directories corresponding to the import paths:
42
43	_obj/            old object directory, left from Makefiles
44	_test/           old test directory, left from Makefiles
45	_testmain.go     old gotest file, left from Makefiles
46	test.out         old test log, left from Makefiles
47	build.out        old test log, left from Makefiles
48	*.[568ao]        object files, left from Makefiles
49
50	DIR(.exe)        from go build
51	DIR.test(.exe)   from go test -c
52	MAINFILE(.exe)   from go build MAINFILE.go
53	*.so             from SWIG
54
55In the list, DIR represents the final path element of the
56directory, and MAINFILE is the base name of any Go source
57file in the directory that is not included when building
58the package.
59
60The -i flag causes clean to remove the corresponding installed
61archive or binary (what 'go install' would create).
62
63The -n flag causes clean to print the remove commands it would execute,
64but not run them.
65
66The -r flag causes clean to be applied recursively to all the
67dependencies of the packages named by the import paths.
68
69The -x flag causes clean to print remove commands as it executes them.
70
71The -cache flag causes clean to remove the entire go build cache.
72
73The -testcache flag causes clean to expire all test results in the
74go build cache.
75
76The -modcache flag causes clean to remove the entire module
77download cache, including unpacked source code of versioned
78dependencies.
79
80The -fuzzcache flag causes clean to remove files stored in the Go build
81cache for fuzz testing. The fuzzing engine caches files that expand
82code coverage, so removing them may make fuzzing less effective until
83new inputs are found that provide the same coverage. These files are
84distinct from those stored in testdata directory; clean does not remove
85those files.
86
87For more about build flags, see 'go help build'.
88
89For more about specifying packages, see 'go help packages'.
90	`,
91}
92
93var (
94	cleanI         bool // clean -i flag
95	cleanR         bool // clean -r flag
96	cleanCache     bool // clean -cache flag
97	cleanFuzzcache bool // clean -fuzzcache flag
98	cleanModcache  bool // clean -modcache flag
99	cleanTestcache bool // clean -testcache flag
100)
101
102func init() {
103	// break init cycle
104	CmdClean.Run = runClean
105
106	CmdClean.Flag.BoolVar(&cleanI, "i", false, "")
107	CmdClean.Flag.BoolVar(&cleanR, "r", false, "")
108	CmdClean.Flag.BoolVar(&cleanCache, "cache", false, "")
109	CmdClean.Flag.BoolVar(&cleanFuzzcache, "fuzzcache", false, "")
110	CmdClean.Flag.BoolVar(&cleanModcache, "modcache", false, "")
111	CmdClean.Flag.BoolVar(&cleanTestcache, "testcache", false, "")
112
113	// -n and -x are important enough to be
114	// mentioned explicitly in the docs but they
115	// are part of the build flags.
116
117	work.AddBuildFlags(CmdClean, work.DefaultBuildFlags)
118}
119
120func runClean(ctx context.Context, cmd *base.Command, args []string) {
121	if len(args) > 0 {
122		cacheFlag := ""
123		switch {
124		case cleanCache:
125			cacheFlag = "-cache"
126		case cleanTestcache:
127			cacheFlag = "-testcache"
128		case cleanFuzzcache:
129			cacheFlag = "-fuzzcache"
130		case cleanModcache:
131			cacheFlag = "-modcache"
132		}
133		if cacheFlag != "" {
134			base.Fatalf("go: clean %s cannot be used with package arguments", cacheFlag)
135		}
136	}
137
138	// golang.org/issue/29925: only load packages before cleaning if
139	// either the flags and arguments explicitly imply a package,
140	// or no other target (such as a cache) was requested to be cleaned.
141	cleanPkg := len(args) > 0 || cleanI || cleanR
142	if (!modload.Enabled() || modload.HasModRoot()) &&
143		!cleanCache && !cleanModcache && !cleanTestcache && !cleanFuzzcache {
144		cleanPkg = true
145	}
146
147	if cleanPkg {
148		for _, pkg := range load.PackagesAndErrors(ctx, load.PackageOpts{}, args) {
149			clean(pkg)
150		}
151	}
152
153	sh := work.NewShell("", fmt.Print)
154
155	if cleanCache {
156		dir, _ := cache.DefaultDir()
157		if dir != "off" {
158			// Remove the cache subdirectories but not the top cache directory.
159			// The top cache directory may have been created with special permissions
160			// and not something that we want to remove. Also, we'd like to preserve
161			// the access log for future analysis, even if the cache is cleared.
162			subdirs, _ := filepath.Glob(filepath.Join(str.QuoteGlob(dir), "[0-9a-f][0-9a-f]"))
163			printedErrors := false
164			if len(subdirs) > 0 {
165				if err := sh.RemoveAll(subdirs...); err != nil && !printedErrors {
166					printedErrors = true
167					base.Error(err)
168				}
169			}
170
171			logFile := filepath.Join(dir, "log.txt")
172			if err := sh.RemoveAll(logFile); err != nil && !printedErrors {
173				printedErrors = true
174				base.Error(err)
175			}
176		}
177	}
178
179	if cleanTestcache && !cleanCache {
180		// Instead of walking through the entire cache looking for test results,
181		// we write a file to the cache indicating that all test results from before
182		// right now are to be ignored.
183		dir, _ := cache.DefaultDir()
184		if dir != "off" {
185			f, err := lockedfile.Edit(filepath.Join(dir, "testexpire.txt"))
186			if err == nil {
187				now := time.Now().UnixNano()
188				buf, _ := io.ReadAll(f)
189				prev, _ := strconv.ParseInt(strings.TrimSpace(string(buf)), 10, 64)
190				if now > prev {
191					if err = f.Truncate(0); err == nil {
192						if _, err = f.Seek(0, 0); err == nil {
193							_, err = fmt.Fprintf(f, "%d\n", now)
194						}
195					}
196				}
197				if closeErr := f.Close(); err == nil {
198					err = closeErr
199				}
200			}
201			if err != nil {
202				if _, statErr := os.Stat(dir); !os.IsNotExist(statErr) {
203					base.Error(err)
204				}
205			}
206		}
207	}
208
209	if cleanModcache {
210		if cfg.GOMODCACHE == "" {
211			base.Fatalf("go: cannot clean -modcache without a module cache")
212		}
213		if cfg.BuildN || cfg.BuildX {
214			sh.ShowCmd("", "rm -rf %s", cfg.GOMODCACHE)
215		}
216		if !cfg.BuildN {
217			if err := modfetch.RemoveAll(cfg.GOMODCACHE); err != nil {
218				base.Error(err)
219			}
220		}
221	}
222
223	if cleanFuzzcache {
224		fuzzDir := cache.Default().FuzzDir()
225		if err := sh.RemoveAll(fuzzDir); err != nil {
226			base.Error(err)
227		}
228	}
229}
230
231var cleaned = map[*load.Package]bool{}
232
233// TODO: These are dregs left by Makefile-based builds.
234// Eventually, can stop deleting these.
235var cleanDir = map[string]bool{
236	"_test": true,
237	"_obj":  true,
238}
239
240var cleanFile = map[string]bool{
241	"_testmain.go": true,
242	"test.out":     true,
243	"build.out":    true,
244	"a.out":        true,
245}
246
247var cleanExt = map[string]bool{
248	".5":  true,
249	".6":  true,
250	".8":  true,
251	".a":  true,
252	".o":  true,
253	".so": true,
254}
255
256func clean(p *load.Package) {
257	if cleaned[p] {
258		return
259	}
260	cleaned[p] = true
261
262	if p.Dir == "" {
263		base.Errorf("%v", p.Error)
264		return
265	}
266	dirs, err := os.ReadDir(p.Dir)
267	if err != nil {
268		base.Errorf("go: %s: %v", p.Dir, err)
269		return
270	}
271
272	sh := work.NewShell("", fmt.Print)
273
274	packageFile := map[string]bool{}
275	if p.Name != "main" {
276		// Record which files are not in package main.
277		// The others are.
278		keep := func(list []string) {
279			for _, f := range list {
280				packageFile[f] = true
281			}
282		}
283		keep(p.GoFiles)
284		keep(p.CgoFiles)
285		keep(p.TestGoFiles)
286		keep(p.XTestGoFiles)
287	}
288
289	_, elem := filepath.Split(p.Dir)
290	var allRemove []string
291
292	// Remove dir-named executable only if this is package main.
293	if p.Name == "main" {
294		allRemove = append(allRemove,
295			elem,
296			elem+".exe",
297			p.DefaultExecName(),
298			p.DefaultExecName()+".exe",
299		)
300	}
301
302	// Remove package test executables.
303	allRemove = append(allRemove,
304		elem+".test",
305		elem+".test.exe",
306		p.DefaultExecName()+".test",
307		p.DefaultExecName()+".test.exe",
308	)
309
310	// Remove a potential executable, test executable for each .go file in the directory that
311	// is not part of the directory's package.
312	for _, dir := range dirs {
313		name := dir.Name()
314		if packageFile[name] {
315			continue
316		}
317
318		if dir.IsDir() {
319			continue
320		}
321
322		if base, found := strings.CutSuffix(name, "_test.go"); found {
323			allRemove = append(allRemove, base+".test", base+".test.exe")
324		}
325
326		if base, found := strings.CutSuffix(name, ".go"); found {
327			// TODO(adg,rsc): check that this .go file is actually
328			// in "package main", and therefore capable of building
329			// to an executable file.
330			allRemove = append(allRemove, base, base+".exe")
331		}
332	}
333
334	if cfg.BuildN || cfg.BuildX {
335		sh.ShowCmd(p.Dir, "rm -f %s", strings.Join(allRemove, " "))
336	}
337
338	toRemove := map[string]bool{}
339	for _, name := range allRemove {
340		toRemove[name] = true
341	}
342	for _, dir := range dirs {
343		name := dir.Name()
344		if dir.IsDir() {
345			// TODO: Remove once Makefiles are forgotten.
346			if cleanDir[name] {
347				if err := sh.RemoveAll(filepath.Join(p.Dir, name)); err != nil {
348					base.Error(err)
349				}
350			}
351			continue
352		}
353
354		if cfg.BuildN {
355			continue
356		}
357
358		if cleanFile[name] || cleanExt[filepath.Ext(name)] || toRemove[name] {
359			removeFile(filepath.Join(p.Dir, name))
360		}
361	}
362
363	if cleanI && p.Target != "" {
364		if cfg.BuildN || cfg.BuildX {
365			sh.ShowCmd("", "rm -f %s", p.Target)
366		}
367		if !cfg.BuildN {
368			removeFile(p.Target)
369		}
370	}
371
372	if cleanR {
373		for _, p1 := range p.Internal.Imports {
374			clean(p1)
375		}
376	}
377}
378
379// removeFile tries to remove file f, if error other than file doesn't exist
380// occurs, it will report the error.
381func removeFile(f string) {
382	err := os.Remove(f)
383	if err == nil || os.IsNotExist(err) {
384		return
385	}
386	// Windows does not allow deletion of a binary file while it is executing.
387	if runtime.GOOS == "windows" {
388		// Remove lingering ~ file from last attempt.
389		if _, err2 := os.Stat(f + "~"); err2 == nil {
390			os.Remove(f + "~")
391		}
392		// Try to move it out of the way. If the move fails,
393		// which is likely, we'll try again the
394		// next time we do an install of this binary.
395		if err2 := os.Rename(f, f+"~"); err2 == nil {
396			os.Remove(f + "~")
397			return
398		}
399	}
400	base.Error(err)
401}
402