blob: f38f2e83e457cb66c05c80c6d30c2a05e327584d [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 (
"bytes"
"context"
"encoding/json"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/danjacques/gofslock/fslock"
"google.golang.org/protobuf/proto"
"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/vpython/wheel"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/data/stringset"
"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"
)
// ErrNotComplete is a sentinel error returned by AssertCompleteAndLoad to
// indicate that the Environment is missing its completion flag.
var ErrNotComplete = errors.New("environment is not complete")
const (
lockHeldDelay = 10 * time.Millisecond
)
// pipIsolateOptions is a collection of "pip" overrides that we use to restrict
// unpredictable and uncontrolled behavior from "pip".
//
// Environment Variables:
// "pip" is used internally within VirtualEnv, and there is no mechanism to
// pass it our isolation flags. Therefore, we resort to using environment
// variable configuration:
// https://pip.pypa.io/en/stable/user_guide/#environment-variables
//
// Flags:
// When we run "pip install", we pass it "--isolated", which removes the
// influence of environment variable configuration. Since we're running "pip"
// directly, we can pass command-line flag equivalents. The benefits of using
// "--isolated" are worth the cost of exporting dual environment/flag versions
// of these isolation directives.
type pipOption struct {
// env is the environment variable string to add ("FOO=BAR").
env string
// installFlag is the "pip install" flag equivalent.
installFlag string
}
var pipIsolateOptions = []pipOption{
// Override any global configuration that prohibits binary (wheel) usage.
{"PIP_NO_BINARY=:none:", "--no-binary=:none:"},
// Enforce that all packages must be installed with wheels.
{"PIP_ONLY_BINARY=:all:", "--only-binary=:all:"},
}
// blocker is an fslock.Blocker implementation that sleeps lockHeldDelay in
// between attempts.
func blocker(c context.Context) fslock.Blocker {
return func() error {
logging.Debugf(c, "Lock is currently held. Sleeping %v and retrying...", lockHeldDelay)
tr := clock.Sleep(c, lockHeldDelay)
return tr.Err
}
}
func withTempDir(l logging.Logger, dir, prefix string, fn func(string) error) error {
tdir := filesystem.TempDir{
Dir: dir,
Prefix: prefix,
CleanupErrFunc: func(tdir string, err error) {
l.Infof("Failed to remove temporary directory: %s", err)
},
}
return tdir.With(fn)
}
// EnvRootFromStampPath calculates the environment root from an exported
// environment specification file path.
//
// The specification path is: <EnvRoot>/<SpecHash>/EnvironmentStampPath, so our
// EnvRoot is two directories up.
//
// We export EnvSpecPath as an absolute path. However, since someone else
// could have overridden it or exported their own, let's make sure.
func EnvRootFromStampPath(path string) (string, error) {
if err := filesystem.AbsPath(&path); err != nil {
return "", errors.Annotate(err,
"failed to get absolute path for specification file path: %s", path).Err()
}
return filepath.Dir(filepath.Dir(path)), nil
}
// With creates a new Env and executes "fn" with assumed ownership of that Env.
//
// The Context passed to "fn" will be cancelled if we lose perceived ownership
// of the configured environment. This is not an expected scenario, and should
// be considered an error condition. The Env passed to "fn" is valid only for
// the duration of the callback.
//
// It will lock around the VirtualEnv to ensure that multiple processes do not
// conflict with each other. If a VirtualEnv for this specification already
// exists, it will be used directly without any additional setup.
//
// If another process holds the lock, With will return an error if
// cfg.FailIfLocked is true, or try again until it obtains the lock otherwise.
func With(c context.Context, cfg Config, fn func(context.Context, *Env) error) error {
// Track which VirtualEnv we use so we can exempt them from pruning.
usedEnvs := stringset.New(2)
// Initialized python runtime outside makeEnv to avoid the expense of
// finding interpreter twice.
e := &vpython.Environment{}
e.Spec = cfg.Spec.Clone()
if err := cfg.resolveRuntime(c, e); err != nil {
return errors.Annotate(err, "failed to resolve python runtime").Err()
}
// Start with an empty VirtualEnv. We will use this to probe the local
// system.
//
// If our configured VirtualEnv is, itself, an empty then we can
// skip this.
if cfg.HasWheels() {
// Use an empty VirtualEnv to probe the runtime environment.
//
// Disable pruning for this step since we'll be doing that later with the
// full environment initialization.
emptyEnv, err := cfg.WithoutWheels().makeEnv(c, e)
if err != nil {
return errors.Annotate(err, "failed to initialize empty probe environment").Err()
}
if err := emptyEnv.ensure(c, !cfg.FailIfLocked); err != nil {
return errors.Annotate(err, "failed to create empty probe environment").Err()
}
usedEnvs.Add(emptyEnv.Name)
e = emptyEnv.Environment
}
// Run the real config, now with runtime data.
env, err := cfg.makeEnv(c, e)
if err != nil {
return err
}
usedEnvs.Add(env.Name)
return env.withImpl(c, !cfg.FailIfLocked, usedEnvs, fn)
}
// Delete removes all resources consumed by an environment.
//
// Delete will acquire an exclusive lock on the environment and assert that it
// is not in use prior to deletion. This is non-blocking, and an error will be
// returned if the lock could not be acquired.
//
// If deletion fails, a wrapped error will be returned.
func Delete(c context.Context, cfg Config) error {
// Attempt to acquire the environment's lock.
e, err := cfg.makeEnv(c, nil)
if err != nil {
return err
}
return e.Delete(c)
}
// Env is a fully set-up Python virtual environment. It is configured
// based on the contents of an vpython.Spec file by Setup.
//
// Env should not be instantiated directly; it must be created by calling
// Config.Env.
//
// All paths in Env are absolute.
type Env struct {
// Config is this Env's Config, fully-resolved.
Config *Config
// Root is the Env container's root directory path.
Root string
// Name is the hash of the specification file for this Env.
Name string
// Python is the path to the Env Python interpreter.
Python string
// Environment is the resolved Python environment that this VirtualEnv is
// configured with. It is resolved during environment setup and saved into the
// environment's EnvironmentStampPath.
Environment *vpython.Environment
// BinDir is the VirtualEnv "bin" directory, containing Python and installed
// wheel binaries.
BinDir string
// EnvironmentStampPath is the path to the vpython.Environment stamp file that
// details a constructed environment. It will be in text protobuf format, and,
// therefore, suitable for input to other "vpython" invocations.
EnvironmentStampPath string
// LockHandle is the active lock handle for the current VirtualEnv lock.
// Only read-only operations should be performed on the handle.
LockHandle fslock.Handle
// LockPath is the path to this Env-specific lock file. It will be at:
// "<baseDir>/.<name>.lock".
lockPath string
// completeFlagPath is the path to this Env's complete flag.
// It will be at "<Root>/complete.flag".
completeFlagPath string
// interpreter is the VirtualEnv Python interpreter.
//
// It is configured to use the Python member as its base executable, and is
// initialized on first call to Interpreter.
interpreter *python.Interpreter
}
// ensure ensures that the configured VirtualEnv is set-up. This may involve
// a fast-path completion flag check or a slow lock/build phase.
func (e *Env) ensure(c context.Context, blocking bool) (err error) {
// Fastest path: If this environment is already complete, then there is no
// additional setup necessary.
if err := e.AssertCompleteAndLoad(); err == nil {
logging.Debugf(c, "Environment is already initialized: %s", e.Environment)
return nil
}
// Repeatedly try and create our Env. We do this so that if we
// encounter a lock, we will let the other process finish and try and leverage
// its success.
for {
// We will be creating the Env. Acquire an exclusive lock so that any other
// processes will wait for our setup to complete.
switch lock, err := e.acquireExclusiveLock(); err {
case nil:
// We MUST successfully release our exclusive lock on completion.
return mustReleaseLock(c, lock, func() error {
// Fast path: if our complete flag is present, assume that the
// environment is setup and complete. No additional work is necessary.
err := e.AssertCompleteAndLoad()
if err == nil {
// This will generally happen if another process created the environment
// in between when we checked for the completion stamp initially and
// when we actually obtained the lock.
logging.Debugf(c, "Completion flag found! Environment is set-up: %s", e.completeFlagPath)
return nil
}
logging.WithError(err).Debugf(c, "VirtualEnv is not complete.")
// No complete flag. Create a new VirtualEnv here.
if err := e.createLocked(c); err != nil {
return errors.Annotate(err, "failed to create new VirtualEnv").Err()
}
// Mark that this environment is complete. This MUST succeed so other
// instances know that this environment is complete.
if err := e.touchCompleteFlagLocked(); err != nil {
return errors.Annotate(err, "failed to create complete flag").Err()
}
logging.Debugf(c, "Successfully created new virtual environment [%s]!", e.Name)
return nil
})
case fslock.ErrLockHeld:
// We couldn't get an exclusive lock. Try again to load the environment
// stamp, asserting the existence of the completion flag in the process.
// If another process has created the environment, we may be able to use
// it without ever having to obtain its lock!
if err := e.AssertCompleteAndLoad(); err == nil {
logging.Infof(c, "Environment was completed while waiting for lock: %s", e.EnvironmentStampPath)
return nil
}
logging.Fields{
logging.ErrorKey: err,
"path": e.EnvironmentStampPath,
}.Debugf(c, "Lock is held, and environment is not complete.")
if !blocking {
return errors.Annotate(err, "VirtualEnv lock is currently held (non-blocking)").Err()
}
// Some other process holds the lock. Sleep a little and retry.
if err := blocker(c)(); err != nil {
return err
}
default:
return errors.Annotate(err, "failed to ensure VirtualEnv").Err()
}
}
}
func (e *Env) withImpl(c context.Context, blocking bool, used stringset.Set,
fn func(context.Context, *Env) error) (err error) {
// Setup the VirtualEnv environment.
//
// Setup will obtain an exclusive lock on the environment for set-up and
// release it when it finishes. We will then obtain a shared lock on the
// environment to represent its use.
//
// An exclusive lock may be taken on the environment in between the exclusive
// lock being released and the shared lock being obtained. If that happens,
// we will (if configured to block) repeat our loop and call "ensure" again.
for {
if err := e.ensure(c, blocking); err != nil {
return err
}
// Acquire a shared lock on the environment to note its continued usage.
switch lock, err := e.acquireSharedLock(); err {
case nil:
logging.Debugf(c, "Acquired shared lock for: %s", e.Name)
environmentWasIncomplete := false
err := mustReleaseLock(c, lock, func() error {
// Assert that the environment wasn't deleted in between creation and
// our acquisition of the lock.
if err := e.assertComplete(); err != nil {
logging.WithError(err).Infof(c, "Environment is no longer complete; recreating...")
environmentWasIncomplete = true
return nil
}
// Try and touch the complete flag to update its timestamp and mark this
// environment's utility.
if err := e.touchCompleteFlagLocked(); err != nil {
logging.Debugf(c, "Failed to update environment timestamp.")
}
// Perform a pruning round. Failure is non-fatal.
if perr := prune(c, e.Config, used); perr != nil {
logging.WithError(perr).Infof(c, "Failed to perform pruning round after initialization.")
}
e.LockHandle = lock
defer func() {
e.LockHandle = nil
}()
return fn(c, e)
})
if err != nil {
return err
}
logging.Debugf(c, "Released shared lock for: %s", e.Name)
if !environmentWasIncomplete {
return nil
}
case fslock.ErrLockHeld:
logging.Fields{
logging.ErrorKey: err,
"path": e.EnvironmentStampPath,
}.Debugf(c, "Could not obtain shared usage lock.")
if !blocking {
return errors.Annotate(err, "VirtualEnv lock is currently held (non-blocking)").Err()
}
// Some other process holds the lock. Sleep a little and retry.
if err := blocker(c)(); err != nil {
return err
}
default:
return errors.Annotate(err, "failed to use VirtualEnv").Err()
}
}
}
// Interpreter returns the VirtualEnv's isolated Python Interpreter instance.
func (e *Env) Interpreter() *python.Interpreter {
if e.interpreter == nil {
e.interpreter = &python.Interpreter{
Python: e.Python,
}
}
return e.interpreter
}
func (e *Env) acquireExclusiveLock() (fslock.Handle, error) { return fslock.Lock(e.lockPath) }
func (e *Env) acquireSharedLock() (fslock.Handle, error) { return fslock.LockShared(e.lockPath) }
func (e *Env) withExclusiveLockNonBlocking(fn func() error) error {
return fslock.With(e.lockPath, fn)
}
// WriteEnvironmentStamp writes a text protobuf form of spec to path.
func (e *Env) WriteEnvironmentStamp() error {
environment := e.Environment
if environment == nil {
environment = &vpython.Environment{}
}
return writeTextProto(e.EnvironmentStampPath, environment)
}
// AssertCompleteAndLoad asserts that the VirtualEnv's completion
// flag exists. If it does, the environment's stamp is loaded into e.Environment
// and nil is returned.
//
// An error is returned if the completion flag does not exist, or if the
// VirtualEnv environment stamp could not be loaded.
func (e *Env) AssertCompleteAndLoad() error {
if err := e.assertComplete(); err != nil {
return err
}
var environment vpython.Environment
if err := spec.LoadEnvironment(e.EnvironmentStampPath, &environment); err != nil {
return err
}
if err := spec.NormalizeEnvironment(&environment); err != nil {
return errors.Annotate(err, "failed to normalize stamp environment").Err()
}
// If we are configured with an environment, validate that it matches the
// the environment that we just loaded.
//
// We only consider our environment-defining fields (Spec and Runtime).
//
// Note that both environments will have been normalized at this point, so
// comparison should be reliable.
if e.Environment != nil {
if !proto.Equal(e.Environment.Spec, environment.Spec) {
return errors.New("environment stamp specification does not match")
}
if !proto.Equal(e.Environment.Runtime, environment.Runtime) {
return errors.New("environment stamp runtime does not match")
}
}
e.Environment = &environment
return nil
}
func (e *Env) assertComplete() error {
// Ensure that the environment has its completion flag.
switch _, err := os.Stat(e.completeFlagPath); {
case filesystem.IsNotExist(err):
return ErrNotComplete
case err != nil:
return errors.Annotate(err, "failed to check for completion flag").Err()
default:
return nil
}
}
func (e *Env) createLocked(c context.Context) error {
// If our root directory already exists, delete it.
if _, err := os.Stat(e.Root); err == nil {
logging.Infof(c, "Deleting existing VirtualEnv: %s", e.Root)
if renamedTo, err := filesystem.RenamingRemoveAll(e.Root, ""); err != nil {
// Removal might have failed, but if renaming succeeded, ignore the
// garbage.
if _, err2 := os.Stat(e.Root); err2 == nil {
// TODO(crbug/869227): remove this logging once root cause is found and fixed.
logging.Warningf(c, "crbug/869227: ==== failed to remove %q, current files/dirs: ====", e.Root)
_ = filepath.Walk(e.Root, func(path string, info os.FileInfo, err error) error {
if err != nil {
logging.Debugf(c, " %q: ERROR: %s", path, err)
} else {
logging.Debugf(c, " %q: %s", path, info.Mode())
}
return nil
})
logging.Warningf(c, "crbug/869227: ==== end ====")
return errors.Annotate(err, "failed to remove existing root").Err()
}
logging.Warningf(c, "renamed existing root %q to %q, but failed to remove, leaving as is and continuing",
e.Root, renamedTo)
}
}
// Make sure our environment's base directory exists.
if err := filesystem.MakeDirs(e.Root); err != nil {
return errors.Annotate(err, "failed to create environment root").Err()
}
logging.Infof(c, "Using virtual environment root: %s", e.Root)
// Build our package list. Always install our base VirtualEnv package.
packages := make([]*vpython.Spec_Package, 1, 1+len(e.Environment.Spec.Wheel))
packages[0] = e.Environment.Spec.Virtualenv
packages = append(packages, e.Environment.Spec.Wheel...)
// Create a directory to bootstrap VirtualEnv from.
//
// This directory will be a very short-named temporary directory. This is
// because it really quickly runs up into traditional Windows path limitations
// when ZIP-importing sub-sub-sub-sub-packages (e.g., pip, requests, etc.).
//
// We will clean this directory up on termination.
err := withTempDir(logging.Get(c), "", "vpython_bootstrap", func(bootstrapDir string) error {
pkgDir := filepath.Join(bootstrapDir, "packages")
if err := filesystem.MakeDirs(pkgDir); err != nil {
return errors.Annotate(err, "could not create bootstrap packages directory").Err()
}
setupEnv := e.isolatedSetupEnvironment(bootstrapDir)
setupEnvSorted := setupEnv.Sorted()
if err := e.downloadPackages(c, pkgDir, packages); err != nil {
return errors.Annotate(err, "failed to download packages").Err()
}
// Installing base VirtualEnv.
if err := e.installVirtualEnv(c, pkgDir, setupEnvSorted); err != nil {
return errors.Annotate(err, "failed to install VirtualEnv").Err()
}
// Load PEP425 tags, if we don't already have them.
if e.Environment.Pep425Tag == nil {
pep425Tags, err := e.getPEP425Tags(c, setupEnvSorted)
if err != nil {
return errors.Annotate(err, "failed to get PEP425 tags").Err()
}
e.Environment.Pep425Tag = pep425Tags
}
// Install our wheel files.
if len(e.Environment.Spec.Wheel) > 0 {
// Install wheels into our VirtualEnv.
if err := e.installWheels(c, bootstrapDir, pkgDir, setupEnvSorted); err != nil {
return errors.Annotate(err, "failed to install wheels").Err()
}
}
// Inject our site customization
if err := e.injectSiteCustomization(c, setupEnvSorted); err != nil {
return errors.Annotate(err, "failed to inject site customizations").Err()
}
return nil
})
if err != nil {
return err
}
// Write our specification file.
if err := e.WriteEnvironmentStamp(); err != nil {
return errors.Annotate(err, "failed to write environment stamp file to: %s",
e.EnvironmentStampPath).Err()
}
logging.Debugf(c, "Wrote environment stamp file to: %s", e.EnvironmentStampPath)
// Finalize our VirtualEnv for bootstrap execution.
if err := e.finalize(c); err != nil {
return errors.Annotate(err, "failed to prepare VirtualEnv").Err()
}
return nil
}
func (e *Env) downloadPackages(c context.Context, dst string, packages []*vpython.Spec_Package) error {
// Create a wheel sub-directory underneath of root.
logging.Debugf(c, "Loading %d package(s) into: %s", len(packages), dst)
if err := e.Config.Loader.Ensure(c, dst, packages); err != nil {
return errors.Annotate(err, "failed to download packages").Err()
}
return nil
}
func (e *Env) installVirtualEnv(c context.Context, pkgDir string, env []string) error {
// Create our VirtualEnv package staging sub-directory underneath of root.
bsDir := filepath.Join(e.Root, ".virtualenv")
if err := filesystem.MakeDirs(bsDir); err != nil {
return errors.Annotate(err, "failed to create VirtualEnv bootstrap directory").
InternalReason("path(%s)", bsDir).Err()
}
// Identify the virtualenv directory: will have "virtualenv-" prefix.
matches, err := filepath.Glob(filepath.Join(pkgDir, "virtualenv-*"))
if err != nil {
return errors.Annotate(err, "failed to glob for 'virtualenv-' directory").Err()
}
if len(matches) == 0 {
return errors.Reason("no 'virtualenv-' directory provided by package").Err()
}
venvDir := matches[0]
logging.Debugf(c, "Creating VirtualEnv at: %s", e.Root)
cmd := e.Config.systemInterpreter().MkIsolatedCommand(c,
python.ScriptTarget{Path: "virtualenv.py"},
"--no-download",
e.Root)
defer cmd.Cleanup()
cmd.Env = env
cmd.Dir = venvDir
dumpOutput := attachOutputForLogging(c, logging.Debug, cmd.Cmd)
if err := cmd.Run(); err != nil {
dumpOutput(c, logging.Error)
return errors.Annotate(err, "failed to create VirtualEnv").Err()
}
logging.Debugf(c, "Making VirtualEnv relocatable at: %s", e.Root)
cmd = e.Interpreter().MkIsolatedCommand(c,
python.ScriptTarget{Path: "virtualenv.py"},
"--relocatable",
e.Root)
defer cmd.Cleanup()
cmd.Env = env
cmd.Dir = venvDir
dumpOutput = attachOutputForLogging(c, logging.Debug, cmd.Cmd)
if err := cmd.Run(); err != nil {
dumpOutput(c, logging.Error)
return errors.Annotate(err, "failed to make VirtualEnv relocatable").Err()
}
return nil
}
// getStdlibPath figures out the location of the 'lib/python2.7' type folder for
// the current interpreter.
func (e *Env) getStdlibPath(c context.Context, env []string) (string, error) {
// This script will return the directory where the `site` Python module is found.
// This module is always created by the VirtualEnv, and so is a reliable
// indicator of where 'stdlib' imports exist.
const script = `import os, site, sys; ` +
`sys.stdout.write(os.path.dirname(site.__file__))`
cmd := e.Interpreter().MkIsolatedCommand(c, python.CommandTarget{Command: script})
defer cmd.Cleanup()
cmd.Env = env
var stdout bytes.Buffer
cmd.Stdout = &stdout
dumpOutput := attachOutputForLogging(c, logging.Debug, cmd.Cmd)
if err := cmd.Run(); err != nil {
dumpOutput(c, logging.Error)
return "", errors.Annotate(err, "failed to get stdlib module path").Err()
}
return stdout.String(), nil
}
// This script will return a list of 3-entry lists:
// [0]: version (e.g., "cp27")
// [1]: abi (e.g., "cp27mu", "none")
// [2]: arch (e.g., "x86_64", "armv7l", "any")
const pep425TagsScript = `
import json, sys
import pip._internal.utils.compatibility_tags as compatibility_tags
tags = [(t.interpreter, t.abi, t.platform) for t in compatibility_tags.get_supported()]
sys.stdout.write(json.dumps(tags))
`
// getPEP425Tags calls Python's compatibility_tags package to retrieve the tags.
//
// This must be run while "pip" is installed in the VirtualEnv.
func (e *Env) getPEP425Tags(c context.Context, env []string) ([]*vpython.PEP425Tag, error) {
type pep425TagEntry []string
cmd := e.Interpreter().MkIsolatedCommand(c, python.CommandTarget{Command: pep425TagsScript})
defer cmd.Cleanup()
cmd.Env = env
var stdout bytes.Buffer
cmd.Stdout = &stdout
dumpOutput := attachOutputForLogging(c, logging.Debug, cmd.Cmd)
if err := cmd.Run(); err != nil {
dumpOutput(c, logging.Error)
return nil, errors.Annotate(err, "failed to get PEP425 tags").Err()
}
var tagEntries []pep425TagEntry
if err := json.Unmarshal(stdout.Bytes(), &tagEntries); err != nil {
return nil, errors.Annotate(err, "failed to unmarshal PEP425 tag output: %s", stdout.String()).Err()
}
tags := make([]*vpython.PEP425Tag, len(tagEntries))
for i, te := range tagEntries {
if len(te) != 3 {
return nil, errors.Reason("invalid PEP425 tag entry: %v", te).
InternalReason("index(%d)", i).Err()
}
tags[i] = &vpython.PEP425Tag{
Python: te[0],
Abi: te[1],
Platform: te[2],
}
}
// If we're Debug-logging, calculate and display the tags that were probed.
if logging.IsLogging(c, logging.Debug) {
tagStr := make([]string, len(tags))
for i, t := range tags {
tagStr[i] = t.TagString()
}
logging.Debugf(c, "Loaded PEP425 tags: [%s]", strings.Join(tagStr, ", "))
}
return tags, nil
}
func (e *Env) installWheels(c context.Context, bootstrapDir, pkgDir string, env []string) error {
// Identify all downloaded wheels and parse them.
wheels, err := wheel.ScanDir(pkgDir)
if err != nil {
return errors.Annotate(err, "failed to load wheels").Err()
}
// Build a "wheel" requirements file.
reqPath := filepath.Join(bootstrapDir, "requirements.txt")
logging.Debugf(c, "Rendering requirements file to: %s", reqPath)
if err := wheel.WriteRequirementsFile(reqPath, wheels); err != nil {
return errors.Annotate(err, "failed to render requirements file").Err()
}
// We use "--isolated", which disables "PIP_" environment variable
// configuration overrides that we set up in "isolatedSetupEnvironment".
// Therefore, we append flag equivalents.
//
// See pipIsolateOptions.
pythonCmd := []string{
"install",
"--isolated",
"--compile",
"--no-index",
"--find-links", pkgDir,
"--requirement", reqPath,
}
for _, opt := range pipIsolateOptions {
pythonCmd = append(pythonCmd, opt.installFlag)
}
cmd := e.Interpreter().MkIsolatedCommand(c,
python.ModuleTarget{Module: "pip"},
pythonCmd...)
defer cmd.Cleanup()
cmd.Env = env
dumpOutput := attachOutputForLogging(c, logging.Debug, cmd.Cmd)
if err := cmd.Run(); err != nil {
dumpOutput(c, logging.Error)
return errors.Annotate(err, "failed to install wheels").Err()
}
return nil
}
func (e *Env) injectSiteCustomization(c context.Context, env []string) error {
basePath, err := e.getStdlibPath(c, env)
if err != nil {
return err
}
// NOTE: Any assets added to the virtualenv here must have their asset hashes
// incorporated in the spec.Hash in config.go:envNameForSpec.
//
// If you need to add files other than siteCustomizePy, please consider making
// this function more generic (e.g. s/assets/overlay, then looping through all
// content in overlay, adding it to the virtualenv).
siteCustomizePath := filepath.Join(basePath, siteCustomizePy)
if err := writeFile(siteCustomizePath, assets.GetAsset(siteCustomizePy), 0444); err != nil {
return errors.Annotate(err, "cannot create sitecustomize.py").Err()
}
// This is read by the VirtualEnv's generated 'site.py', instructing it not to import global site packages.
if err := filesystem.Touch(filepath.Join(basePath, "no-global-site-packages.txt"), time.Time{}, 0444); err != nil {
return errors.Annotate(err, "cannot touch no-global-site-packages.txt").Err()
}
return nil
}
func (e *Env) finalize(c context.Context) error {
// Change all files to read-only, except:
// - Our root directory, which must be writable in order to update our
// completion flag.
// - Our environment stamp, which must be trivially re-writable.
if !e.Config.testLeaveReadWrite {
err := filesystem.MakeReadOnly(e.Root, func(path string) bool {
switch path {
case e.Root, e.completeFlagPath:
return false
default:
return true
}
})
if err != nil {
return errors.Annotate(err, "failed to mark environment read-only").Err()
}
}
return nil
}
func (e *Env) touchCompleteFlagLocked() error {
if err := filesystem.Touch(e.completeFlagPath, time.Time{}, 0644); err != nil {
return errors.Annotate(err, "").Err()
}
return nil
}
func (e *Env) isolatedSetupEnvironment(bootstrapDir string) environ.Env {
env := e.Config.SetupEnv.Clone()
python.IsolateEnvironment(&env, false)
env.RemoveMatch(func(k, v string) bool {
// Remove all VIRTUALENV_* environment variables. These can influence
// VirtualEnv behavior, and we want to control that.
//
// https://virtualenv.pypa.io/en/stable/reference/#environment-variables
if strings.HasPrefix(k, "VIRTUALENV_") || strings.HasPrefix(k, "VIRTUAL_ENV_") {
return true
}
// Remove all "PIP_" environment variable overrides. See:
// https://pip.pypa.io/en/stable/user_guide/#environment-variables
if strings.HasPrefix(k, "PIP_") {
return true
}
return false
})
// Use a temporary HOME directory.
//
// This is an ugly hack. However, it's the only way to stop VirtualEnv's
// "pip" invocation's "setuptools" invocation from reading the local user's
// "~/.pydistutils.cfg" file, which can lead to broken VirtualEnv results.
//
// It has the nice side-effect of catching some potential artifacts that can
// be generated. There probably won't be any, but if they are, they will be
// cleaned up now.
//
// This also eliminates the default VirtualEnv configuration file, which is
// located relative to $HOME.
env.Set("HOME", bootstrapDir)
// Set some basic "pip" options to override any "pip" configuration.
//
// Unfortunately, there's no mechanism to disable loading global configuration
// files, and if we want to influence VirtualEnv (which calls "pip")
// internally, we are stuck overriding it with our own preferred defaults.
for _, opt := range pipIsolateOptions {
env.SetEntry(opt.env)
}
return env
}
// Delete removes all resources consumed by an environment.
//
// Delete will acquire an exclusive lock on the environment and assert that it
// is not in use prior to deletion. This is non-blocking, and an error will be
// returned if the lock could not be acquired.
//
// If the environment was not deleted, a non-nil wrapped error will be returned.
// If the deletion failed because the lock was held, a wrapped
// fslock.ErrLockHeld will be returned.
func (e *Env) Delete(c context.Context) error {
removedLock := false
err := e.withExclusiveLockNonBlocking(func() error {
logging.Debugf(c, "(Delete) Got exclusive lock for: %s", e.Name)
// Delete our environment directory.
if err := filesystem.RemoveAll(e.Root); err != nil {
return errors.Annotate(err, "failed to delete environment root").Err()
}
// Attempt to delete our lock. On POSIX systems, this will successfully
// delete the lock. On Windows systems, there will be contention, since the
// lock is held by us.
//
// In this case, we'll try again to delete it after we release the lock.
// If someone else takes out the lock in between, the delete will similarly
// fail.
if err := os.Remove(e.lockPath); err == nil {
removedLock = true
} else {
logging.WithError(err).Debugf(c, "failed to delete lock file while holding lock: %s", e.lockPath)
}
return nil
})
logging.Debugf(c, "(Delete) Released exclusive lock for: %s", e.Name)
if err != nil {
return errors.Annotate(err, "failed to delete environment").Err()
}
// Try and remove the lock now that we don't hold it.
if !removedLock {
if err := os.Remove(e.lockPath); err != nil {
return errors.Annotate(err, "failed to remove lock").Err()
}
}
return nil
}
// completionFlagTimestamp returns the timestamp on the environment's completion
// flag.
//
// If the completion flag does not exist (incomplete environment), a zero time
// will be returned with no error.
//
// If an error is encountered while checking for the timestamp, it will be
// returned.
func (e *Env) completionFlagTimestamp() (time.Time, error) {
// Read the complete flag file's timestamp.
switch st, err := os.Stat(e.completeFlagPath); {
case err == nil:
return st.ModTime(), nil
case os.IsNotExist(err):
return time.Time{}, nil
default:
return time.Time{}, errors.Annotate(err, "failed to stat completion flag: %s",
e.completeFlagPath).Err()
}
}
// attachOutputForLogging modifies the supplied cmd's Stdout and Stderr fields
// to output appropriately, assuming it isn't otherwise configured.
//
// If we are logging at level l, the process's Stdout and Stderr will be
// directly connected to STDERR
//
// This will return a callback that can be invoked to dump any buffered process
// output at the specified logging level.
func attachOutputForLogging(c context.Context, l logging.Level, cmd *exec.Cmd) func(context.Context, logging.Level) {
if logging.IsLogging(c, logging.Info) {
logging.Infof(c, "Running Python command (cwd=%s): %s",
cmd.Dir, strings.Join(cmd.Args, " "))
}
var out io.Writer
var buf bytes.Buffer
if logging.IsLogging(c, l) {
// If we're logging, redirect all process output to STDERR (same as logger
// uses).
out = os.Stderr
} else {
out = &buf
}
if cmd.Stdout == nil {
// STDOUT will be sent to our output channel, since this logging for
// debugging, not actual functional process output.
cmd.Stdout = out
}
if cmd.Stderr == nil {
cmd.Stderr = out
}
// Do not dump any additional error output.
return func(c context.Context, l logging.Level) {
logging.Logf(c, l, "Command (cwd=%s): %s\nProcess output:\n%s\nEnvironment:\n%s",
cmd.Dir, cmd.Args, buf.Bytes(), strings.Join(cmd.Env, "\n"))
}
}
// mustReleaseLock calls the wrapped function, releasing the lock at the end
// of its execution. If the lock could not be released, this function will
// panic, since the locking state can no longer be determined.
func mustReleaseLock(c context.Context, lock fslock.Handle, fn func() error) error {
defer func() {
if err := lock.Unlock(); err != nil {
errors.Log(c, errors.Annotate(err, "failed to release lock").Err())
// TODO(maruel): There's a bug somewhere here that cases failures on
// Swarming tasks. Since they are running in a contained environment, it
// is not as much as a big deal. Experimenting if a Swarming task can
// survive the fact that the lock is not released.
// https://crbug.com/869227
//panic(err)
}
}()
return fn()
}
// writeFile writes the contents of data to the specified path. It
// handles additional cases where the file already exists by deleting it
// first, allowing it to be overwritten.
func writeFile(path string, data []byte, mode os.FileMode) error {
// Ensure that the parent directory is user-writable, since this is a
// requirement in order to make modifications to that directory.
parentDir := filepath.Dir(path)
if err := filesystem.MakePathUserWritable(parentDir, nil); err != nil {
return errors.Annotate(err, "failed to mark parent directory user-writable").
InternalReason("path(%q)", parentDir).Err()
}
// Ensure that the target path doesn't already exist. Use filesystem.RemoveAll
// in case it exists, but is not user-writable.
if err := filesystem.RemoveAll(path); err != nil {
return err
}
if err := os.WriteFile(path, data, mode); err != nil {
return errors.Annotate(err, "could not write file contents").InternalReason("path(%q)", path).Err()
}
return nil
}
// StripVirtualEnvPaths looks for all $PATH elements which are the `BinDir` of
// a VirtualEnv deployment (created by VPython or not), and removes them. These
// directories contain a `python` interpreter and various scripts (like
// activate).
//
// This uses VirtualEnv's "<BinDir>/activate_this.py" file to identify
// VirtualEnvs, which is installed by all known versions of VirtualEnv.
//
// This returns a modified copy of env and a list of pruned paths (if any).
func StripVirtualEnvPaths(env environ.Env) (ret environ.Env, pruned []string) {
ret = env.Clone()
path := filepath.SplitList(env.Get("PATH"))
newPath := make([]string, 0, len(path))
for _, entry := range path {
if _, err := os.Stat(filepath.Join(entry, "activate_this.py")); err == nil {
pruned = append(pruned, entry)
} else {
newPath = append(newPath, entry)
}
}
ret.Set("PATH", strings.Join(newPath, string(filepath.ListSeparator)))
return
}