Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(91)

Side by Side Diff: vpython/spec/load.go

Issue 2705623003: vpython: Add environment spec package. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | vpython/spec/load_test.go » ('j') | vpython/spec/spec.go » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2017 The LUCI Authors. All rights reserved.
2 // Use of this source code is governed under the Apache License, Version 2.0
3 // that can be found in the LICENSE file.
4
5 package spec
6
7 import (
8 "bufio"
9 "io/ioutil"
10 "os"
11 "path/filepath"
12 "strings"
13
14 "github.com/luci/luci-go/vpython/api/env"
15
16 "github.com/luci/luci-go/common/errors"
17 "github.com/luci/luci-go/common/logging"
18 cproto "github.com/luci/luci-go/common/proto"
19
20 "github.com/golang/protobuf/proto"
21 "golang.org/x/net/context"
22 )
23
24 // Suffix is the filesystem suffix for a script's partner specification file.
25 //
26 // See LoadForScript for more information.
27 const Suffix = ".vpython"
28
29 // Load loads an environment specification file text protobuf from the supplied
iannucci 2017/02/21 09:09:55 let's be careful not to overload 'environment' her
dnj 2017/02/22 07:37:19 Done.
30 // path.
31 func Load(path string) (*env.Spec, error) {
iannucci 2017/02/21 09:09:55 I don't suppose we can s/env/vpython?
dnj 2017/02/22 07:37:19 hrm sure, done.
32 content, err := ioutil.ReadFile(path)
33 if err != nil {
34 return nil, errors.Annotate(err).Reason("failed to load file fro m: %(path)s").
35 D("path", path).
36 Err()
37 }
38
39 spec, err := Parse(string(content))
40 if err != nil {
41 return nil, errors.Annotate(err).Err()
42 }
43 return spec, nil
44 }
45
46 // Parse loads a specification message from a content string.
47 func Parse(content string) (*env.Spec, error) {
48 var spec env.Spec
49 if err := cproto.UnmarshalTextML(content, &spec); err != nil {
50 return nil, errors.Annotate(err).Reason("failed to unmarshal env .Spec").Err()
51 }
52 return &spec, nil
53 }
54
55 // Write writes a text protobuf form of spec to path.
56 func Write(spec *env.Spec, path string) error {
57 fd, err := os.Create(path)
58 if err != nil {
59 return errors.Annotate(err).Reason("failed to create output file ").Err()
60 }
61
62 if err := proto.MarshalText(fd, spec); err != nil {
63 _ = fd.Close()
64 return errors.Annotate(err).Reason("failed to output text protob uf").Err()
65 }
66
67 if err := fd.Close(); err != nil {
68 return errors.Annotate(err).Reason("failed to Close file").Err()
69 }
70 return nil
71 }
72
73 // LoadForScript attempts to load a spec file for the specified script. If
74 // nothing went wrong, a nil error will be returned. If a spec file was
75 // identified, it will also be returned. Otherwise, a nil spec will be returned.
76 //
77 // Spec files can be specified in a variety of ways. This function will look for
78 // them in the following order, and return the first one that was identified:
79 //
80 // - Partner File
81 // - Inline
82 //
83 // Partner File
84 // ============
85 //
86 // LoadForScript traverses the filesystem to find the environment
87 // specification file that is naturally associated with the specified
88 // path.
89 //
90 // If the path is a Python script (e.g, "/path/to/test.py"), isModule will be
91 // false, and the file will be found at "/path/to/test.py.vpython".
92 //
93 // If the path is a Python module (isModule is true), FindEnvSpecForScript walks
94 // upwards in the directory structure, looking for a file that shares a module
95 // directory name and ends with ".vpython". For example, for module:
96 //
97 // /path/to/foo/bar/baz/__init__.py
98 // /path/to/foo/bar/__init__.py
99 // /path/to/foo/__init__.py
100 // /path/to/foo.vpython
101 //
102 // LoadForScript will first look at "/path/to/foo/bar/baz", then walk upwards
103 // until it either hits a directory that doesn't contain an "__init__.py" file,
104 // or finds the ES path. In this case, for module "foo.bar.baz", it will
105 // identify "/path/to/foo.vpython" as the ES file for that module.
106 //
107 // Inline
108 // ======
109 //
110 // LoadForScript scans through the contents of the file at path and attempts to
111 // load environment specification boundaries.
112 //
113 // If the file at path does not exist, or if the file does not contain spec
114 // guards, a nil spec will be returned.
115 //
116 // The embedded enviornment specification is a text protobuf embedded within
117 // the file. To parse it, the file is scanned line-by-line for a beginning and
118 // ending guard. The content between those guards is minimally processed, then
119 // interpreted as a text protobuf.
120 //
121 // [VPYTHON:BEGIN]
122 // wheel {
123 // path: ...
124 // version: ...
125 // }
126 // [VPYTHON:END]
127 //
128 // To allow VPYTHON directives to be embedded in a language-compatible manner
129 // (with indentation, comments, etc.), the processor will identify any common
130 // characters preceding the BEGIN and END clauses. If they match, those
131 // characters will be automatically stripped out of the intermediate lines. This
132 // can be used to embed the directives in comments:
133 //
134 // // [VPYTHON:BEGIN]
135 // // wheel {
136 // // path: ...
137 // // version: ...
138 // // }
139 // // [VPYTHON:END]
140 //
141 // In this case, the "// " characters will be removed.
142 func LoadForScript(c context.Context, path string, isModule bool) (*env.Spec, er ror) {
143 // Partner File: Try loading the spec from an adjacent file.
144 specPath, err := findForScript(path, isModule)
145 if err != nil {
146 return nil, errors.Annotate(err).Reason("failed to scan for file system spec").Err()
147 }
148 if specPath != "" {
149 switch sp, err := Load(specPath); {
150 case err != nil:
151 return nil, errors.Annotate(err).Reason("failed to load specification file").
152 D("specPath", specPath).
153 Err()
154
155 case sp != nil:
156 logging.Infof(c, "Loaded environment spec from: %s", spe cPath)
157 return sp, nil
158 }
159 }
160
161 // Inline: Try and parse the main script for the spec file.
162 mainScript := path
163 if isModule {
164 // Module.
165 mainScript = filepath.Join(mainScript, "__main__.py")
166 }
167 switch sp, err := parseFrom(mainScript); {
168 case err != nil:
169 return nil, errors.Annotate(err).Reason("failed to parse inline spec from: %(script)s").
170 D("script", mainScript).
171 Err()
172
173 case sp != nil:
174 logging.Infof(c, "Loaded inline spec from: %s", mainScript)
175 return sp, nil
176 }
177
178 // No spec file found.
179 return nil, nil
180 }
181
182 func findForScript(path string, isModule bool) (string, error) {
183 // Otherwise, try and find the ES file associated with this script.
iannucci 2017/02/21 09:09:54 otherwise?
dnj 2017/02/22 07:37:19 Done.
184 if !isModule {
185 path += Suffix
186 if st, err := os.Stat(path); err != nil || st.IsDir() {
187 // File does not exist at this path.
188 return "", nil
189 }
190 return path, nil
191 }
192
193 // If it's a directory, scan for an ".es" file until we don't have a
iannucci 2017/02/21 09:09:54 .es file?
dnj 2017/02/22 07:37:19 Done.
194 // __init__.py.
195 for {
196 prev := path
197
198 // Directory must be a Python module.
199 initPath := filepath.Join(path, "__init__.py")
200 if _, err := os.Stat(initPath); err != nil {
201 if os.IsNotExist(err) {
202 // Not a Python module, so we're done our search .
203 return "", nil
204 }
205 return "", errors.Annotate(err).Reason("failed to stat f or: %(path)").
206 D("path", initPath).
207 Err()
208 }
209
210 // Does a spec file exist for this path?
211 specPath := path + Suffix
212 switch _, err := os.Stat(specPath); {
213 case err == nil:
214 // Found the file.
215 return specPath, nil
216
217 case os.IsNotExist(err):
218 // Recurse to parent.
219 path = filepath.Dir(path)
220 if path == prev {
221 // Finished recursing, no ES file.
222 return "", nil
223 }
224
225 default:
226 return "", errors.Annotate(err).Reason("failed to check for spec file at: %(path)s").
227 D("path", specPath).
228 Err()
229 }
230 }
231 }
232
233 func parseFrom(path string) (*env.Spec, error) {
234 const (
235 beginGuard = "[VPYTHON:BEGIN]"
236 endGuard = "[VPYTHON:END]"
237 )
238
239 fd, err := os.Open(path)
240 if err != nil {
241 return nil, errors.Annotate(err).Reason("failed to open file").E rr()
242 }
243 defer fd.Close()
244
245 s := bufio.NewScanner(fd)
246 var (
247 content []string
248 beginLine string
249 endLine string
250 inRegion = false
251 )
252 for s.Scan() {
253 line := strings.TrimSpace(s.Text())
254 if !inRegion {
255 inRegion = strings.HasSuffix(line, beginGuard)
256 beginLine = line
257 } else {
258 if strings.HasSuffix(line, endGuard) {
259 // Finished processing.
260 endLine = line
261 break
262 }
263 content = append(content, line)
264 }
265 }
266 if err := s.Err(); err != nil {
267 return nil, errors.Annotate(err).Reason("error scanning file").E rr()
268 }
269 if len(content) == 0 {
270 return nil, nil
271 }
272 if endLine == "" {
273 return nil, errors.New("unterminated inline spec file")
274 }
275
276 // If we have a common begin/end prefix, trim it from each content line that
277 // also has it.
278 prefix := beginLine[:len(beginLine)-len(beginGuard)]
279 if endLine[:len(endLine)-len(endGuard)] != prefix {
280 prefix = ""
iannucci 2017/02/21 09:09:54 should this just be an error?
dnj 2017/02/22 07:37:19 I don't think so. Let's be a little flexible for n
281 }
282 if prefix != "" {
283 for i, line := range content {
284 if len(line) < len(prefix) {
285 // This line is shorter than the prefix. Does th e part of that line that
286 // exists match the prefix up until that point?
287 if line == prefix[:len(line)] {
288 // Yes, so empty line.
289 line = ""
290 }
iannucci 2017/02/21 09:09:54 what happens in the else case? doesn't matter?
dnj 2017/02/22 07:37:19 Yeah, if there is no shorter prefix to trim, we le
291 } else {
292 line = strings.TrimPrefix(line, prefix)
293 }
294 content[i] = line
295 }
296 }
297
298 // Process the resulting file.
299 spec, err := Parse(strings.Join(content, "\n"))
300 if err != nil {
301 return nil, errors.Annotate(err).Reason("failed to parse spec fi le from: %(path)s").
302 D("path", path).
303 Err()
304 }
305 return spec, nil
306 }
OLDNEW
« no previous file with comments | « no previous file | vpython/spec/load_test.go » ('j') | vpython/spec/spec.go » ('J')

Powered by Google App Engine
This is Rietveld 408576698