| 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/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 } |
| OLD | NEW |