Source file src/cmd/go/internal/clean/clean.go

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

View as plain text