| // Copyright 2017 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package gps |
| |
| import ( |
| "bytes" |
| "fmt" |
| "os" |
| "path/filepath" |
| "sort" |
| "strings" |
| |
| "github.com/golang/dep/internal/fs" |
| "github.com/pkg/errors" |
| ) |
| |
| // PruneOptions represents the pruning options used to write the dependecy tree. |
| type PruneOptions uint8 |
| |
| const ( |
| // PruneNestedVendorDirs indicates if nested vendor directories should be pruned. |
| PruneNestedVendorDirs PruneOptions = 1 << iota |
| // PruneUnusedPackages indicates if unused Go packages should be pruned. |
| PruneUnusedPackages |
| // PruneNonGoFiles indicates if non-Go files should be pruned. |
| // Files matching licenseFilePrefixes and legalFileSubstrings are kept in |
| // an attempt to comply with legal requirements. |
| PruneNonGoFiles |
| // PruneGoTestFiles indicates if Go test files should be pruned. |
| PruneGoTestFiles |
| ) |
| |
| // PruneOptionSet represents trinary distinctions for each of the types of |
| // prune rules (as expressed via PruneOptions): nested vendor directories, |
| // unused packages, non-go files, and go test files. |
| // |
| // The three-way distinction is between "none", "true", and "false", represented |
| // by uint8 values of 0, 1, and 2, respectively. |
| // |
| // This trinary distinction is necessary in order to record, with full fidelity, |
| // a cascading tree of pruning values, as expressed in CascadingPruneOptions; a |
| // simple boolean cannot delineate between "false" and "none". |
| type PruneOptionSet struct { |
| NestedVendor uint8 |
| UnusedPackages uint8 |
| NonGoFiles uint8 |
| GoTests uint8 |
| } |
| |
| // CascadingPruneOptions is a set of rules for pruning a dependency tree. |
| // |
| // The DefaultOptions are the global default pruning rules, expressed as a |
| // single PruneOptions bitfield. These global rules will cascade down to |
| // individual project rules, unless superseded. |
| type CascadingPruneOptions struct { |
| DefaultOptions PruneOptions |
| PerProjectOptions map[ProjectRoot]PruneOptionSet |
| } |
| |
| // ParsePruneOptions extracts PruneOptions from a string using the standard |
| // encoding. |
| func ParsePruneOptions(input string) (PruneOptions, error) { |
| var po PruneOptions |
| for _, char := range input { |
| switch char { |
| case 'T': |
| po |= PruneGoTestFiles |
| case 'U': |
| po |= PruneUnusedPackages |
| case 'N': |
| po |= PruneNonGoFiles |
| case 'V': |
| po |= PruneNestedVendorDirs |
| default: |
| return 0, errors.Errorf("unknown pruning code %q", char) |
| } |
| } |
| |
| return po, nil |
| } |
| |
| func (po PruneOptions) String() string { |
| var buf bytes.Buffer |
| |
| if po&PruneNonGoFiles != 0 { |
| fmt.Fprintf(&buf, "N") |
| } |
| if po&PruneUnusedPackages != 0 { |
| fmt.Fprintf(&buf, "U") |
| } |
| if po&PruneGoTestFiles != 0 { |
| fmt.Fprintf(&buf, "T") |
| } |
| if po&PruneNestedVendorDirs != 0 { |
| fmt.Fprintf(&buf, "V") |
| } |
| |
| return buf.String() |
| } |
| |
| // PruneOptionsFor returns the PruneOptions bits for the given project, |
| // indicating which pruning rules should be applied to the project's code. |
| // |
| // It computes the cascade from default to project-specific options (if any) on |
| // the fly. |
| func (o CascadingPruneOptions) PruneOptionsFor(pr ProjectRoot) PruneOptions { |
| po, has := o.PerProjectOptions[pr] |
| if !has { |
| return o.DefaultOptions |
| } |
| |
| ops := o.DefaultOptions |
| if po.NestedVendor != 0 { |
| if po.NestedVendor == 1 { |
| ops |= PruneNestedVendorDirs |
| } else { |
| ops &^= PruneNestedVendorDirs |
| } |
| } |
| |
| if po.UnusedPackages != 0 { |
| if po.UnusedPackages == 1 { |
| ops |= PruneUnusedPackages |
| } else { |
| ops &^= PruneUnusedPackages |
| } |
| } |
| |
| if po.NonGoFiles != 0 { |
| if po.NonGoFiles == 1 { |
| ops |= PruneNonGoFiles |
| } else { |
| ops &^= PruneNonGoFiles |
| } |
| } |
| |
| if po.GoTests != 0 { |
| if po.GoTests == 1 { |
| ops |= PruneGoTestFiles |
| } else { |
| ops &^= PruneGoTestFiles |
| } |
| } |
| |
| return ops |
| } |
| |
| func defaultCascadingPruneOptions() CascadingPruneOptions { |
| return CascadingPruneOptions{ |
| DefaultOptions: PruneNestedVendorDirs, |
| PerProjectOptions: map[ProjectRoot]PruneOptionSet{}, |
| } |
| } |
| |
| var ( |
| // licenseFilePrefixes is a list of name prefixes for license files. |
| licenseFilePrefixes = []string{ |
| "license", |
| "licence", |
| "copying", |
| "unlicense", |
| "copyright", |
| "copyleft", |
| } |
| // legalFileSubstrings contains substrings that are likey part of a legal |
| // declaration file. |
| legalFileSubstrings = []string{ |
| "authors", |
| "contributors", |
| "legal", |
| "notice", |
| "disclaimer", |
| "patent", |
| "third-party", |
| "thirdparty", |
| } |
| ) |
| |
| // PruneProject remove excess files according to the options passed, from |
| // the lp directory in baseDir. |
| func PruneProject(baseDir string, lp LockedProject, options PruneOptions) error { |
| fsState, err := deriveFilesystemState(baseDir) |
| |
| if err != nil { |
| return errors.Wrap(err, "could not derive filesystem state") |
| } |
| |
| if (options & PruneNestedVendorDirs) != 0 { |
| if err := pruneVendorDirs(fsState); err != nil { |
| return errors.Wrapf(err, "failed to prune nested vendor directories") |
| } |
| } |
| |
| if (options & PruneUnusedPackages) != 0 { |
| if _, err := pruneUnusedPackages(lp, fsState); err != nil { |
| return errors.Wrap(err, "failed to prune unused packages") |
| } |
| } |
| |
| if (options & PruneNonGoFiles) != 0 { |
| if err := pruneNonGoFiles(fsState); err != nil { |
| return errors.Wrap(err, "failed to prune non-Go files") |
| } |
| } |
| |
| if (options & PruneGoTestFiles) != 0 { |
| if err := pruneGoTestFiles(fsState); err != nil { |
| return errors.Wrap(err, "failed to prune Go test files") |
| } |
| } |
| |
| if err := deleteEmptyDirs(fsState); err != nil { |
| return errors.Wrap(err, "could not delete empty dirs") |
| } |
| |
| return nil |
| } |
| |
| // pruneVendorDirs deletes all nested vendor directories within baseDir. |
| func pruneVendorDirs(fsState filesystemState) error { |
| for _, dir := range fsState.dirs { |
| if filepath.Base(dir) == "vendor" { |
| err := os.RemoveAll(filepath.Join(fsState.root, dir)) |
| if err != nil && !os.IsNotExist(err) { |
| return err |
| } |
| } |
| } |
| |
| for _, link := range fsState.links { |
| if filepath.Base(link.path) == "vendor" { |
| err := os.Remove(filepath.Join(fsState.root, link.path)) |
| if err != nil && !os.IsNotExist(err) { |
| return err |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| // pruneUnusedPackages deletes unimported packages found in fsState. |
| // Determining whether packages are imported or not is based on the passed LockedProject. |
| func pruneUnusedPackages(lp LockedProject, fsState filesystemState) (map[string]interface{}, error) { |
| unusedPackages := calculateUnusedPackages(lp, fsState) |
| toDelete := collectUnusedPackagesFiles(fsState, unusedPackages) |
| |
| for _, path := range toDelete { |
| if err := os.Remove(path); err != nil && !os.IsNotExist(err) { |
| return nil, err |
| } |
| } |
| |
| return unusedPackages, nil |
| } |
| |
| // calculateUnusedPackages generates a list of unused packages in lp. |
| func calculateUnusedPackages(lp LockedProject, fsState filesystemState) map[string]interface{} { |
| unused := make(map[string]interface{}) |
| imported := make(map[string]interface{}) |
| |
| for _, pkg := range lp.Packages() { |
| imported[pkg] = nil |
| } |
| |
| // Add the root package if it's not imported. |
| if _, ok := imported["."]; !ok { |
| unused["."] = nil |
| } |
| |
| for _, dirPath := range fsState.dirs { |
| pkg := filepath.ToSlash(dirPath) |
| |
| if _, ok := imported[pkg]; !ok { |
| unused[pkg] = nil |
| } |
| } |
| |
| return unused |
| } |
| |
| // collectUnusedPackagesFiles returns a slice of all files in the unused |
| // packages based on fsState. |
| func collectUnusedPackagesFiles(fsState filesystemState, unusedPackages map[string]interface{}) []string { |
| // TODO(ibrasho): is this useful? |
| files := make([]string, 0, len(unusedPackages)) |
| |
| for _, path := range fsState.files { |
| // Keep preserved files. |
| if isPreservedFile(filepath.Base(path)) { |
| continue |
| } |
| |
| pkg := filepath.ToSlash(filepath.Dir(path)) |
| |
| if _, ok := unusedPackages[pkg]; ok { |
| files = append(files, filepath.Join(fsState.root, path)) |
| } |
| } |
| |
| return files |
| } |
| |
| func isSourceFile(path string) bool { |
| ext := fileExt(path) |
| |
| // Refer to: https://github.com/golang/go/blob/release-branch.go1.9/src/go/build/build.go#L750 |
| switch ext { |
| case ".go": |
| return true |
| case ".c": |
| return true |
| case ".cc", ".cpp", ".cxx": |
| return true |
| case ".m": |
| return true |
| case ".h", ".hh", ".hpp", ".hxx": |
| return true |
| case ".f", ".F", ".for", ".f90": |
| return true |
| case ".s": |
| return true |
| case ".S": |
| return true |
| case ".swig": |
| return true |
| case ".swigcxx": |
| return true |
| case ".syso": |
| return true |
| } |
| return false |
| } |
| |
| // pruneNonGoFiles delete all non-Go files existing in fsState. |
| // |
| // Files matching licenseFilePrefixes and legalFileSubstrings are not pruned. |
| func pruneNonGoFiles(fsState filesystemState) error { |
| toDelete := make([]string, 0, len(fsState.files)/4) |
| |
| for _, path := range fsState.files { |
| if isSourceFile(path) { |
| continue |
| } |
| |
| // Ignore preserved files. |
| if isPreservedFile(filepath.Base(path)) { |
| continue |
| } |
| |
| toDelete = append(toDelete, filepath.Join(fsState.root, path)) |
| } |
| |
| for _, path := range toDelete { |
| if err := os.Remove(path); err != nil && !os.IsNotExist(err) { |
| return err |
| } |
| } |
| |
| return nil |
| } |
| |
| // isPreservedFile checks if the file name indicates that the file should be |
| // preserved based on licenseFilePrefixes or legalFileSubstrings. |
| // This applies only to non-source files. |
| func isPreservedFile(name string) bool { |
| if isSourceFile(name) { |
| return false |
| } |
| |
| name = strings.ToLower(name) |
| |
| for _, prefix := range licenseFilePrefixes { |
| if strings.HasPrefix(name, prefix) { |
| return true |
| } |
| } |
| |
| for _, substring := range legalFileSubstrings { |
| if strings.Contains(name, substring) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| // pruneGoTestFiles deletes all Go test files (*_test.go) in fsState. |
| func pruneGoTestFiles(fsState filesystemState) error { |
| toDelete := make([]string, 0, len(fsState.files)/2) |
| |
| for _, path := range fsState.files { |
| if strings.HasSuffix(path, "_test.go") { |
| toDelete = append(toDelete, filepath.Join(fsState.root, path)) |
| } |
| } |
| |
| for _, path := range toDelete { |
| if err := os.Remove(path); err != nil && !os.IsNotExist(err) { |
| return err |
| } |
| } |
| |
| return nil |
| } |
| |
| func deleteEmptyDirs(fsState filesystemState) error { |
| sort.Sort(sort.Reverse(sort.StringSlice(fsState.dirs))) |
| |
| for _, dir := range fsState.dirs { |
| path := filepath.Join(fsState.root, dir) |
| |
| notEmpty, err := fs.IsNonEmptyDir(path) |
| if err != nil { |
| return err |
| } |
| |
| if !notEmpty { |
| if err := os.Remove(path); err != nil && !os.IsNotExist(err) { |
| return err |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| func fileExt(name string) string { |
| i := strings.LastIndex(name, ".") |
| if i < 0 { |
| return "" |
| } |
| return name[i:] |
| } |