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