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

Side by Side Diff: vpython/venv/venv.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/test_data/shirt.src/shirt/pants.py ('k') | vpython/venv/venv_resources_test.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 "io/ioutil"
9 "os"
10 "path/filepath"
11 "sync"
12 "time"
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 "github.com/luci/luci-go/vpython/wheel"
18
19 "github.com/luci/luci-go/common/clock"
20 "github.com/luci/luci-go/common/errors"
21 "github.com/luci/luci-go/common/logging"
22 "github.com/luci/luci-go/common/system/filesystem"
23
24 "github.com/danjacques/gofslock/fslock"
25 "golang.org/x/net/context"
26 )
27
28 const (
29 lockHeldDelay = 5 * time.Second
30 defaultKeepAliveInterval = 1 * time.Hour
31 )
32
33 // blocker is an fslock.Blocker implementation that sleeps lockHeldDelay in
34 // between attempts.
35 func blocker(c context.Context) fslock.Blocker {
36 return func() error {
37 logging.Debugf(c, "Lock is currently held. Sleeping %v and retry ing...", lockHeldDelay)
38 clock.Sleep(c, lockHeldDelay)
39 return nil
40 }
41 }
42
43 func withTempDir(l logging.Logger, prefix string, fn func(string) error) error {
44 tdir, err := ioutil.TempDir("", prefix)
45 if err != nil {
46 return errors.Annotate(err).Reason("failed to create temporary d irectory").Err()
47 }
48 defer func() {
49 if err := filesystem.RemoveAll(tdir); err != nil {
50 l.Warningf("Failed to remove temporary directory: %s", e rr)
51 }
52 }()
53
54 return fn(tdir)
55 }
56
57 // EnvRootFromSpecPath calculates the environment root from an exported
58 // environment specification file path.
59 //
60 // The specification path is: <EnvRoot>/<SpecHash>/<spec>, so our EnvRoot
61 // is two directories up.
62 //
63 // We export EnvSpecPath as an asbolute path. However, since someone else
64 // could have overridden it or exported their own, let's make sure.
65 func EnvRootFromSpecPath(path string) (string, error) {
66 if err := filesystem.AbsPath(&path); err != nil {
67 return "", errors.Annotate(err).
68 Reason("failed to get absolute path for specification fi le path: %(path)s").
69 Err()
70 }
71 return filepath.Dir(filepath.Dir(path)), nil
72 }
73
74 // With creates a new Env and executes "fn" with assumed ownership of that Env.
75 //
76 // The Context passed to "fn" will be cancelled if we lose perceived ownership
77 // of the configured environment. This is not an expected scenario, and should
78 // be considered an error condition. The Env passed to "fn" is valid only for
79 // the duration of the callback.
80 //
81 // It will lock around the VirtualEnv to ensure that multiple processes do not
82 // conflict with each other. If a VirtualEnv for this specification already
83 // exists, it will be used directly without any additional setup.
84 //
85 // If another process holds the lock, With will return an error (if blocking is
86 // false) or try again until it obtains the lock (if blocking is true).
87 func With(c context.Context, cfg Config, blocking bool, fn func(context.Context, *Env) error) error {
88 e, err := cfg.setupEnv(c)
89 if err != nil {
90 return errors.Annotate(err).Reason("failed to resolve VirtualEnv ").Err()
91 }
92
93 // Setup the environment.
94 if err := e.setupImpl(c, blocking); err != nil {
95 return errors.Annotate(err).Err()
96 }
97
98 // Perform a pruning round. Failure is non-fatal.
99 if perr := prune(c, e.Config, e.name); perr != nil {
100 logging.WithError(perr).Warningf(c, "Failed to perform pruning r ound after initialization.")
101 }
102
103 // Set-up our environment Context.
104 var monitorWG sync.WaitGroup
105 c, cancelFunc := context.WithCancel(c)
106 defer func() {
107 // Cancel our Context and reap our monitor goroutine(s).
108 cancelFunc()
109 monitorWG.Wait()
110 }()
111
112 // If we have a prune threshold, touch the "complete flag" periodically.
113 //
114 // Our refresh interval must be at least 1/4 the prune threshold, but id eally
115 // would be higher.
116 if interval := e.Config.PruneThreshold / 4; interval > 0 {
117 if interval > defaultKeepAliveInterval {
118 interval = defaultKeepAliveInterval
119 }
120
121 monitorWG.Add(1)
122 go func() {
123 defer monitorWG.Done()
124 e.keepAliveMonitor(c, interval, cancelFunc)
125 }()
126 }
127
128 return fn(c, e)
129 }
130
131 // Env is a fully set-up Python virtual environment. It is configured
132 // based on the contents of an vpython.Spec file by Setup.
133 //
134 // Env should not be instantiated directly; it must be created by calling
135 // Config.Env.
136 //
137 // All paths in Env are absolute.
138 type Env struct {
139 // Config is this Env's Config, fully-resolved.
140 Config *Config
141
142 // Root is the Env container's root directory path.
143 Root string
144
145 // Python is the path to the Env Python interpreter.
146 Python string
147
148 // SepcPath is the path to the specification file that was used to const ruct
149 // this environment. It will be in text protobuf format, and, therefore,
150 // suitable for input to other "vpython" invocations.
151 SpecPath string
152
153 // name is the hash of the specification file for this Env.
154 name string
155 // lockPath is the path to this Env-specific lock file. It will be at:
156 // "<baseDir>/.<name>.lock".
157 lockPath string
158 // completeFlagPath is the path to this Env's complete flag.
159 // It will be at "<Root>/complete.flag".
160 completeFlagPath string
161 }
162
163 // InterpreterCommand returns a Python interpreter Command pointing to the
164 // VirtualEnv's Python installation.
165 func (e *Env) InterpreterCommand() *python.Command {
166 i := python.Interpreter{
167 Python: e.Python,
168 }
169 cmd := i.Command()
170 cmd.Isolated = true
171 return cmd
172 }
173
174 func (e *Env) setupImpl(c context.Context, blocking bool) error {
175 // Repeatedly try and create our Env. We do this so that if we
176 // encounter a lock, we will let the other process finish and try and le verage
177 // its success.
178 for {
179 // We will be creating the Env. We will lock around a file for t his Env hash
180 // so that any other processes that may be trying to simultaneou sly
181 // manipulate Env will be forced to wait.
182 err := fslock.With(e.lockPath, func() error {
183 // Check for completion flag.
184 if _, err := os.Stat(e.completeFlagPath); err != nil {
185 // No complete flag. Create a new VirtualEnv her e.
186 if err := e.createLocked(c); err != nil {
187 return errors.Annotate(err).Reason("fail ed to create new VirtualEnv").Err()
188 }
189
190 // Successfully created the environment! Mark th is with a completion
191 // flag.
192 if err := e.touchCompleteFlagLocked(); err != ni l {
193 return errors.Annotate(err).Reason("fail ed to create complete flag").Err()
194 }
195 } else {
196 // Fast path: if our complete flag is present, a ssume that the
197 // environment is setup and complete. No locking or additional work necessary.
198 logging.Debugf(c, "Completion flag found! Enviro nment is set-up: %s", e.completeFlagPath)
199
200 // Mark that we care about this enviornment. Thi s is non-fatal if it
201 // fails.
202 if err := e.touchCompleteFlagLocked(); err != ni l {
203 logging.WithError(err).Warningf(c, "Fail ed to update existing complete flag.")
204 }
205 }
206
207 return nil
208 })
209 switch err {
210 case nil:
211 return nil
212
213 case fslock.ErrLockHeld:
214 if !blocking {
215 return errors.Annotate(err).Reason("VirtualEnv l ock is currently held (non-blocking)").Err()
216 }
217
218 // Some other process holds the lock. Sleep a little and retry.
219 logging.Warningf(c, "VirtualEnv lock is currently held. Retrying after delay (%s)...", lockHeldDelay)
220 if tr := clock.Sleep(c, lockHeldDelay); tr.Incomplete() {
221 return tr.Err
222 }
223 continue
224
225 default:
226 return errors.Annotate(err).Reason("failed to create Vir tualEnv").Err()
227 }
228 }
229 }
230
231 func (e *Env) withLockBlocking(c context.Context, fn func() error) error {
232 return fslock.WithBlocking(e.lockPath, blocker(c), fn)
233 }
234
235 // Delete deletes this environment, if it exists.
236 func (e *Env) Delete(c context.Context) error {
237 err := e.withLockBlocking(c, func() error {
238 if err := e.deleteLocked(c); err != nil {
239 return errors.Annotate(err).Err()
240 }
241 return nil
242 })
243 if err != nil {
244 errors.Annotate(err).Reason("failed to delete environment").Err( )
245 }
246 return nil
247 }
248
249 func (e *Env) createLocked(c context.Context) error {
250 // If our root directory already exists, delete it.
251 if _, err := os.Stat(e.Root); err == nil {
252 logging.Warningf(c, "Deleting existing VirtualEnv: %s", e.Root)
253 if err := filesystem.RemoveAll(e.Root); err != nil {
254 return errors.Reason("failed to remove existing root").E rr()
255 }
256 }
257
258 // Make sure our environment's base directory exists.
259 if err := filesystem.MakeDirs(e.Root); err != nil {
260 return errors.Annotate(err).Reason("failed to create environment root").Err()
261 }
262 logging.Infof(c, "Using virtual environment root: %s", e.Root)
263
264 // Build our package list. Always install our base VirtualEnv package.
265 packages := make([]*vpython.Spec_Package, 1, 1+len(e.Config.Spec.Wheel))
266 packages[0] = e.Config.Spec.Virtualenv
267 packages = append(packages, e.Config.Spec.Wheel...)
268
269 // Create a directory to bootstrap VirtualEnv from.
270 //
271 // This directory will be a very short-named temporary directory. This i s
272 // because it really quickly runs up into traditional Windows path limit ations
273 // when ZIP-importing sub-sub-sub-sub-packages (e.g., pip, requests, etc .).
274 //
275 // We will clean this directory up on termination.
276 err := withTempDir(logging.Get(c), "vpython_bootstrap", func(bootstrapDi r string) error {
277 pkgDir := filepath.Join(bootstrapDir, "packages")
278 if err := filesystem.MakeDirs(pkgDir); err != nil {
279 return errors.Annotate(err).Reason("could not create boo tstrap packages directory").Err()
280 }
281
282 if err := e.downloadPackages(c, pkgDir, packages); err != nil {
283 return errors.Annotate(err).Reason("failed to download p ackages").Err()
284 }
285
286 // Installing base VirtualEnv.
287 if err := e.installVirtualEnv(c, pkgDir); err != nil {
288 return errors.Annotate(err).Reason("failed to install Vi rtualEnv").Err()
289 }
290
291 // Install our wheel files.
292 if len(e.Config.Spec.Wheel) > 0 {
293 // Install wheels into our VirtualEnv.
294 if err := e.installWheels(c, bootstrapDir, pkgDir); err != nil {
295 return errors.Annotate(err).Reason("failed to in stall wheels").Err()
296 }
297 }
298
299 return nil
300 })
301 if err != nil {
302 return err
303 }
304
305 // Write our specification file.
306 if err := spec.Write(e.Config.Spec, e.SpecPath); err != nil {
307 return errors.Annotate(err).Reason("failed to write spec file to : %(path)s").
308 D("path", e.SpecPath).
309 Err()
310 }
311 logging.Debugf(c, "Wrote specification file to: %s", e.SpecPath)
312
313 // Finalize our VirtualEnv for bootstrap execution.
314 if err := e.finalize(c); err != nil {
315 return errors.Annotate(err).Reason("failed to prepare VirtualEnv ").Err()
316 }
317
318 return nil
319 }
320
321 func (e *Env) downloadPackages(c context.Context, dst string, packages []*vpytho n.Spec_Package) error {
322 // Create a wheel sub-directory underneath of root.
323 logging.Debugf(c, "Loading %d package(s) into: %s", len(packages), dst)
324 if err := e.Config.Loader.Ensure(c, dst, packages); err != nil {
325 return errors.Annotate(err).Reason("failed to download packages" ).Err()
326 }
327 return nil
328 }
329
330 func (e *Env) installVirtualEnv(c context.Context, pkgDir string) error {
331 // Create our VirtualEnv package staging sub-directory underneath of roo t.
332 bsDir := filepath.Join(e.Root, ".virtualenv")
333 if err := filesystem.MakeDirs(bsDir); err != nil {
334 return errors.Annotate(err).Reason("failed to create VirtualEnv bootstrap directory").
335 D("path", bsDir).
336 Err()
337 }
338
339 // Identify the virtualenv directory: will have "virtualenv-" prefix.
340 matches, err := filepath.Glob(filepath.Join(pkgDir, "virtualenv-*"))
341 if err != nil {
342 return errors.Annotate(err).Reason("failed to glob for 'virtuale nv-' directory").Err()
343 }
344 if len(matches) == 0 {
345 return errors.Reason("no 'virtualenv-' directory provided by pac kage").Err()
346 }
347
348 logging.Debugf(c, "Creating VirtualEnv at: %s", e.Root)
349 i := e.Config.systemInterpreter()
350 i.WorkDir = matches[0]
351 err = i.Run(c,
352 "virtualenv.py",
353 "--no-download",
354 e.Root)
355 if err != nil {
356 return errors.Annotate(err).Reason("failed to create VirtualEnv" ).Err()
357 }
358
359 return nil
360 }
361
362 func (e *Env) installWheels(c context.Context, bootstrapDir, pkgDir string) erro r {
363 // Identify all downloaded wheels and parse them.
364 wheels, err := wheel.ScanDir(pkgDir)
365 if err != nil {
366 return errors.Annotate(err).Reason("failed to load wheels").Err( )
367 }
368
369 // Build a "wheel" requirements file.
370 reqPath := filepath.Join(bootstrapDir, "requirements.txt")
371 logging.Debugf(c, "Rendering requirements file to: %s", reqPath)
372 if err := wheel.WriteRequirementsFile(reqPath, wheels); err != nil {
373 return errors.Annotate(err).Reason("failed to render requirement s file").Err()
374 }
375
376 cmd := e.venvInterpreter()
377 err = cmd.Run(c,
378 "-m", "pip",
379 "install",
380 "--use-wheel",
381 "--compile",
382 "--no-index",
383 "--find-links", pkgDir,
384 "--requirement", reqPath)
385 if err != nil {
386 return errors.Annotate(err).Reason("failed to install wheels").E rr()
387 }
388 return nil
389 }
390
391 func (e *Env) finalize(c context.Context) error {
392 // Uninstall "pip" and "wheel", preventing (easy) augmentation of the
393 // environment.
394 if !e.Config.testPreserveInstallationCapability {
395 cmd := e.venvInterpreter()
396 err := cmd.Run(c,
397 "-m", "pip",
398 "uninstall",
399 "--quiet",
400 "--yes",
401 "pip", "wheel")
402 if err != nil {
403 return errors.Annotate(err).Reason("failed to install wh eels").Err()
404 }
405 }
406
407 // Change all files to read-only, except:
408 // - Our root directory, which must be writable in order to update our
409 // completion flag.
410 // - Our completion flag, which must be trivially re-writable.
411 if !e.Config.testLeaveReadWrite {
412 err := filesystem.MakeReadOnly(e.Root, func(path string) bool {
413 switch path {
414 case e.Root, e.completeFlagPath:
415 return false
416 default:
417 return true
418 }
419 })
420 if err != nil {
421 return errors.Annotate(err).Reason("failed to mark envir onment read-only").Err()
422 }
423 }
424 return nil
425 }
426
427 func (e *Env) venvInterpreter() *python.Command {
428 cmd := e.InterpreterCommand()
429 cmd.WorkDir = e.Root
430 return cmd
431 }
432
433 // touchCompleteFlag touches the complete flag, creating it and/or
434 // updating its timestamp.
435 func (e *Env) touchCompleteFlag(c context.Context) error {
436 err := e.withLockBlocking(c, e.touchCompleteFlagLocked)
437 if err != nil {
438 return errors.Annotate(err).Reason("failed to touch complete fla g").Err()
439 }
440 return err
441 }
442
443 func (e *Env) touchCompleteFlagLocked() error {
444 if err := filesystem.Touch(e.completeFlagPath, time.Time{}, 0644); err ! = nil {
445 return errors.Annotate(err).Err()
446 }
447 return nil
448 }
449
450 func (e *Env) deleteLocked(c context.Context) error {
451 // Delete our environment directory.
452 if err := filesystem.RemoveAll(e.Root); err != nil {
453 return errors.Annotate(err).Reason("failed to delete environment root").Err()
454 }
455
456 // Delete our lock path.
457 if err := os.Remove(e.lockPath); err != nil {
458 return errors.Annotate(err).Reason("failed to delete lock").Err( )
459 }
460 return nil
461 }
462
463 // keepAliveMonitor periodically refreshes the environment's "completed" flag
464 // timestamp.
465 //
466 // It runs in its own goroutine.
467 func (e *Env) keepAliveMonitor(c context.Context, interval time.Duration, cancel Func context.CancelFunc) {
468 timer := clock.NewTimer(c)
469 defer timer.Stop()
470
471 for {
472 logging.Debugf(c, "Keep-alive: Sleeping %s...", interval)
473 timer.Reset(interval)
474 select {
475 case <-c.Done():
476 logging.WithError(c.Err()).Debugf(c, "Keep-alive: monito r's Context was cancelled.")
477 return
478
479 case tr := <-timer.GetC():
480 if tr.Err != nil {
481 logging.WithError(tr.Err).Debugf(c, "Keep-alive: monitor's timer finished.")
482 return
483 }
484 }
485
486 logging.Debugf(c, "Keep-alive: Updating completed flag timestamp .")
487 if err := e.touchCompleteFlag(c); err != nil {
488 errors.Log(c, errors.Annotate(err).Reason("failed to ref resh timestamp").Err())
489 cancelFunc()
490 return
491 }
492 }
493 }
OLDNEW
« no previous file with comments | « vpython/venv/test_data/shirt.src/shirt/pants.py ('k') | vpython/venv/venv_resources_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698