Chromium Code Reviews| OLD | NEW |
|---|---|
| (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 } | |
| OLD | NEW |