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

Unified 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « vpython/venv/.gitignore ('k') | vpython/venv/prune.go » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: vpython/venv/config.go
diff --git a/vpython/venv/config.go b/vpython/venv/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..d901533b658491b0c25d1bd1b85231c0835ff4ec
--- /dev/null
+++ b/vpython/venv/config.go
@@ -0,0 +1,290 @@
+// Copyright 2017 The LUCI Authors. All rights reserved.
+// Use of this source code is governed under the Apache License, Version 2.0
+// that can be found in the LICENSE file.
+
+package venv
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+ "unicode/utf8"
+
+ "github.com/luci/luci-go/vpython/api/vpython"
+ "github.com/luci/luci-go/vpython/python"
+ "github.com/luci/luci-go/vpython/spec"
+
+ "github.com/luci/luci-go/common/errors"
+ "github.com/luci/luci-go/common/logging"
+ "github.com/luci/luci-go/common/system/filesystem"
+
+ "golang.org/x/net/context"
+)
+
+// PackageLoader loads package information from a specification file's Package
+// message onto the local system.
+type PackageLoader interface {
+ // Resolve processes the supplied packages, updating their fields to their
+ // resolved values. Resolved packages must fully specify the package instance
+ // that is being deployed.
+ //
+ // If needed, resolution may use the supplied root path as a persistent
+ // working directory. This path may not exist; Resolve is responsible for
+ // creating it if needed.
+ //
+ // root, if used, must be safe for concurrent use.
+ Resolve(c context.Context, root string, packages []*vpython.Spec_Package) error
+
+ // Ensure installs the supplied packages into root.
+ //
+ // The packages will have been previously resolved via Resolve.
+ Ensure(c context.Context, root string, packages []*vpython.Spec_Package) error
+}
+
+// Config is the configuration for a managed VirtualEnv.
+//
+// A VirtualEnv is specified based on its resolved vpython.Spec.
+type Config struct {
+ // MaxHashLen is the maximum number of hash characters to use in VirtualEnv
+ // directory names.
+ MaxHashLen int
+
+ // BaseDir is the parent directory of all VirtualEnv.
+ BaseDir string
+
+ // Package is the VirtualEnv package to install. It must be non-nil and
+ // valid. It will be used if the environment specification doesn't supply an
+ // overriding one.
+ Package vpython.Spec_Package
+
+ // Python is the Python interpreter to use. If empty, one will be resolved
+ // based on the Spec and the current PATH.
+ Python string
+
+ // Spec is the specification file to use to construct the VirtualEnv. If
+ // nil, or if fields are missing, they will be filled in by probing the system
+ // PATH.
+ Spec *vpython.Spec
+
+ // PruneThreshold, if >0, is the maximum age of a VirtualEnv before it should
+ // be pruned. If <= 0, there is no maximum age, so no pruning will be
+ // performed.
+ PruneThreshold time.Duration
+ // MaxPrunesPerSweep applies a limit to the number of items to prune per
+ // execution.
+ //
+ // If <= 0, no limit will be applied.
+ MaxPrunesPerSweep int
+
+ // Loader is the PackageLoader instance to use for package resolution and
+ // deployment.
+ Loader PackageLoader
+
+ // LoaderResolveRoot is the common persistent root directory to use for the
+ // package loader's package resolution. This must be safe for concurrent
+ // use.
+ //
+ // Each VirtualEnv will have its own loader destination directory (root).
+ // However, that root is homed in a directory that is named after the resolved
+ // packages. LoaderResolveRoot is used during setup to resolve those package
+ // values, and therefore can't re-use the VirtualEnv root.
+ LoaderResolveRoot string
+
+ // MaxScriptPathLen, if >0, is the maximum allowed VirutalEnv path length.
+ // If set, and if the VirtualEnv is configured to be installed at a path
+ // greater than this, Env will fail.
+ //
+ // This can be used to enforce "shebang" length limits, whereupon generated
+ // VirtualEnv scripts may be generated with a "shebang" (#!) line longer than
+ // what is allowed by the operating system.
+ MaxScriptPathLen int
+
+ // testPreserveInstallationCapability is a testing parameter. If true, the
+ // VirtualEnv's ability to install will be preserved after the setup. This is
+ // used by the test whell generation bootstrap code.
+ testPreserveInstallationCapability bool
+
+ // testLeaveReadWrite, if true, instructs the VirtualEnv setup to leave the
+ // directory read/write. This makes it easier to manage, and is safe since it
+ // is not a production directory.
+ testLeaveReadWrite bool
+}
+
+// setupEnv processes the config, validating and, where appropriate, populating
+// any components. Upon success, it returns a configured Env instance.
+//
+// The returned Env instance may or may not actually exist. Setup must be called
+// prior to using it.
+func (cfg *Config) setupEnv(c context.Context) (*Env, error) {
+ // We MUST have a package loader.
+ if cfg.Loader == nil {
+ return nil, errors.New("no package loader provided")
+ }
+
+ // Resolve our base directory, if one is not supplied.
+ if cfg.BaseDir == "" {
+ // Use one in a temporary directory.
+ cfg.BaseDir = filepath.Join(os.TempDir(), "vpython")
+ logging.Debugf(c, "Using tempdir-relative environment root: %s", cfg.BaseDir)
+ }
+ if err := filesystem.AbsPath(&cfg.BaseDir); err != nil {
+ return nil, errors.Annotate(err).Reason("failed to resolve absolute path of base directory").Err()
+ }
+
+ // Enforce maximum path length.
+ if cfg.MaxScriptPathLen > 0 {
+ if longestPath := longestGeneratedScriptPath(cfg.BaseDir); longestPath != "" {
+ longestPathLen := utf8.RuneCountInString(longestPath)
+ if longestPathLen > cfg.MaxScriptPathLen {
+ return nil, errors.Reason("expected deepest path length (%(len)d) exceeds threshold (%(threshold)d)").
+ D("len", longestPathLen).
+ D("threshold", cfg.MaxScriptPathLen).
+ D("longestPath", longestPath).
+ Err()
+ }
+ }
+ }
+
+ if err := filesystem.MakeDirs(cfg.BaseDir); err != nil {
+ return nil, errors.Annotate(err).Reason("could not create environment root: %(root)s").
+ D("root", cfg.BaseDir).
+ Err()
+ }
+
+ // Determine our common loader root.
+ if cfg.LoaderResolveRoot == "" {
+ cfg.LoaderResolveRoot = filepath.Join(cfg.BaseDir, ".package_loader")
+ }
+
+ // Ensure and normalize our specification file.
+ if cfg.Spec == nil {
+ cfg.Spec = &vpython.Spec{}
+ }
+ if err := spec.Normalize(cfg.Spec); err != nil {
+ return nil, errors.Annotate(err).Reason("invalid specification").Err()
+ }
+
+ // Choose our VirtualEnv package.
+ if cfg.Spec.Virtualenv == nil {
+ cfg.Spec.Virtualenv = &cfg.Package
+ }
+
+ if err := cfg.resolvePackages(c); err != nil {
+ return nil, errors.Annotate(err).Reason("failed to resolve packages").Err()
+ }
+
+ if err := cfg.resolvePythonInterpreter(c); err != nil {
+ return nil, errors.Annotate(err).Reason("failed to resolve system Python interpreter").Err()
+ }
+
+ // Generate our enviornment name based on the deterministic hash of its
+ // fully-resolved specification.
+ envName := spec.Hash(cfg.Spec)
+ if cfg.MaxHashLen > 0 && len(envName) > cfg.MaxHashLen {
+ envName = envName[:cfg.MaxHashLen]
+ }
+ return cfg.envForName(envName), nil
+}
+
+// Prune performs a pruning round on the environment set described by this
+// Config.
+func (cfg *Config) Prune(c context.Context) error {
+ if err := prune(c, cfg, ""); err != nil {
+ return errors.Annotate(err).Err()
+ }
+ return nil
+}
+
+// envForExisting creates an Env for a named directory.
+func (cfg *Config) envForName(name string) *Env {
+ // Env-specific root directory: <BaseDir>/<name>
+ venvRoot := filepath.Join(cfg.BaseDir, name)
+ return &Env{
+ Config: cfg,
+ Root: venvRoot,
+ Python: venvBinPath(venvRoot, "python"),
+ SpecPath: filepath.Join(venvRoot, "enviornment.pb.txt"),
+
+ name: name,
+ lockPath: filepath.Join(cfg.BaseDir, fmt.Sprintf(".%s.lock", name)),
+ completeFlagPath: filepath.Join(venvRoot, "complete.flag"),
+ }
+}
+
+func (cfg *Config) resolvePackages(c context.Context) error {
+ // Create a single package list. Our VirtualEnv will be index 0 (need
+ // this so we can back-port it into its VirtualEnv property).
+ packages := make([]*vpython.Spec_Package, 1, 1+len(cfg.Spec.Wheel))
+ packages[0] = cfg.Spec.Virtualenv
+ packages = append(packages, cfg.Spec.Wheel...)
+
+ // Resolve our packages. Because we're using pointers, the in-place
+ // updating will update the actual spec file!
+ if err := cfg.Loader.Resolve(c, cfg.LoaderResolveRoot, packages); err != nil {
+ return errors.Annotate(err).Reason("failed to resolve packages").Err()
+ }
+ return nil
+}
+
+func (cfg *Config) resolvePythonInterpreter(c context.Context) error {
+ specVers, err := python.ParseVersion(cfg.Spec.PythonVersion)
+ if err != nil {
+ return errors.Annotate(err).Reason("failed to parse Python version from: %(value)q").
+ D("value", cfg.Spec.PythonVersion).
+ Err()
+ }
+
+ var i *python.Interpreter
+ if cfg.Python == "" {
+ // No explicitly-specified Python path. Determine one based on the
+ // specification.
+ if i, err = python.Find(c, specVers); err != nil {
+ return errors.Annotate(err).Reason("could not find Python for: %(vers)s").
+ D("vers", specVers).
+ Err()
+ }
+ cfg.Python = i.Python
+ } else {
+ i = &python.Interpreter{
+ Python: cfg.Python,
+ }
+ }
+
+ // Confirm that the version of the interpreter matches that which is
+ // expected.
+ interpreterVers, err := i.GetVersion(c)
+ if err != nil {
+ return errors.Annotate(err).Reason("failed to determine Python version for: %(python)s").
+ D("python", cfg.Python).
+ Err()
+ }
+ if !specVers.IsSatisfiedBy(interpreterVers) {
+ return errors.Reason("supplied Python version (%(supplied)s) doesn't match specification (%(spec)s)").
+ D("supplied", interpreterVers).
+ D("spec", specVers).
+ Err()
+ }
+ cfg.Spec.PythonVersion = interpreterVers.String()
+
+ // Resolve to absolute path.
+ if err := filesystem.AbsPath(&cfg.Python); err != nil {
+ return errors.Annotate(err).Reason("could not get absolute path for: %(python)s").
+ D("python", cfg.Python).
+ Err()
+ }
+ logging.Debugf(c, "Resolved system Python interpreter (%s): %s", cfg.Spec.PythonVersion, cfg.Python)
+ return nil
+}
+
+func (cfg *Config) systemInterpreter() *python.Command {
+ if cfg.Python == "" {
+ return nil
+ }
+ i := python.Interpreter{
+ Python: cfg.Python,
+ }
+ cmd := i.Command()
+ cmd.Isolated = true
+ return cmd
+}
« 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