Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(86)

Side by Side Diff: vpython/venv/config.go

Issue 2699063004: vpython: Add VirtualEnv creation package. (Closed)
Patch Set: bootstrap straight out of tempdir Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « vpython/venv/.gitignore ('k') | vpython/venv/prune.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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/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 "github.com/luci/luci-go/common/system/filesystem"
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 }
OLDNEW
« no previous file with comments | « vpython/venv/.gitignore ('k') | vpython/venv/prune.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698