github.com/golang/dep@v0.5.4/gps/prune.go (about)

     1  // Copyright 2017 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 gps
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  
    15  	"github.com/golang/dep/internal/fs"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  // PruneOptions represents the pruning options used to write the dependecy tree.
    20  type PruneOptions uint8
    21  
    22  const (
    23  	// PruneNestedVendorDirs indicates if nested vendor directories should be pruned.
    24  	PruneNestedVendorDirs PruneOptions = 1 << iota
    25  	// PruneUnusedPackages indicates if unused Go packages should be pruned.
    26  	PruneUnusedPackages
    27  	// PruneNonGoFiles indicates if non-Go files should be pruned.
    28  	// Files matching licenseFilePrefixes and legalFileSubstrings are kept in
    29  	// an attempt to comply with legal requirements.
    30  	PruneNonGoFiles
    31  	// PruneGoTestFiles indicates if Go test files should be pruned.
    32  	PruneGoTestFiles
    33  )
    34  
    35  // PruneOptionSet represents trinary distinctions for each of the types of
    36  // prune rules (as expressed via PruneOptions): nested vendor directories,
    37  // unused packages, non-go files, and go test files.
    38  //
    39  // The three-way distinction is between "none", "true", and "false", represented
    40  // by uint8 values of 0, 1, and 2, respectively.
    41  //
    42  // This trinary distinction is necessary in order to record, with full fidelity,
    43  // a cascading tree of pruning values, as expressed in CascadingPruneOptions; a
    44  // simple boolean cannot delineate between "false" and "none".
    45  type PruneOptionSet struct {
    46  	NestedVendor   uint8
    47  	UnusedPackages uint8
    48  	NonGoFiles     uint8
    49  	GoTests        uint8
    50  }
    51  
    52  // CascadingPruneOptions is a set of rules for pruning a dependency tree.
    53  //
    54  // The DefaultOptions are the global default pruning rules, expressed as a
    55  // single PruneOptions bitfield. These global rules will cascade down to
    56  // individual project rules, unless superseded.
    57  type CascadingPruneOptions struct {
    58  	DefaultOptions    PruneOptions
    59  	PerProjectOptions map[ProjectRoot]PruneOptionSet
    60  }
    61  
    62  // ParsePruneOptions extracts PruneOptions from a string using the standard
    63  // encoding.
    64  func ParsePruneOptions(input string) (PruneOptions, error) {
    65  	var po PruneOptions
    66  	for _, char := range input {
    67  		switch char {
    68  		case 'T':
    69  			po |= PruneGoTestFiles
    70  		case 'U':
    71  			po |= PruneUnusedPackages
    72  		case 'N':
    73  			po |= PruneNonGoFiles
    74  		case 'V':
    75  			po |= PruneNestedVendorDirs
    76  		default:
    77  			return 0, errors.Errorf("unknown pruning code %q", char)
    78  		}
    79  	}
    80  
    81  	return po, nil
    82  }
    83  
    84  func (po PruneOptions) String() string {
    85  	var buf bytes.Buffer
    86  
    87  	if po&PruneNonGoFiles != 0 {
    88  		fmt.Fprintf(&buf, "N")
    89  	}
    90  	if po&PruneUnusedPackages != 0 {
    91  		fmt.Fprintf(&buf, "U")
    92  	}
    93  	if po&PruneGoTestFiles != 0 {
    94  		fmt.Fprintf(&buf, "T")
    95  	}
    96  	if po&PruneNestedVendorDirs != 0 {
    97  		fmt.Fprintf(&buf, "V")
    98  	}
    99  
   100  	return buf.String()
   101  }
   102  
   103  // PruneOptionsFor returns the PruneOptions bits for the given project,
   104  // indicating which pruning rules should be applied to the project's code.
   105  //
   106  // It computes the cascade from default to project-specific options (if any) on
   107  // the fly.
   108  func (o CascadingPruneOptions) PruneOptionsFor(pr ProjectRoot) PruneOptions {
   109  	po, has := o.PerProjectOptions[pr]
   110  	if !has {
   111  		return o.DefaultOptions
   112  	}
   113  
   114  	ops := o.DefaultOptions
   115  	if po.NestedVendor != 0 {
   116  		if po.NestedVendor == 1 {
   117  			ops |= PruneNestedVendorDirs
   118  		} else {
   119  			ops &^= PruneNestedVendorDirs
   120  		}
   121  	}
   122  
   123  	if po.UnusedPackages != 0 {
   124  		if po.UnusedPackages == 1 {
   125  			ops |= PruneUnusedPackages
   126  		} else {
   127  			ops &^= PruneUnusedPackages
   128  		}
   129  	}
   130  
   131  	if po.NonGoFiles != 0 {
   132  		if po.NonGoFiles == 1 {
   133  			ops |= PruneNonGoFiles
   134  		} else {
   135  			ops &^= PruneNonGoFiles
   136  		}
   137  	}
   138  
   139  	if po.GoTests != 0 {
   140  		if po.GoTests == 1 {
   141  			ops |= PruneGoTestFiles
   142  		} else {
   143  			ops &^= PruneGoTestFiles
   144  		}
   145  	}
   146  
   147  	return ops
   148  }
   149  
   150  func defaultCascadingPruneOptions() CascadingPruneOptions {
   151  	return CascadingPruneOptions{
   152  		DefaultOptions:    PruneNestedVendorDirs,
   153  		PerProjectOptions: map[ProjectRoot]PruneOptionSet{},
   154  	}
   155  }
   156  
   157  var (
   158  	// licenseFilePrefixes is a list of name prefixes for license files.
   159  	licenseFilePrefixes = []string{
   160  		"license",
   161  		"licence",
   162  		"copying",
   163  		"unlicense",
   164  		"copyright",
   165  		"copyleft",
   166  	}
   167  	// legalFileSubstrings contains substrings that are likey part of a legal
   168  	// declaration file.
   169  	legalFileSubstrings = []string{
   170  		"authors",
   171  		"contributors",
   172  		"legal",
   173  		"notice",
   174  		"disclaimer",
   175  		"patent",
   176  		"third-party",
   177  		"thirdparty",
   178  	}
   179  )
   180  
   181  // PruneProject remove excess files according to the options passed, from
   182  // the lp directory in baseDir.
   183  func PruneProject(baseDir string, lp LockedProject, options PruneOptions) error {
   184  	fsState, err := deriveFilesystemState(baseDir)
   185  
   186  	if err != nil {
   187  		return errors.Wrap(err, "could not derive filesystem state")
   188  	}
   189  
   190  	if (options & PruneNestedVendorDirs) != 0 {
   191  		if err := pruneVendorDirs(fsState); err != nil {
   192  			return errors.Wrapf(err, "failed to prune nested vendor directories")
   193  		}
   194  	}
   195  
   196  	if (options & PruneUnusedPackages) != 0 {
   197  		if _, err := pruneUnusedPackages(lp, fsState); err != nil {
   198  			return errors.Wrap(err, "failed to prune unused packages")
   199  		}
   200  	}
   201  
   202  	if (options & PruneNonGoFiles) != 0 {
   203  		if err := pruneNonGoFiles(fsState); err != nil {
   204  			return errors.Wrap(err, "failed to prune non-Go files")
   205  		}
   206  	}
   207  
   208  	if (options & PruneGoTestFiles) != 0 {
   209  		if err := pruneGoTestFiles(fsState); err != nil {
   210  			return errors.Wrap(err, "failed to prune Go test files")
   211  		}
   212  	}
   213  
   214  	if err := deleteEmptyDirs(fsState); err != nil {
   215  		return errors.Wrap(err, "could not delete empty dirs")
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  // pruneVendorDirs deletes all nested vendor directories within baseDir.
   222  func pruneVendorDirs(fsState filesystemState) error {
   223  	for _, dir := range fsState.dirs {
   224  		if filepath.Base(dir) == "vendor" {
   225  			err := os.RemoveAll(filepath.Join(fsState.root, dir))
   226  			if err != nil && !os.IsNotExist(err) {
   227  				return err
   228  			}
   229  		}
   230  	}
   231  
   232  	for _, link := range fsState.links {
   233  		if filepath.Base(link.path) == "vendor" {
   234  			err := os.Remove(filepath.Join(fsState.root, link.path))
   235  			if err != nil && !os.IsNotExist(err) {
   236  				return err
   237  			}
   238  		}
   239  	}
   240  
   241  	return nil
   242  }
   243  
   244  // pruneUnusedPackages deletes unimported packages found in fsState.
   245  // Determining whether packages are imported or not is based on the passed LockedProject.
   246  func pruneUnusedPackages(lp LockedProject, fsState filesystemState) (map[string]interface{}, error) {
   247  	unusedPackages := calculateUnusedPackages(lp, fsState)
   248  	toDelete := collectUnusedPackagesFiles(fsState, unusedPackages)
   249  
   250  	for _, path := range toDelete {
   251  		if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
   252  			return nil, err
   253  		}
   254  	}
   255  
   256  	return unusedPackages, nil
   257  }
   258  
   259  // calculateUnusedPackages generates a list of unused packages in lp.
   260  func calculateUnusedPackages(lp LockedProject, fsState filesystemState) map[string]interface{} {
   261  	unused := make(map[string]interface{})
   262  	imported := make(map[string]interface{})
   263  
   264  	for _, pkg := range lp.Packages() {
   265  		imported[pkg] = nil
   266  	}
   267  
   268  	// Add the root package if it's not imported.
   269  	if _, ok := imported["."]; !ok {
   270  		unused["."] = nil
   271  	}
   272  
   273  	for _, dirPath := range fsState.dirs {
   274  		pkg := filepath.ToSlash(dirPath)
   275  
   276  		if _, ok := imported[pkg]; !ok {
   277  			unused[pkg] = nil
   278  		}
   279  	}
   280  
   281  	return unused
   282  }
   283  
   284  // collectUnusedPackagesFiles returns a slice of all files in the unused
   285  // packages based on fsState.
   286  func collectUnusedPackagesFiles(fsState filesystemState, unusedPackages map[string]interface{}) []string {
   287  	// TODO(ibrasho): is this useful?
   288  	files := make([]string, 0, len(unusedPackages))
   289  
   290  	for _, path := range fsState.files {
   291  		// Keep preserved files.
   292  		if isPreservedFile(filepath.Base(path)) {
   293  			continue
   294  		}
   295  
   296  		pkg := filepath.ToSlash(filepath.Dir(path))
   297  
   298  		if _, ok := unusedPackages[pkg]; ok {
   299  			files = append(files, filepath.Join(fsState.root, path))
   300  		}
   301  	}
   302  
   303  	return files
   304  }
   305  
   306  func isSourceFile(path string) bool {
   307  	ext := fileExt(path)
   308  
   309  	// Refer to: https://github.com/golang/go/blob/release-branch.go1.9/src/go/build/build.go#L750
   310  	switch ext {
   311  	case ".go":
   312  		return true
   313  	case ".c":
   314  		return true
   315  	case ".cc", ".cpp", ".cxx":
   316  		return true
   317  	case ".m":
   318  		return true
   319  	case ".h", ".hh", ".hpp", ".hxx":
   320  		return true
   321  	case ".f", ".F", ".for", ".f90":
   322  		return true
   323  	case ".s":
   324  		return true
   325  	case ".S":
   326  		return true
   327  	case ".swig":
   328  		return true
   329  	case ".swigcxx":
   330  		return true
   331  	case ".syso":
   332  		return true
   333  	}
   334  	return false
   335  }
   336  
   337  // pruneNonGoFiles delete all non-Go files existing in fsState.
   338  //
   339  // Files matching licenseFilePrefixes and legalFileSubstrings are not pruned.
   340  func pruneNonGoFiles(fsState filesystemState) error {
   341  	toDelete := make([]string, 0, len(fsState.files)/4)
   342  
   343  	for _, path := range fsState.files {
   344  		if isSourceFile(path) {
   345  			continue
   346  		}
   347  
   348  		// Ignore preserved files.
   349  		if isPreservedFile(filepath.Base(path)) {
   350  			continue
   351  		}
   352  
   353  		toDelete = append(toDelete, filepath.Join(fsState.root, path))
   354  	}
   355  
   356  	for _, path := range toDelete {
   357  		if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
   358  			return err
   359  		}
   360  	}
   361  
   362  	return nil
   363  }
   364  
   365  // isPreservedFile checks if the file name indicates that the file should be
   366  // preserved based on licenseFilePrefixes or legalFileSubstrings.
   367  // This applies only to non-source files.
   368  func isPreservedFile(name string) bool {
   369  	if isSourceFile(name) {
   370  		return false
   371  	}
   372  
   373  	name = strings.ToLower(name)
   374  
   375  	for _, prefix := range licenseFilePrefixes {
   376  		if strings.HasPrefix(name, prefix) {
   377  			return true
   378  		}
   379  	}
   380  
   381  	for _, substring := range legalFileSubstrings {
   382  		if strings.Contains(name, substring) {
   383  			return true
   384  		}
   385  	}
   386  
   387  	return false
   388  }
   389  
   390  // pruneGoTestFiles deletes all Go test files (*_test.go) in fsState.
   391  func pruneGoTestFiles(fsState filesystemState) error {
   392  	toDelete := make([]string, 0, len(fsState.files)/2)
   393  
   394  	for _, path := range fsState.files {
   395  		if strings.HasSuffix(path, "_test.go") {
   396  			toDelete = append(toDelete, filepath.Join(fsState.root, path))
   397  		}
   398  	}
   399  
   400  	for _, path := range toDelete {
   401  		if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
   402  			return err
   403  		}
   404  	}
   405  
   406  	return nil
   407  }
   408  
   409  func deleteEmptyDirs(fsState filesystemState) error {
   410  	sort.Sort(sort.Reverse(sort.StringSlice(fsState.dirs)))
   411  
   412  	for _, dir := range fsState.dirs {
   413  		path := filepath.Join(fsState.root, dir)
   414  
   415  		notEmpty, err := fs.IsNonEmptyDir(path)
   416  		if err != nil {
   417  			return err
   418  		}
   419  
   420  		if !notEmpty {
   421  			if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
   422  				return err
   423  			}
   424  		}
   425  	}
   426  
   427  	return nil
   428  }
   429  
   430  func fileExt(name string) string {
   431  	i := strings.LastIndex(name, ".")
   432  	if i < 0 {
   433  		return ""
   434  	}
   435  	return name[i:]
   436  }