OLD | NEW |
| (Empty) |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 package cipd | |
6 | |
7 import ( | |
8 "fmt" | |
9 "io" | |
10 "io/ioutil" | |
11 "path/filepath" | |
12 "regexp" | |
13 "sort" | |
14 | |
15 "github.com/go-yaml/yaml" | |
16 ) | |
17 | |
18 // PackageDef defines how exactly to build a package: what files to put into it, | |
19 // how to name them, how to name the package itself, etc. It is loaded from | |
20 // *.yaml file. | |
21 type PackageDef struct { | |
22 // Package defines a name of the package. | |
23 Package string | |
24 // Root defines where to search for files, relative to package file itse
lf. | |
25 Root string | |
26 // Data describe what to add to the package. | |
27 Data []PackageChunkDef | |
28 } | |
29 | |
30 // PackageChunkDef represents one entry in 'data' section of package definition | |
31 // file. It is either a single file, or a recursively scanned directory (with | |
32 // optional list of regexps for files to skip). | |
33 type PackageChunkDef struct { | |
34 // Dir is a directory to add to the package (recursively). | |
35 Dir string | |
36 // File is a single file to add to the package. | |
37 File string | |
38 // Exclude is a list of glob patterns to exclude when scanning a directo
ry. | |
39 Exclude []string | |
40 } | |
41 | |
42 // LoadPackageDef loads package definition from a YAML source code. In | |
43 // substitutes %{...} strings in the definition with corresponding values | |
44 // from 'vars' map. | |
45 func LoadPackageDef(r io.Reader, vars map[string]string) (out PackageDef, err er
ror) { | |
46 data, err := ioutil.ReadAll(r) | |
47 if err != nil { | |
48 return | |
49 } | |
50 err = yaml.Unmarshal(data, &out) | |
51 if err != nil { | |
52 return | |
53 } | |
54 | |
55 // Substitute variables in all strings. | |
56 for _, str := range out.strings() { | |
57 *str, err = subVars(*str, vars) | |
58 if err != nil { | |
59 return | |
60 } | |
61 } | |
62 | |
63 // Validate package name right away. | |
64 err = ValidatePackageName(out.Package) | |
65 if err != nil { | |
66 return | |
67 } | |
68 | |
69 // Make sure "file" or "dir" are used, but not both. | |
70 for i, chunk := range out.Data { | |
71 if chunk.File == "" && chunk.Dir == "" { | |
72 return out, fmt.Errorf("files entry #%d needs 'file' or
'dir' key", i) | |
73 } | |
74 if chunk.File != "" && chunk.Dir != "" { | |
75 return out, fmt.Errorf("files entry #%d can't have both
'files' and 'dir' keys", i) | |
76 } | |
77 } | |
78 | |
79 // Default 'root' to a directory with the package def file. | |
80 if out.Root == "" { | |
81 out.Root = "." | |
82 } | |
83 | |
84 return | |
85 } | |
86 | |
87 // FindFiles scans files system and returns all files to be added to the | |
88 // package. It uses a path to package definition file directory ('cwd' argument) | |
89 // to find a root of the package. | |
90 func (def *PackageDef) FindFiles(cwd string) ([]File, error) { | |
91 // Root of the package is defined relative to package def YAML file. | |
92 absCwd, err := filepath.Abs(cwd) | |
93 if err != nil { | |
94 return nil, err | |
95 } | |
96 root := filepath.Clean(filepath.Join(absCwd, filepath.FromSlash(def.Root
))) | |
97 | |
98 // Helper to get absolute path to a file given path relative to root. | |
99 makeAbs := func(p string) string { | |
100 return filepath.Join(root, filepath.FromSlash(p)) | |
101 } | |
102 | |
103 // Used to skip duplicates. | |
104 seen := map[string]File{} | |
105 add := func(f File) { | |
106 if seen[f.Name()] == nil { | |
107 seen[f.Name()] = f | |
108 } | |
109 } | |
110 | |
111 log.Info("Enumerating files to zip...") | |
112 for _, chunk := range def.Data { | |
113 // Individual file. | |
114 if chunk.File != "" { | |
115 file, err := WrapFile(makeAbs(chunk.File), root, nil) | |
116 if err != nil { | |
117 return nil, err | |
118 } | |
119 add(file) | |
120 continue | |
121 } | |
122 | |
123 // A subdirectory to scan (with filtering). | |
124 if chunk.Dir != "" { | |
125 // Absolute path to directory to scan. | |
126 startDir := makeAbs(chunk.Dir) | |
127 // Exclude files as specified in 'exclude' section. | |
128 exclude, err := makeExclusionFilter(startDir, chunk.Excl
ude) | |
129 if err != nil { | |
130 return nil, err | |
131 } | |
132 // Run the scan. | |
133 files, err := ScanFileSystem(startDir, root, exclude) | |
134 if err != nil { | |
135 return nil, err | |
136 } | |
137 for _, f := range files { | |
138 add(f) | |
139 } | |
140 continue | |
141 } | |
142 | |
143 // LoadPackageDef does validation, so this should not happen. | |
144 return nil, fmt.Errorf("Unexpected definition: %v", chunk) | |
145 } | |
146 | |
147 // Sort by Name(). | |
148 names := make([]string, 0, len(seen)) | |
149 for n := range seen { | |
150 names = append(names, n) | |
151 } | |
152 sort.Strings(names) | |
153 | |
154 // Final sorted array of File. | |
155 out := make([]File, 0, len(names)) | |
156 for _, n := range names { | |
157 out = append(out, seen[n]) | |
158 } | |
159 return out, nil | |
160 } | |
161 | |
162 // makeExclusionFilter produces a predicate that checks an absolute file path | |
163 // against a list of regexps (defined against slash separated paths relative to | |
164 // 'startDir'). The predicate takes absolute native path, converts it to slash | |
165 // separated path relative to 'startDir' and checks against list of regexps in | |
166 // 'patterns'. Returns true on match. | |
167 func makeExclusionFilter(startDir string, patterns []string) (ScanFilter, error)
{ | |
168 if len(patterns) == 0 { | |
169 return nil, nil | |
170 } | |
171 | |
172 // Compile regular expressions. | |
173 exps := []*regexp.Regexp{} | |
174 for _, expr := range patterns { | |
175 if expr == "" { | |
176 continue | |
177 } | |
178 if expr[0] != '^' { | |
179 expr = "^" + expr | |
180 } | |
181 if expr[len(expr)-1] != '$' { | |
182 expr = expr + "$" | |
183 } | |
184 re, err := regexp.Compile(expr) | |
185 if err != nil { | |
186 return nil, err | |
187 } | |
188 exps = append(exps, re) | |
189 } | |
190 | |
191 return func(abs string) bool { | |
192 rel, err := filepath.Rel(startDir, abs) | |
193 if err != nil { | |
194 log.Warnf("Unexpected error when evaluating %s: %s", abs
, err) | |
195 return true | |
196 } | |
197 // Do not evaluate paths outside of startDir. | |
198 rel = filepath.ToSlash(rel) | |
199 if rel[:3] == "../" { | |
200 return false | |
201 } | |
202 for _, exp := range exps { | |
203 if exp.MatchString(rel) { | |
204 return true | |
205 } | |
206 } | |
207 return false | |
208 }, nil | |
209 } | |
210 | |
211 //////////////////////////////////////////////////////////////////////////////// | |
212 // Variable substitution. | |
213 | |
214 var subVarsRe = regexp.MustCompile(`\$\{[^\}]+\}`) | |
215 | |
216 // strings return array of pointers to all strings in PackageDef that can | |
217 // contain ${var} variables. | |
218 func (def *PackageDef) strings() []*string { | |
219 out := []*string{ | |
220 &def.Package, | |
221 &def.Root, | |
222 } | |
223 // Important to use index here, to get a point to a real object, not its
copy. | |
224 for i := range def.Data { | |
225 out = append(out, def.Data[i].strings()...) | |
226 } | |
227 return out | |
228 } | |
229 | |
230 // strings return array of pointers to all strings in PackageChunkDef that can | |
231 // contain ${var} variables. | |
232 func (def *PackageChunkDef) strings() []*string { | |
233 out := []*string{ | |
234 &def.Dir, | |
235 &def.File, | |
236 } | |
237 for i := range def.Exclude { | |
238 out = append(out, &def.Exclude[i]) | |
239 } | |
240 return out | |
241 } | |
242 | |
243 // subVars replaces "${key}" in strings with values from 'vars' map. Returns | |
244 // error if some keys weren't found in 'vars' map. | |
245 func subVars(s string, vars map[string]string) (string, error) { | |
246 badKeys := []string{} | |
247 res := subVarsRe.ReplaceAllStringFunc(s, func(match string) string { | |
248 // Strip '${' and '}'. | |
249 key := match[2 : len(match)-1] | |
250 val, ok := vars[key] | |
251 if !ok { | |
252 badKeys = append(badKeys, key) | |
253 return match | |
254 } | |
255 return val | |
256 }) | |
257 if len(badKeys) != 0 { | |
258 return res, fmt.Errorf("Values for some variables are not provid
ed: %v", badKeys) | |
259 } | |
260 return res, nil | |
261 } | |
OLD | NEW |