blob: 99137d702a1c390ddcd3dcba3a65adc5dc730514 [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package venv
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"go.chromium.org/luci/vpython/api/vpython"
"go.chromium.org/luci/vpython/python"
"go.chromium.org/luci/vpython/spec"
"go.chromium.org/luci/vpython/venv/assets"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/system/environ"
"go.chromium.org/luci/common/system/filesystem"
)
// EnvironmentVersion is an environment version string. It must advance each
// time the layout of a VirtualEnv environment changes.
const EnvironmentVersion = "v4"
const siteCustomizePy = "sitecustomize.py"
type SliceFlag []string
func (i *SliceFlag) String() string {
return fmt.Sprintf("%v", *i)
}
func (i *SliceFlag) Set(value string) error {
*i = append(*i, value)
return nil
}
// 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
// SetupEnv if not nil, is the base environment that will be used during
// VirtualEnv setup operations.
SetupEnv environ.Env
// OverrideName overrides the name of the specified VirtualEnv.
//
// Because the name is no longer derived from the specification, this will
// force revalidation and deletion of any existing content if it is not a
// fully defined and matching VirtualEnv
OverrideName 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 a hardcoded set of Python interpreters to use, keyed by
// the python minor version, such as "3.8". If a key is found for the
// Python version required by the spec, and that file exists, it will
// be used without further probing. Otherwise, a list of possible
// interpreters may be given as UnversionedPython; the first one
// matching the Spec version will be used.
//
// If no interpreter is found via either of these methods, PATH will be
// searched for a matching interpreter.
Python map[string]string
UnversionedPython SliceFlag
// LookPathFunc, if not nil, will be used instead of exec.LookPath to find the
// underlying Python interpreter.
LookPathFunc python.LookPathFunc
// 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
// FailIfLocked, if true, means that if a lock is encountered during
// VirtualEnv operation, the VirtualEnv will exit immediately with an error.
// If false, the VirtualEnv will block pending the lock's availability.
FailIfLocked bool
// si is the system Python interpreter. It is resolved during
// "resolvePythonInterpreter".
si *python.Interpreter
// 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
}
// WithoutWheels returns a clone of cfg that depends on no additional packages.
//
// If cfg is already an empty it will be returned directly.
func (cfg *Config) WithoutWheels() *Config {
if !cfg.HasWheels() {
return cfg
}
clone := *cfg
clone.OverrideName = ""
clone.Spec = clone.Spec.Clone()
clone.Spec.Wheel = nil
return &clone
}
// HasWheels returns true if this environment declares wheel dependencies.
func (cfg *Config) HasWheels() bool {
return cfg.Spec != nil && len(cfg.Spec.Wheel) > 0
}
// resolveRuntime find the python interpreter in ine vpython environment, and
// update the environment runtime.
func (cfg *Config) resolveRuntime(c context.Context, e *vpython.Environment) error {
if cfg.si != nil && e.Runtime != nil {
return nil
}
if err := cfg.resolvePythonInterpreter(c, e.Spec); err != nil {
return errors.Annotate(err, "failed to resolve system Python interpreter").Err()
}
r, err := cfg.si.GetRuntime(c)
if err != nil {
return errors.Annotate(err, "failed to resolve system Python runtime").Err()
}
e.Runtime = r
logging.Debugf(c, "Resolved system Python runtime: %#s", e.Runtime)
return nil
}
// makeEnv processes the config, validating and, where appropriate, populating
// any components. Upon success, it returns a configured Env instance.
//
// The supplied vpython.Environment is derived externally, and may be nil if
// this is a bootstrapped Environment.
//
// The returned Env instance may or may not actually exist. Setup must be called
// prior to using it.
func (cfg *Config) makeEnv(c context.Context, e *vpython.Environment) (*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, "failed to resolve absolute path of base directory").Err()
}
// Construct a new, independent Environment for this Env.
e = e.Clone()
if cfg.Spec != nil {
e.Spec = cfg.Spec.Clone()
}
if err := spec.NormalizeEnvironment(e); err != nil {
return nil, errors.Annotate(err, "invalid environment").Err()
}
// If the environment doesn't specify a VirtualEnv package (expected), use
// our default.
if e.Spec.Virtualenv == nil {
e.Spec.Virtualenv = &cfg.Package
}
if err := cfg.Loader.Resolve(c, e); err != nil {
return nil, errors.Annotate(err, "failed to resolve packages").Err()
}
if err := cfg.resolveRuntime(c, e); err != nil {
return nil, err
}
// Ensure that our base directory exists.
if err := filesystem.MakeDirs(cfg.BaseDir); err != nil {
return nil, errors.Annotate(err, "could not create environment root: %s", cfg.BaseDir).Err()
}
// Generate our environment name based on the deterministic hash of its
// fully-resolved specification.
envName := cfg.OverrideName
if envName == "" {
envName = cfg.envNameForSpec(c, e.Spec, e.Runtime)
}
env := cfg.envForName(envName, e)
return env, nil
}
// EnvName returns the VirtualEnv environment name for the environment that cfg
// describes.
func (cfg *Config) envNameForSpec(c context.Context, s *vpython.Spec, rt *vpython.Runtime) string {
uid := "<unknown>"
if user, err := currentUID(); err == nil {
uid = user
} else {
logging.Warningf(c, "Unable to find current user ID: %s", err)
}
name := spec.Hash(s, rt, EnvironmentVersion,
string(assets.GetAssetSHA256(siteCustomizePy)), uid)
if cfg.MaxHashLen > 0 && len(name) > cfg.MaxHashLen {
name = name[:cfg.MaxHashLen]
}
return name
}
// 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, nil); err != nil {
return errors.Annotate(err, "").Err()
}
return nil
}
// envForName creates an Env for a named directory.
//
// The Environment, e, can be nil; however, code paths that require it may not
// be called.
func (cfg *Config) envForName(name string, e *vpython.Environment) *Env {
// Env-specific root directory: <BaseDir>/<name>
venvRoot := filepath.Join(cfg.BaseDir, name)
binDir := venvBinDir(venvRoot)
return &Env{
Config: cfg,
Root: venvRoot,
Name: name,
Python: filepath.Join(binDir, "python"),
Environment: e,
BinDir: binDir,
EnvironmentStampPath: filepath.Join(venvRoot, fmt.Sprintf("environment.%s.pb.txt", vpython.Version)),
lockPath: filepath.Join(cfg.BaseDir, fmt.Sprintf(".%s.lock", name)),
completeFlagPath: filepath.Join(venvRoot, "complete.flag"),
}
}
func (cfg *Config) resolvePythonInterpreter(c context.Context, s *vpython.Spec) error {
specVers, err := python.ParseVersion(s.PythonVersion)
if err != nil {
return errors.Annotate(err, "failed to parse Python version from: %q", s.PythonVersion).Err()
}
if len(cfg.UnversionedPython) > 0 {
// Select the interpreter that matches the specification.
for _, pythonPath := range cfg.UnversionedPython {
i := python.Interpreter{Python: pythonPath}
if err := i.Normalize(); err != nil {
continue
}
iv, err := i.GetVersion(c)
if err != nil {
continue
}
if specVers.IsSatisfiedBy(iv) {
cfg.si = &i
break
}
}
if cfg.si == nil {
return errors.Reason("none of %v matched specification %s", cfg.UnversionedPython, specVers).Err()
}
} else {
if pythonPath, present := cfg.Python[s.PythonVersion]; present {
// A Python interpreter was given for this specific version.
// Make sure it exists before using it.
// TODO: Use a build-time variable to control whether the bundled
// interpreter is expected to be present; skip the stat if so.
_, err := os.Stat(pythonPath)
if err == nil {
cfg.si = &python.Interpreter{Python: pythonPath}
if err := cfg.si.Normalize(); err != nil {
return errors.Annotate(err, "unable to normalize python interpreter").Err()
}
}
}
if cfg.si == nil {
// No explicitly-specified Python path. Search PATH to find one
// matching the specification.
if cfg.si, err = python.Find(c, specVers, cfg.LookPathFunc); err != nil {
return errors.Annotate(err, "could not find Python for: %s", specVers).Err()
}
}
}
if err := cfg.si.Normalize(); err != nil {
return err
}
interpreterVers, err := cfg.si.GetVersion(c)
if err != nil {
return errors.Annotate(err, "failed to determine Python version for: %s", cfg.si.Python).Err()
}
s.PythonVersion = interpreterVers.String()
return nil
}
func (cfg *Config) systemInterpreter() *python.Interpreter { return cfg.si }