forked from golang/dep
-
Notifications
You must be signed in to change notification settings - Fork 0
/
prune.go
331 lines (274 loc) · 8.06 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
// 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
// PruneProjectOptions is map of prune options per project name.
type PruneProjectOptions map[ProjectRoot]PruneOptions
// RootPruneOptions represents the root prune options for the project.
// It contains the global options and a map of options per project.
type RootPruneOptions struct {
PruneOptions PruneOptions
ProjectOptions PruneProjectOptions
}
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
)
// DefaultRootPruneOptions instantiates a copy of the default root prune options.
func DefaultRootPruneOptions() RootPruneOptions {
return RootPruneOptions{
PruneOptions: PruneNestedVendorDirs,
ProjectOptions: PruneProjectOptions{},
}
}
// PruneOptionsFor returns the prune options for the passed project root.
//
// It will return the root prune options if the project does not have specific
// options or if it does not exist in the manifest.
func (o *RootPruneOptions) PruneOptionsFor(pr ProjectRoot) PruneOptions {
if po, ok := o.ProjectOptions[pr]; ok {
return po
}
return o.PruneOptions
}
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:]
}