| 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 venv |
| 6 |
| 7 import ( |
| 8 "fmt" |
| 9 "os" |
| 10 "path/filepath" |
| 11 "time" |
| 12 "unicode/utf8" |
| 13 |
| 14 "github.com/luci/luci-go/vpython/api/vpython" |
| 15 "github.com/luci/luci-go/vpython/filesystem" |
| 16 "github.com/luci/luci-go/vpython/python" |
| 17 "github.com/luci/luci-go/vpython/spec" |
| 18 |
| 19 "github.com/luci/luci-go/common/errors" |
| 20 "github.com/luci/luci-go/common/logging" |
| 21 |
| 22 "golang.org/x/net/context" |
| 23 ) |
| 24 |
| 25 // PackageLoader loads package information from a specification file's Package |
| 26 // message onto the local system. |
| 27 type PackageLoader interface { |
| 28 // Resolve processes the supplied packages, updating their fields to the
ir |
| 29 // resolved values. Resolved packages must fully specify the package ins
tance |
| 30 // that is being deployed. |
| 31 // |
| 32 // If needed, resolution may use the supplied root path as a persistent |
| 33 // working directory. This path may not exist; Resolve is responsible fo
r |
| 34 // creating it if needed. |
| 35 // |
| 36 // root, if used, must be safe for concurrent use. |
| 37 Resolve(c context.Context, root string, packages []*vpython.Spec_Package
) error |
| 38 |
| 39 // Ensure installs the supplied packages into root. |
| 40 // |
| 41 // The packages will have been previously resolved via Resolve. |
| 42 Ensure(c context.Context, root string, packages []*vpython.Spec_Package)
error |
| 43 } |
| 44 |
| 45 // Config is the configuration for a managed VirtualEnv. |
| 46 // |
| 47 // A VirtualEnv is specified based on its resolved vpython.Spec. |
| 48 type Config struct { |
| 49 // MaxHashLen is the maximum number of hash characters to use in Virtual
Env |
| 50 // directory names. |
| 51 MaxHashLen int |
| 52 |
| 53 // BaseDir is the parent directory of all VirtualEnv. |
| 54 BaseDir string |
| 55 |
| 56 // Package is the VirtualEnv package to install. It must be non-nil and |
| 57 // valid. It will be used if the environment specification doesn't suppl
y an |
| 58 // overriding one. |
| 59 Package vpython.Spec_Package |
| 60 |
| 61 // Python is the Python interpreter to use. If empty, one will be resolv
ed |
| 62 // based on the Spec and the current PATH. |
| 63 Python string |
| 64 |
| 65 // Spec is the specification file to use to construct the VirtualEnv. If |
| 66 // nil, or if fields are missing, they will be filled in by probing the
system |
| 67 // PATH. |
| 68 Spec *vpython.Spec |
| 69 |
| 70 // PruneThreshold, if >0, is the maximum age of a VirtualEnv before it s
hould |
| 71 // be pruned. If <= 0, there is no maximum age, so no pruning will be |
| 72 // performed. |
| 73 PruneThreshold time.Duration |
| 74 // MaxPrunesPerSweep applies a limit to the number of items to prune per |
| 75 // execution. |
| 76 // |
| 77 // If <= 0, no limit will be applied. |
| 78 MaxPrunesPerSweep int |
| 79 |
| 80 // Loader is the PackageLoader instance to use for package resolution an
d |
| 81 // deployment. |
| 82 Loader PackageLoader |
| 83 |
| 84 // LoaderResolveRoot is the common persistent root directory to use for
the |
| 85 // package loader's package resolution. This must be safe for concurrent |
| 86 // use. |
| 87 // |
| 88 // Each VirtualEnv will have its own loader destination directory (root)
. |
| 89 // However, that root is homed in a directory that is named after the re
solved |
| 90 // packages. LoaderResolveRoot is used during setup to resolve those pac
kage |
| 91 // values, and therefore can't re-use the VirtualEnv root. |
| 92 LoaderResolveRoot string |
| 93 |
| 94 // MaxScriptPathLen, if >0, is the maximum allowed VirutalEnv path lengt
h. |
| 95 // If set, and if the VirtualEnv is configured to be installed at a path |
| 96 // greater than this, Env will fail. |
| 97 // |
| 98 // This can be used to enforce "shebang" length limits, whereupon genera
ted |
| 99 // VirtualEnv scripts may be generated with a "shebang" (#!) line longer
than |
| 100 // what is allowed by the operating system. |
| 101 MaxScriptPathLen int |
| 102 |
| 103 // testPreserveInstallationCapability is a testing parameter. If true, t
he |
| 104 // VirtualEnv's ability to install will be preserved after the setup. Th
is is |
| 105 // used by the test whell generation bootstrap code. |
| 106 testPreserveInstallationCapability bool |
| 107 |
| 108 // testLeaveReadWrite, if true, instructs the VirtualEnv setup to leave
the |
| 109 // directory read/write. This makes it easier to manage, and is safe sin
ce it |
| 110 // is not a production directory. |
| 111 testLeaveReadWrite bool |
| 112 } |
| 113 |
| 114 // setupEnv processes the config, validating and, where appropriate, populating |
| 115 // any components. Upon success, it returns a configured Env instance. |
| 116 // |
| 117 // The returned Env instance may or may not actually exist. Setup must be called |
| 118 // prior to using it. |
| 119 func (cfg *Config) setupEnv(c context.Context) (*Env, error) { |
| 120 // We MUST have a package loader. |
| 121 if cfg.Loader == nil { |
| 122 return nil, errors.New("no package loader provided") |
| 123 } |
| 124 |
| 125 // Resolve our base directory, if one is not supplied. |
| 126 if cfg.BaseDir == "" { |
| 127 // Use one in a temporary directory. |
| 128 cfg.BaseDir = filepath.Join(os.TempDir(), "vpython") |
| 129 logging.Debugf(c, "Using tempdir-relative environment root: %s",
cfg.BaseDir) |
| 130 } |
| 131 if err := filesystem.AbsPath(&cfg.BaseDir); err != nil { |
| 132 return nil, errors.Annotate(err).Reason("failed to resolve absol
ute path of base directory").Err() |
| 133 } |
| 134 |
| 135 // Enforce maximum path length. |
| 136 if cfg.MaxScriptPathLen > 0 { |
| 137 if longestPath := longestGeneratedScriptPath(cfg.BaseDir); longe
stPath != "" { |
| 138 longestPathLen := utf8.RuneCountInString(longestPath) |
| 139 if longestPathLen > cfg.MaxScriptPathLen { |
| 140 return nil, errors.Reason("expected deepest path
length (%(len)d) exceeds threshold (%(threshold)d)"). |
| 141 D("len", longestPathLen). |
| 142 D("threshold", cfg.MaxScriptPathLen). |
| 143 D("longestPath", longestPath). |
| 144 Err() |
| 145 } |
| 146 } |
| 147 } |
| 148 |
| 149 if err := filesystem.MakeDirs(cfg.BaseDir); err != nil { |
| 150 return nil, errors.Annotate(err).Reason("could not create enviro
nment root: %(root)s"). |
| 151 D("root", cfg.BaseDir). |
| 152 Err() |
| 153 } |
| 154 |
| 155 // Determine our common loader root. |
| 156 if cfg.LoaderResolveRoot == "" { |
| 157 cfg.LoaderResolveRoot = filepath.Join(cfg.BaseDir, ".package_loa
der") |
| 158 } |
| 159 |
| 160 // Ensure and normalize our specification file. |
| 161 if cfg.Spec == nil { |
| 162 cfg.Spec = &vpython.Spec{} |
| 163 } |
| 164 if err := spec.Normalize(cfg.Spec); err != nil { |
| 165 return nil, errors.Annotate(err).Reason("invalid specification")
.Err() |
| 166 } |
| 167 |
| 168 // Choose our VirtualEnv package. |
| 169 if cfg.Spec.Virtualenv == nil { |
| 170 cfg.Spec.Virtualenv = &cfg.Package |
| 171 } |
| 172 |
| 173 if err := cfg.resolvePackages(c); err != nil { |
| 174 return nil, errors.Annotate(err).Reason("failed to resolve packa
ges").Err() |
| 175 } |
| 176 |
| 177 if err := cfg.resolvePythonInterpreter(c); err != nil { |
| 178 return nil, errors.Annotate(err).Reason("failed to resolve syste
m Python interpreter").Err() |
| 179 } |
| 180 |
| 181 // Generate our enviornment name based on the deterministic hash of its |
| 182 // fully-resolved specification. |
| 183 envName := spec.Hash(cfg.Spec) |
| 184 if cfg.MaxHashLen > 0 && len(envName) > cfg.MaxHashLen { |
| 185 envName = envName[:cfg.MaxHashLen] |
| 186 } |
| 187 return cfg.envForName(envName), nil |
| 188 } |
| 189 |
| 190 // Prune performs a pruning round on the environment set described by this |
| 191 // Config. |
| 192 func (cfg *Config) Prune(c context.Context) error { |
| 193 if err := prune(c, cfg, ""); err != nil { |
| 194 return errors.Annotate(err).Err() |
| 195 } |
| 196 return nil |
| 197 } |
| 198 |
| 199 // envForExisting creates an Env for a named directory. |
| 200 func (cfg *Config) envForName(name string) *Env { |
| 201 // Env-specific root directory: <BaseDir>/<name> |
| 202 venvRoot := filepath.Join(cfg.BaseDir, name) |
| 203 return &Env{ |
| 204 Config: cfg, |
| 205 Root: venvRoot, |
| 206 Python: venvBinPath(venvRoot, "python"), |
| 207 SpecPath: filepath.Join(venvRoot, "enviornment.pb.txt"), |
| 208 |
| 209 name: name, |
| 210 lockPath: filepath.Join(cfg.BaseDir, fmt.Sprintf(".%s.lo
ck", name)), |
| 211 completeFlagPath: filepath.Join(venvRoot, "complete.flag"), |
| 212 } |
| 213 } |
| 214 |
| 215 func (cfg *Config) resolvePackages(c context.Context) error { |
| 216 // Create a single package list. Our VirtualEnv will be index 0 (need |
| 217 // this so we can back-port it into its VirtualEnv property). |
| 218 packages := make([]*vpython.Spec_Package, 1, 1+len(cfg.Spec.Wheel)) |
| 219 packages[0] = cfg.Spec.Virtualenv |
| 220 packages = append(packages, cfg.Spec.Wheel...) |
| 221 |
| 222 // Resolve our packages. Because we're using pointers, the in-place |
| 223 // updating will update the actual spec file! |
| 224 if err := cfg.Loader.Resolve(c, cfg.LoaderResolveRoot, packages); err !=
nil { |
| 225 return errors.Annotate(err).Reason("failed to resolve packages")
.Err() |
| 226 } |
| 227 return nil |
| 228 } |
| 229 |
| 230 func (cfg *Config) resolvePythonInterpreter(c context.Context) error { |
| 231 specVers, err := python.ParseVersion(cfg.Spec.PythonVersion) |
| 232 if err != nil { |
| 233 return errors.Annotate(err).Reason("failed to parse Python versi
on from: %(value)q"). |
| 234 D("value", cfg.Spec.PythonVersion). |
| 235 Err() |
| 236 } |
| 237 |
| 238 var i *python.Interpreter |
| 239 if cfg.Python == "" { |
| 240 // No explicitly-specified Python path. Determine one based on t
he |
| 241 // specification. |
| 242 if i, err = python.Find(c, specVers); err != nil { |
| 243 return errors.Annotate(err).Reason("could not find Pytho
n for: %(vers)s"). |
| 244 D("vers", specVers). |
| 245 Err() |
| 246 } |
| 247 cfg.Python = i.Python |
| 248 } else { |
| 249 i = &python.Interpreter{ |
| 250 Python: cfg.Python, |
| 251 } |
| 252 } |
| 253 |
| 254 // Confirm that the version of the interpreter matches that which is |
| 255 // expected. |
| 256 interpreterVers, err := i.GetVersion(c) |
| 257 if err != nil { |
| 258 return errors.Annotate(err).Reason("failed to determine Python v
ersion for: %(python)s"). |
| 259 D("python", cfg.Python). |
| 260 Err() |
| 261 } |
| 262 if !specVers.IsSatisfiedBy(interpreterVers) { |
| 263 return errors.Reason("supplied Python version (%(supplied)s) doe
sn't match specification (%(spec)s)"). |
| 264 D("supplied", interpreterVers). |
| 265 D("spec", specVers). |
| 266 Err() |
| 267 } |
| 268 cfg.Spec.PythonVersion = interpreterVers.String() |
| 269 |
| 270 // Resolve to absolute path. |
| 271 if err := filesystem.AbsPath(&cfg.Python); err != nil { |
| 272 return errors.Annotate(err).Reason("could not get absolute path
for: %(python)s"). |
| 273 D("python", cfg.Python). |
| 274 Err() |
| 275 } |
| 276 logging.Debugf(c, "Resolved system Python interpreter (%s): %s", cfg.Spe
c.PythonVersion, cfg.Python) |
| 277 return nil |
| 278 } |
| 279 |
| 280 func (cfg *Config) systemInterpreter() *python.Command { |
| 281 if cfg.Python == "" { |
| 282 return nil |
| 283 } |
| 284 i := python.Interpreter{ |
| 285 Python: cfg.Python, |
| 286 } |
| 287 cmd := i.Command() |
| 288 cmd.Isolated = true |
| 289 return cmd |
| 290 } |
| OLD | NEW |