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

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

Issue 2699063004: vpython: Add VirtualEnv creation package. (Closed)
Patch Set: remake binaries Created 3 years, 10 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
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 "os"
9 "path/filepath"
10 "time"
11
12 "github.com/luci/luci-go/vpython/api/env"
13 "github.com/luci/luci-go/vpython/filesystem"
14 "github.com/luci/luci-go/vpython/python"
15 "github.com/luci/luci-go/vpython/spec"
16 "github.com/luci/luci-go/vpython/wheel"
17
18 "github.com/luci/luci-go/common/clock"
19 "github.com/luci/luci-go/common/errors"
20 "github.com/luci/luci-go/common/logging"
21
22 "github.com/danjacques/gofslock/fslock"
23 "golang.org/x/net/context"
24 )
25
26 const lockHeldDelay = 5 * time.Second
27
28 // blocker is an fslock.Blocker implementation that sleeps lockHeldDelay in
29 // between attempts.
30 func blocker(c context.Context) fslock.Blocker {
31 return func() error {
32 logging.Debugf(c, "Lock is currently held. Sleeping %v and retry ing...", lockHeldDelay)
33 clock.Sleep(c, lockHeldDelay)
34 return nil
35 }
36 }
37
38 // EnvRootFromSpecPath calculates the environment root from an exported
39 // environment specification file path.
40 //
41 // The specification path is: <EnvRoot>/<SpecHash>/<spec>, so our EnvRoot
42 // is two directories up.
43 //
44 // We export EnvSpecPath as an asbolute path. However, since someone else
45 // could have overridden it or exported their own, let's make sure.
46 func EnvRootFromSpecPath(path string) (string, error) {
47 if err := filesystem.AbsPath(&path); err != nil {
48 return "", errors.Annotate(err).
49 Reason("failed to get absolute path for specification fi le path: %(path)s").
50 Err()
51 }
52 return filepath.Dir(filepath.Dir(path)), nil
53 }
54
55 // Env is a fully set-up Python virtual enviornment. It is configured
56 // based on the contents of an env.Spec file by Setup.
57 //
58 // Env should not be instantiated directly; it must be created by calling
59 // Config.Env.
60 //
61 // All paths in Env are absolute.
62 type Env struct {
63 // Config is this Env's Config, fully-resolved.
64 Config *Config
65
66 // Root is the Env container's root directory path.
67 Root string
68
69 // Python is the path to the Env Python interpreter.
70 Python string
71
72 // SepcPath is the path to the specification file that was used to const ruct
73 // this enviornment. It will be in text protobuf format, and, therefore,
74 // suitable for input to other "vpython" invocations.
75 SpecPath string
76
77 // name is the hash of the specification file for this Env.
78 name string
79 // lockPath is the path to this Env-specific lock file. It will be at:
80 // "<baseDir>/.<name>.lock".
81 lockPath string
82 // completeFlagPath is the path to this Env's complete flag.
83 // It will be at "<Root>/complete.flag".
84 completeFlagPath string
85 }
86
87 // Setup creates a new Env.
88 //
89 // It will lock around the Env to ensure that multiple processes do not
90 // conflict with each other. If a Env for this specification already
91 // exists, it will be used directly without any additional setup.
92 //
93 // If another process holds the lock and blocking is true, we will wait for our
94 // turn at the lock. Otherwise, we will return immediately with a locking error.
95 func (e *Env) Setup(c context.Context, blocking bool) error {
96 if err := e.setupImpl(c, blocking); err != nil {
97 return errors.Annotate(err).Err()
98 }
99
100 // Perform a pruning round. Failure is non-fatal.
101 if perr := prune(c, e.Config, e.name); perr != nil {
102 logging.WithError(perr).Warningf(c, "Failed to perform pruning r ound after initialization.")
103 }
104 return nil
105 }
106
107 func (e *Env) setupImpl(c context.Context, blocking bool) error {
108 // Repeatedly try and create our Env. We do this so that if we
109 // encounter a lock, we will let the other process finish and try and le verage
110 // its success.
111 for {
112 // Fast path: if our complete flag is present, assume that the e nvironment
113 // is setup and complete. No locking or additional work necessar y.
114 if _, err := os.Stat(e.completeFlagPath); err == nil {
115 logging.Debugf(c, "Completion flag found! Environment is set-up: %s", e.completeFlagPath)
116
117 // Update the complete flag so the timestamp reflects ou r usage of it.
118 // This is non-fatal if it fails.
iannucci 2017/02/23 00:54:21 couldn't this mean that we return and this env mig
dnj 2017/02/23 20:38:50 Hm, yeah that sounds reasonable.
119 if err := e.touchCompleteFlag(); err != nil {
120 logging.WithError(err).Warningf(c, "Failed to up date complete flag.")
121 }
122
123 return nil
124 }
125
126 // We will be creating the Env. We will to lock around a file fo r this
iannucci 2017/02/23 00:54:21 'will to lock'
dnj 2017/02/23 20:38:49 Done.
127 // Env hash so that any other processes that may be trying to
128 // simultaneously create a Env will be forced to wait.
129 err := fslock.With(e.lockPath, func() error {
130 // Mark that we hit some lock contention. If we did, we will try again
131 // from scratch.
132 if err := e.createLocked(c); err != nil {
133 return errors.Annotate(err).Reason("failed to cr eate new VirtualEnv").Err()
134 }
135 return nil
136 })
137 switch err {
138 case nil:
139 // Successfully created the environment! Mark this with a completion flag.
140 if err := e.touchCompleteFlag(); err != nil {
141 return errors.Annotate(err).Reason("failed to cr eate complete flag").Err()
iannucci 2017/02/23 00:54:21 shouldn't we loop? at least up to some limit?
dnj 2017/02/23 20:38:49 I don't think so. Touching a file should be pretty
142 }
143 return nil
144
145 case fslock.ErrLockHeld:
146 if !blocking {
147 return errors.Annotate(err).Reason("VirtualEnv l ock is currently held (non-blocking)").Err()
148 }
149
150 // Some other process holds the lock. Sleep a little and retry.
151 logging.Warningf(c, "VirtualEnv lock is currently held. Retrying after delay (%s)...",
152 lockHeldDelay)
153 if tr := clock.Sleep(c, lockHeldDelay); tr.Incomplete() {
154 return tr.Err
155 }
156 continue
157
158 default:
159 return errors.Annotate(err).Reason("failed to create Vir tualEnv").Err()
160 }
161 }
162 }
163
164 // Delete deletes this enviornment, if it exists.
165 func (e *Env) Delete(c context.Context) error {
166 err := fslock.WithBlocking(e.lockPath, blocker(c), func() error {
167 if err := e.deleteLocked(c); err != nil {
168 return errors.Annotate(err).Err()
169 }
170 return nil
171 })
172 if err != nil {
173 errors.Annotate(err).Reason("failed to delete enviornment").Err( )
174 }
175 return nil
176 }
177
178 func (e *Env) createLocked(c context.Context) error {
179 // If our root directory already exists, delete it.
180 if _, err := os.Stat(e.Root); err == nil {
181 logging.Warningf(c, "Deleting existing VirtualEnv: %s", e.Root)
182 if err := filesystem.RemoveAll(e.Root); err != nil {
183 return errors.Reason("failed to remove existing root").E rr()
184 }
185 }
186
187 // Make sure our environment's base directory exists.
188 if err := filesystem.MakeDirs(e.Root); err != nil {
189 return errors.Annotate(err).Reason("failed to create environment root").Err()
190 }
191 logging.Infof(c, "Using virtual environment root: %s", e.Root)
192
193 // Build our package list. Always install our base VirtualEnv package.
194 // For resolution purposes, our VirtualEnv package will be index 0.
iannucci 2017/02/23 00:54:21 why? why not just sort them? The Spec_Package are
dnj 2017/02/23 20:38:49 Doesn't really matter. The comment isn't too usefu
195 packages := make([]*env.Spec_Package, 1, 1+len(e.Config.Spec.Wheel))
196 packages[0] = e.Config.Spec.Virtualenv
197 packages = append(packages, e.Config.Spec.Wheel...)
198
199 bootstrapDir := filepath.Join(e.Root, ".vpython_bootstrap")
200 pkgDir := filepath.Join(bootstrapDir, "packages")
iannucci 2017/02/23 00:54:21 we should make sure to delete this directory after
dnj 2017/02/23 20:38:50 We do, yeah. See "finalize".
201 if err := filesystem.MakeDirs(pkgDir); err != nil {
202 return errors.Annotate(err).Reason("could not create bootstrap p ackages directory").Err()
203 }
204
205 if err := e.downloadPackages(c, pkgDir, packages); err != nil {
206 return errors.Annotate(err).Reason("failed to download packages" ).Err()
207 }
208
209 // Installing base VirtualEnv.
210 if err := e.installVirtualEnv(c, pkgDir); err != nil {
211 return errors.Annotate(err).Reason("failed to install VirtualEnv ").Err()
212 }
213
214 // Download our wheel files.
iannucci 2017/02/23 00:54:21 Install?
dnj 2017/02/23 20:38:50 Done.
215 if len(e.Config.Spec.Wheel) > 0 {
216 // Install wheels into our VirtualEnv.
217 if err := e.installWheels(c, bootstrapDir, pkgDir); err != nil {
218 return errors.Annotate(err).Reason("failed to install wh eels").Err()
219 }
220 }
221
222 // Write our specification file.
223 if err := spec.Write(e.Config.Spec, e.SpecPath); err != nil {
224 return errors.Annotate(err).Reason("failed to write spec file to : %(path)s").
225 D("path", e.SpecPath).
226 Err()
227 }
228 logging.Debugf(c, "Wrote specification file to: %s", e.SpecPath)
229
230 // Finalize our VirtualEnv for bootstrap execution.
231 if err := e.finalize(c, bootstrapDir); err != nil {
232 return errors.Annotate(err).Reason("failed to prepare VirtualEnv ").Err()
233 }
234
235 return nil
236 }
237
238 func (e *Env) downloadPackages(c context.Context, dst string, packages []*env.Sp ec_Package) error {
239 // Create a wheel sub-directory underneath of root.
240 logging.Debugf(c, "Loading %d package(s) into: %s", len(packages), dst)
241 if err := e.Config.Loader.Ensure(c, dst, packages); err != nil {
242 return errors.Annotate(err).Reason("failed to download packages" ).Err()
iannucci 2017/02/23 00:54:21 pro tip: errors.Annotate(nil).Reason(...).D(...).E
dnj 2017/02/23 20:38:50 For me, this is too much on a single line :)
243 }
244 return nil
245 }
246
247 func (e *Env) installVirtualEnv(c context.Context, pkgDir string) error {
248 // Create our VirtualEnv package staging sub-directory underneath of roo t.
249 bsDir := filepath.Join(e.Root, ".virtualenv")
250 if err := filesystem.MakeDirs(bsDir); err != nil {
251 return errors.Annotate(err).Reason("failed to create VirtualEnv bootstrap directory").
252 D("path", bsDir).
253 Err()
254 }
255
256 // Identify the virtualenv directory: will have "virtualenv-" prefix.
257 matches, err := filepath.Glob(filepath.Join(pkgDir, "virtualenv-*"))
258 if err != nil {
259 return errors.Annotate(err).Reason("failed to glob for 'virtuale nv-' directory").Err()
260 }
261 if len(matches) == 0 {
262 return errors.Reason("no 'virtualenv-' directory provided by pac kage").Err()
263 }
264
265 logging.Debugf(c, "Creating VirtualEnv at: %s", e.Root)
266 i := e.Config.systemInterpreter()
267 i.WorkDir = matches[0]
268 err = i.Run(c,
269 "virtualenv.py",
270 "--no-download",
271 e.Root)
272 if err != nil {
273 return errors.Annotate(err).Reason("failed to create VirtualEnv" ).Err()
274 }
275
276 return nil
277 }
278
279 func (e *Env) installWheels(c context.Context, bootstrapDir, pkgDir string) erro r {
280 // Identify all downloaded wheels and parse them.
281 wheels, err := wheel.ScanDir(pkgDir)
282 if err != nil {
283 return errors.Annotate(err).Reason("failed to load wheels").Err( )
284 }
285
286 // Build a "wheel" requirements file.
287 reqPath := filepath.Join(bootstrapDir, "requirements.txt")
288 logging.Debugf(c, "Rendering requirements file to: %s", reqPath)
289 if err := wheel.WriteRequirementsFile(reqPath, wheels); err != nil {
290 return errors.Annotate(err).Reason("failed to render requirement s file").Err()
291 }
292
293 cmd := e.venvInterpreter()
294 err = cmd.Run(c,
295 "-m", "pip",
296 "install",
297 "--use-wheel",
298 "--compile",
299 "--no-index",
300 "--find-links", pkgDir,
301 "--requirement", reqPath)
302 if err != nil {
303 return errors.Annotate(err).Reason("failed to install wheels").E rr()
304 }
305 return nil
306 }
307
308 func (e *Env) finalize(c context.Context, bootstrapDir string) error {
309 // Uninstall "pip" and "wheel", preventing (easy) augmentation of the
310 // enviornment.
311 cmd := e.venvInterpreter()
312 err := cmd.Run(c,
313 "-m", "pip",
314 "uninstall",
315 "--quiet",
316 "--yes",
317 "pip", "wheel")
318 if err != nil {
319 return errors.Annotate(err).Reason("failed to install wheels").E rr()
320 }
321
322 // Delete our bootstrap directory (non-fatal).
iannucci 2017/02/23 00:54:21 ah, this is the place we delete it
dnj 2017/02/23 20:38:50 Acknowledged.
323 if err := filesystem.RemoveAll(bootstrapDir); err != nil {
324 logging.WithError(err).Warningf(c, "Failed to delete bootstrap d irectory: %s", bootstrapDir)
325 }
326
327 // Change all files to read-only, except:
328 // - Our root directory, which must be writable in order to update our
329 // completion flag.
330 // - Our completion flag, which must be trivially re-writable.
331 err = filesystem.MakeReadOnly(e.Root, func(path string) bool {
332 switch path {
333 case e.Root, e.completeFlagPath:
334 return false
335 default:
336 return true
337 }
338 })
339 if err != nil {
340 return errors.Annotate(err).Reason("failed to mark environment r ead-only").Err()
341 }
342 return nil
343 }
344
345 func (e *Env) venvInterpreter() *python.Command {
346 cmd := e.InterpreterCommand()
347 cmd.WorkDir = e.Root
348 return cmd
349 }
350
351 // InterpreterCommand returns a Python interpreter Command pointing to the
352 // VirtualEnv's Python installation.
353 func (e *Env) InterpreterCommand() *python.Command {
354 i := python.Interpreter{
355 Python: e.Python,
356 }
357 cmd := i.Command()
358 cmd.Isolated = true
359 return cmd
360 }
361
362 // touchCompleteFlag touches the complete flag, creating it and/or updating its
363 // timestamp.
364 //
365 // This is safe to call without the lock held, since worst-case an update is
366 // overwritten if contested.
367 func (e *Env) touchCompleteFlag() error {
368 if err := filesystem.Touch(e.completeFlagPath, 0644); err != nil {
369 return errors.Annotate(err).Err()
370 }
371 return nil
372 }
373
374 func (e *Env) deleteLocked(c context.Context) error {
375 // Delete our environment directory.
376 if err := filesystem.RemoveAll(e.Root); err != nil {
377 return errors.Annotate(err).Reason("failed to delete environment root").Err()
378 }
379
380 // Delete our lock path.
381 if err := os.Remove(e.lockPath); err != nil {
382 return errors.Annotate(err).Reason("failed to delete lock").Err( )
383 }
384 return nil
385 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698