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

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

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

Powered by Google App Engine
This is Rietveld 408576698