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  	moduleLoaderState := modload.NewState()
   124  	modload.InitWorkfile(moduleLoaderState)
   125  	if len(args) > 0 {
   126  		cacheFlag := ""
   127  		switch {
   128  		case cleanCache:
   129  			cacheFlag = "-cache"
   130  		case cleanTestcache:
   131  			cacheFlag = "-testcache"
   132  		case cleanFuzzcache:
   133  			cacheFlag = "-fuzzcache"
   134  		case cleanModcache:
   135  			cacheFlag = "-modcache"
   136  		}
   137  		if cacheFlag != "" {
   138  			base.Fatalf("go: clean %s cannot be used with package arguments", cacheFlag)
   139  		}
   140  	}
   141  
   142  	// golang.org/issue/29925: only load packages before cleaning if
   143  	// either the flags and arguments explicitly imply a package,
   144  	// or no other target (such as a cache) was requested to be cleaned.
   145  	cleanPkg := len(args) > 0 || cleanI || cleanR
   146  	if (!modload.Enabled(moduleLoaderState) || modload.HasModRoot(moduleLoaderState)) &&
   147  		!cleanCache && !cleanModcache && !cleanTestcache && !cleanFuzzcache {
   148  		cleanPkg = true
   149  	}
   150  
   151  	if cleanPkg {
   152  		for _, pkg := range load.PackagesAndErrors(moduleLoaderState, ctx, load.PackageOpts{}, args) {
   153  			clean(pkg)
   154  		}
   155  	}
   156  
   157  	sh := work.NewShell("", &load.TextPrinter{Writer: os.Stdout})
   158  
   159  	if cleanCache {
   160  		dir, _, err := cache.DefaultDir()
   161  		if err != nil {
   162  			base.Fatal(err)
   163  		}
   164  		if dir != "off" {
   165  			// Remove the cache subdirectories but not the top cache directory.
   166  			// The top cache directory may have been created with special permissions
   167  			// and not something that we want to remove. Also, we'd like to preserve
   168  			// the access log for future analysis, even if the cache is cleared.
   169  			subdirs, _ := filepath.Glob(filepath.Join(str.QuoteGlob(dir), "[0-9a-f][0-9a-f]"))
   170  			printedErrors := false
   171  			if len(subdirs) > 0 {
   172  				if err := sh.RemoveAll(subdirs...); err != nil && !printedErrors {
   173  					printedErrors = true
   174  					base.Error(err)
   175  				}
   176  			}
   177  
   178  			logFile := filepath.Join(dir, "log.txt")
   179  			if err := sh.RemoveAll(logFile); err != nil && !printedErrors {
   180  				printedErrors = true
   181  				base.Error(err)
   182  			}
   183  		}
   184  	}
   185  
   186  	if cleanTestcache && !cleanCache {
   187  		// Instead of walking through the entire cache looking for test results,
   188  		// we write a file to the cache indicating that all test results from before
   189  		// right now are to be ignored.
   190  		dir, _, err := cache.DefaultDir()
   191  		if err != nil {
   192  			base.Fatal(err)
   193  		}
   194  		if dir != "off" {
   195  			f, err := lockedfile.Edit(filepath.Join(dir, "testexpire.txt"))
   196  			if err == nil {
   197  				now := time.Now().UnixNano()
   198  				buf, _ := io.ReadAll(f)
   199  				prev, _ := strconv.ParseInt(strings.TrimSpace(string(buf)), 10, 64)
   200  				if now > prev {
   201  					if err = f.Truncate(0); err == nil {
   202  						if _, err = f.Seek(0, 0); err == nil {
   203  							_, err = fmt.Fprintf(f, "%d\n", now)
   204  						}
   205  					}
   206  				}
   207  				if closeErr := f.Close(); err == nil {
   208  					err = closeErr
   209  				}
   210  			}
   211  			if err != nil {
   212  				if _, statErr := os.Stat(dir); !os.IsNotExist(statErr) {
   213  					base.Error(err)
   214  				}
   215  			}
   216  		}
   217  	}
   218  
   219  	if cleanModcache {
   220  		if cfg.GOMODCACHE == "" {
   221  			base.Fatalf("go: cannot clean -modcache without a module cache")
   222  		}
   223  		if cfg.BuildN || cfg.BuildX {
   224  			sh.ShowCmd("", "rm -rf %s", cfg.GOMODCACHE)
   225  		}
   226  		if !cfg.BuildN {
   227  			if err := modfetch.RemoveAll(cfg.GOMODCACHE); err != nil {
   228  				base.Error(err)
   229  
   230  				// Add extra logging for the purposes of debugging #68087.
   231  				// We're getting ENOTEMPTY errors on openbsd from RemoveAll.
   232  				// Check for os.ErrExist, which can match syscall.ENOTEMPTY
   233  				// and syscall.EEXIST, because syscall.ENOTEMPTY is not defined
   234  				// on all platforms.
   235  				if runtime.GOOS == "openbsd" && errors.Is(err, fs.ErrExist) {
   236  					logFilesInGOMODCACHE()
   237  				}
   238  			}
   239  		}
   240  	}
   241  
   242  	if cleanFuzzcache {
   243  		fuzzDir := cache.Default().FuzzDir()
   244  		if err := sh.RemoveAll(fuzzDir); err != nil {
   245  			base.Error(err)
   246  		}
   247  	}
   248  }
   249  
   250  // logFilesInGOMODCACHE reports the file names and modes for the files in GOMODCACHE using base.Error.
   251  func logFilesInGOMODCACHE() {
   252  	var found []string
   253  	werr := filepath.WalkDir(cfg.GOMODCACHE, func(path string, d fs.DirEntry, err error) error {
   254  		if err != nil {
   255  			return err
   256  		}
   257  		var mode string
   258  		info, err := d.Info()
   259  		if err == nil {
   260  			mode = info.Mode().String()
   261  		} else {
   262  			mode = fmt.Sprintf("<err: %s>", info.Mode())
   263  		}
   264  		found = append(found, fmt.Sprintf("%s (mode: %s)", path, mode))
   265  		return nil
   266  	})
   267  	if werr != nil {
   268  		base.Errorf("walking files in GOMODCACHE (for debugging go.dev/issue/68087): %v", werr)
   269  	}
   270  	base.Errorf("files in GOMODCACHE (for debugging go.dev/issue/68087):\n%s", strings.Join(found, "\n"))
   271  }
   272  
   273  var cleaned = map[*load.Package]bool{}
   274  
   275  // TODO: These are dregs left by Makefile-based builds.
   276  // Eventually, can stop deleting these.
   277  var cleanDir = map[string]bool{
   278  	"_test": true,
   279  	"_obj":  true,
   280  }
   281  
   282  var cleanFile = map[string]bool{
   283  	"_testmain.go": true,
   284  	"test.out":     true,
   285  	"build.out":    true,
   286  	"a.out":        true,
   287  }
   288  
   289  var cleanExt = map[string]bool{
   290  	".5":  true,
   291  	".6":  true,
   292  	".8":  true,
   293  	".a":  true,
   294  	".o":  true,
   295  	".so": true,
   296  }
   297  
   298  func clean(p *load.Package) {
   299  	if cleaned[p] {
   300  		return
   301  	}
   302  	cleaned[p] = true
   303  
   304  	if p.Dir == "" {
   305  		base.Errorf("%v", p.Error)
   306  		return
   307  	}
   308  	dirs, err := os.ReadDir(p.Dir)
   309  	if err != nil {
   310  		base.Errorf("go: %s: %v", p.Dir, err)
   311  		return
   312  	}
   313  
   314  	sh := work.NewShell("", &load.TextPrinter{Writer: os.Stdout})
   315  
   316  	packageFile := map[string]bool{}
   317  	if p.Name != "main" {
   318  		// Record which files are not in package main.
   319  		// The others are.
   320  		keep := func(list []string) {
   321  			for _, f := range list {
   322  				packageFile[f] = true
   323  			}
   324  		}
   325  		keep(p.GoFiles)
   326  		keep(p.CgoFiles)
   327  		keep(p.TestGoFiles)
   328  		keep(p.XTestGoFiles)
   329  	}
   330  
   331  	_, elem := filepath.Split(p.Dir)
   332  	var allRemove []string
   333  
   334  	// Remove dir-named executable only if this is package main.
   335  	if p.Name == "main" {
   336  		allRemove = append(allRemove,
   337  			elem,
   338  			elem+".exe",
   339  			p.DefaultExecName(),
   340  			p.DefaultExecName()+".exe",
   341  		)
   342  	}
   343  
   344  	// Remove package test executables.
   345  	allRemove = append(allRemove,
   346  		elem+".test",
   347  		elem+".test.exe",
   348  		p.DefaultExecName()+".test",
   349  		p.DefaultExecName()+".test.exe",
   350  	)
   351  
   352  	// Remove a potential executable, test executable for each .go file in the directory that
   353  	// is not part of the directory's package.
   354  	for _, dir := range dirs {
   355  		name := dir.Name()
   356  		if packageFile[name] {
   357  			continue
   358  		}
   359  
   360  		if dir.IsDir() {
   361  			continue
   362  		}
   363  
   364  		if base, found := strings.CutSuffix(name, "_test.go"); found {
   365  			allRemove = append(allRemove, base+".test", base+".test.exe")
   366  		}
   367  
   368  		if base, found := strings.CutSuffix(name, ".go"); found {
   369  			// TODO(adg,rsc): check that this .go file is actually
   370  			// in "package main", and therefore capable of building
   371  			// to an executable file.
   372  			allRemove = append(allRemove, base, base+".exe")
   373  		}
   374  	}
   375  
   376  	if cfg.BuildN || cfg.BuildX {
   377  		sh.ShowCmd(p.Dir, "rm -f %s", strings.Join(allRemove, " "))
   378  	}
   379  
   380  	toRemove := map[string]bool{}
   381  	for _, name := range allRemove {
   382  		toRemove[name] = true
   383  	}
   384  	for _, dir := range dirs {
   385  		name := dir.Name()
   386  		if dir.IsDir() {
   387  			// TODO: Remove once Makefiles are forgotten.
   388  			if cleanDir[name] {
   389  				if err := sh.RemoveAll(filepath.Join(p.Dir, name)); err != nil {
   390  					base.Error(err)
   391  				}
   392  			}
   393  			continue
   394  		}
   395  
   396  		if cfg.BuildN {
   397  			continue
   398  		}
   399  
   400  		if cleanFile[name] || cleanExt[filepath.Ext(name)] || toRemove[name] {
   401  			removeFile(filepath.Join(p.Dir, name))
   402  		}
   403  	}
   404  
   405  	if cleanI && p.Target != "" {
   406  		if cfg.BuildN || cfg.BuildX {
   407  			sh.ShowCmd("", "rm -f %s", p.Target)
   408  		}
   409  		if !cfg.BuildN {
   410  			removeFile(p.Target)
   411  		}
   412  	}
   413  
   414  	if cleanR {
   415  		for _, p1 := range p.Internal.Imports {
   416  			clean(p1)
   417  		}
   418  	}
   419  }
   420  
   421  // removeFile tries to remove file f, if error other than file doesn't exist
   422  // occurs, it will report the error.
   423  func removeFile(f string) {
   424  	err := os.Remove(f)
   425  	if err == nil || os.IsNotExist(err) {
   426  		return
   427  	}
   428  	// Windows does not allow deletion of a binary file while it is executing.
   429  	if runtime.GOOS == "windows" {
   430  		// Remove lingering ~ file from last attempt.
   431  		if _, err2 := os.Stat(f + "~"); err2 == nil {
   432  			os.Remove(f + "~")
   433  		}
   434  		// Try to move it out of the way. If the move fails,
   435  		// which is likely, we'll try again the
   436  		// next time we do an install of this binary.
   437  		if err2 := os.Rename(f, f+"~"); err2 == nil {
   438  			os.Remove(f + "~")
   439  			return
   440  		}
   441  	}
   442  	base.Error(err)
   443  }
   444  

View as plain text