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 }