| OLD | NEW |
| 1 // Copyright 2017 The LUCI Authors. All rights reserved. | 1 // Copyright 2017 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 | 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. | 3 // that can be found in the LICENSE file. |
| 4 | 4 |
| 5 package python | 5 package python |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "crypto/sha256" |
| 9 "encoding/hex" |
| 10 "io" |
| 11 "os" |
| 8 "os/exec" | 12 "os/exec" |
| 13 "path/filepath" |
| 9 "strings" | 14 "strings" |
| 10 "sync" | 15 "sync" |
| 11 | 16 |
| 12 "github.com/luci/luci-go/common/errors" | 17 "github.com/luci/luci-go/common/errors" |
| 18 "github.com/luci/luci-go/common/system/filesystem" |
| 13 | 19 |
| 14 "golang.org/x/net/context" | 20 "golang.org/x/net/context" |
| 15 ) | 21 ) |
| 16 | 22 |
| 17 type runnerFunc func(cmd *exec.Cmd, capture bool) (string, error) | 23 type runnerFunc func(cmd *exec.Cmd, capture bool) (string, error) |
| 18 | 24 |
| 19 // Interpreter represents a system Python interpreter. It exposes the ability | 25 // Interpreter represents a system Python interpreter. It exposes the ability |
| 20 // to use common functionality of that interpreter. | 26 // to use common functionality of that interpreter. |
| 21 type Interpreter struct { | 27 type Interpreter struct { |
| 22 // Python is the path to the system Python interpreter. | 28 // Python is the path to the system Python interpreter. |
| 23 Python string | 29 Python string |
| 24 | 30 |
| 25 // cachedVersion is the cached Version for this interpreter. It is popul
ated | 31 // cachedVersion is the cached Version for this interpreter. It is popul
ated |
| 26 // on the first GetVersion call. | 32 // on the first GetVersion call. |
| 27 cachedVersion *Version | 33 cachedVersion *Version |
| 28 cachedVersionMu sync.Mutex | 34 cachedVersionMu sync.Mutex |
| 29 | 35 |
| 36 // cachedHash is the cached SHA256 hash string of the interpreter's bina
ry |
| 37 // contents. It is populated once, protected by cachedHashOnce. |
| 38 cachedHash string |
| 39 cachedHashErr error |
| 40 cachedHashOnce sync.Once |
| 41 |
| 30 // testCommandHook, if not nil, is called on generated Command results p
rior | 42 // testCommandHook, if not nil, is called on generated Command results p
rior |
| 31 // to returning them. | 43 // to returning them. |
| 32 testCommandHook func(*exec.Cmd) | 44 testCommandHook func(*exec.Cmd) |
| 33 } | 45 } |
| 34 | 46 |
| 47 // Normalize normalizes the Interpreter configuration by resolving relative |
| 48 // paths into absolute paths and evaluating symlnks. |
| 49 func (i *Interpreter) Normalize() error { |
| 50 if err := filesystem.AbsPath(&i.Python); err != nil { |
| 51 return err |
| 52 } |
| 53 resolved, err := filepath.EvalSymlinks(i.Python) |
| 54 if err != nil { |
| 55 return errors.Annotate(err).Reason("could not evaluate symlinks
for: %(path)q"). |
| 56 D("path", i.Python). |
| 57 Err() |
| 58 } |
| 59 i.Python = resolved |
| 60 return nil |
| 61 } |
| 62 |
| 35 // IsolatedCommand returns a configurable exec.Cmd structure bound to this | 63 // IsolatedCommand returns a configurable exec.Cmd structure bound to this |
| 36 // Interpreter. | 64 // Interpreter. |
| 37 // | 65 // |
| 38 // The supplied arguments have several Python isolation flags prepended to them | 66 // The supplied arguments have several Python isolation flags prepended to them |
| 39 // to remove environmental factors such as: | 67 // to remove environmental factors such as: |
| 40 // - The user's "site.py". | 68 // - The user's "site.py". |
| 41 // - The current PYTHONPATH environment variable. | 69 // - The current PYTHONPATH environment variable. |
| 42 // - Compiled ".pyc/.pyo" files. | 70 // - Compiled ".pyc/.pyo" files. |
| 43 func (i *Interpreter) IsolatedCommand(c context.Context, args ...string) *exec.C
md { | 71 func (i *Interpreter) IsolatedCommand(c context.Context, args ...string) *exec.C
md { |
| 44 // Isolate the supplied arguments. | 72 // Isolate the supplied arguments. |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 78 | 106 |
| 79 if v, err = parseVersionOutput(strings.TrimSpace(string(out))); err != n
il { | 107 if v, err = parseVersionOutput(strings.TrimSpace(string(out))); err != n
il { |
| 80 err = errors.Annotate(err).Err() | 108 err = errors.Annotate(err).Err() |
| 81 return | 109 return |
| 82 } | 110 } |
| 83 | 111 |
| 84 i.cachedVersion = &v | 112 i.cachedVersion = &v |
| 85 return | 113 return |
| 86 } | 114 } |
| 87 | 115 |
| 116 // Hash returns the SHA256 hash string of this interpreter. |
| 117 // |
| 118 // The hash value is cached; if called multiple times, the cached value will |
| 119 // be returned. |
| 120 func (i *Interpreter) Hash() (string, error) { |
| 121 hashInterpreter := func(path string) (string, error) { |
| 122 fd, err := os.Open(i.Python) |
| 123 if err != nil { |
| 124 return "", errors.Annotate(err).Reason("failed to open i
nterpreter").Err() |
| 125 } |
| 126 defer fd.Close() |
| 127 |
| 128 hash := sha256.New() |
| 129 if _, err := io.Copy(hash, fd); err != nil { |
| 130 return "", errors.Annotate(err).Reason("failed to read [
%(path)s] for hashing"). |
| 131 D("path", path). |
| 132 Err() |
| 133 } |
| 134 |
| 135 return hex.EncodeToString(hash.Sum(nil)), nil |
| 136 } |
| 137 |
| 138 i.cachedHashOnce.Do(func() { |
| 139 i.cachedHash, i.cachedHashErr = hashInterpreter(i.Python) |
| 140 }) |
| 141 return i.cachedHash, i.cachedHashErr |
| 142 } |
| 143 |
| 88 func parseVersionOutput(output string) (Version, error) { | 144 func parseVersionOutput(output string) (Version, error) { |
| 89 // Expected output: | 145 // Expected output: |
| 90 // Python X.Y.Z | 146 // Python X.Y.Z |
| 91 parts := strings.SplitN(output, " ", 2) | 147 parts := strings.SplitN(output, " ", 2) |
| 92 if len(parts) != 2 || parts[0] != "Python" { | 148 if len(parts) != 2 || parts[0] != "Python" { |
| 93 return Version{}, errors.Reason("unknown version output"). | 149 return Version{}, errors.Reason("unknown version output"). |
| 94 D("output", output). | 150 D("output", output). |
| 95 Err() | 151 Err() |
| 96 } | 152 } |
| 97 | 153 |
| 98 v, err := ParseVersion(parts[1]) | 154 v, err := ParseVersion(parts[1]) |
| 99 if err != nil { | 155 if err != nil { |
| 100 err = errors.Annotate(err).Reason("failed to parse version from:
%(value)q"). | 156 err = errors.Annotate(err).Reason("failed to parse version from:
%(value)q"). |
| 101 D("value", parts[1]). | 157 D("value", parts[1]). |
| 102 Err() | 158 Err() |
| 103 return v, err | 159 return v, err |
| 104 } | 160 } |
| 105 return v, nil | 161 return v, nil |
| 106 } | 162 } |
| OLD | NEW |