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

View as plain text