forked from golang/dep
-
Notifications
You must be signed in to change notification settings - Fork 0
/
prune.go
382 lines (321 loc) · 9.29 KB
/
prune.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
// 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 (
"log"
"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
}
// 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, logger *log.Logger) 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 perserved 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
}
// 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 {
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":
continue
case ".c":
continue
case ".cc", ".cpp", ".cxx":
continue
case ".m":
continue
case ".h", ".hh", ".hpp", ".hxx":
continue
case ".f", ".F", ".for", ".f90":
continue
case ".s":
continue
case ".S":
continue
case ".swig":
continue
case ".swigcxx":
continue
case ".syso":
continue
}
// Ignore perserved 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.
func isPreservedFile(name string) bool {
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:]
}