blob: 05ed793b59fed29adde019a5cbfe062cfe892c0e [file] [log] [blame] [edit]
// 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 application
import (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/maruel/subcommands"
"github.com/mitchellh/go-homedir"
"golang.org/x/net/context"
"github.com/luci/luci-go/vpython"
vpythonAPI "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/vpython/venv"
cipdVersion "github.com/luci/luci-go/cipd/version"
"github.com/luci/luci-go/common/cli"
"github.com/luci/luci-go/common/errors"
"github.com/luci/luci-go/common/logging"
"github.com/luci/luci-go/common/logging/gologger"
"github.com/luci/luci-go/common/system/environ"
"github.com/luci/luci-go/common/system/exitcode"
"github.com/luci/luci-go/common/system/filesystem"
"github.com/luci/luci-go/common/system/prober"
)
const (
// VirtualEnvRootENV is an environment variable that, if set, will be used
// as the default VirtualEnv root.
//
// This value overrides the default (~/.vpython), but can be overridden by the
// "-root" flag.
//
// Like "-root", if this value is present but empty, a tempdir will be used
// for the VirtualEnv root.
VirtualEnvRootENV = "VPYTHON_VIRTUALENV_ROOT"
// DefaultSpecENV is an enviornment variable that, if set, will be used as the
// default VirtualEnv spec file if none is provided or found through probing.
DefaultSpecENV = "VPYTHON_DEFAULT_SPEC"
)
// ReturnCodeError is an error wrapping a return code value.
type ReturnCodeError int
func (err ReturnCodeError) Error() string {
return fmt.Sprintf("python interpreter returned non-zero error: %d", err)
}
// VerificationFunc is a function used in environment verification.
//
// VerificationFunc will be invoked with a PackageLoader and an Environment to
// use for verification.
type VerificationFunc func(context.Context, string, venv.PackageLoader, *vpythonAPI.Environment)
// Config is an application's default configuration.
type Config struct {
// PackageLoader is the package loader to use.
PackageLoader venv.PackageLoader
// VENVPackage is the VirtualEnv package to use for bootstrap generation.
VENVPackage vpythonAPI.Spec_Package
// BaseWheels is the set of wheels to include in the spec. These will always
// be merged into the runtime spec and normalized, such that any duplicate
// wheels will be deduplicated.
BaseWheels []*vpythonAPI.Spec_Package
// RelativePathOverride is a series of forward-slash-delimited paths to
// directories relative to the "vpython" executable that will be checked
// for Python targets prior to checking PATH. This allows bundles (e.g., CIPD)
// that include both the wrapper and a real implementation, to force the
// wrapper to use the bundled implementation if present.
//
// See "github.com/luci/luci-go/common/wrapper/prober.Probe"'s
// RelativePathOverride member for more information.
RelativePathOverride []string
// PruneThreshold, if > 0, is the maximum age of a VirtualEnv before it
// becomes candidate for pruning. If <= 0, no pruning will be performed.
//
// See venv.Config's PruneThreshold.
PruneThreshold time.Duration
// MaxPrunesPerSweep, if > 0, is the maximum number of VirtualEnv that should
// be pruned passively. If <= 0, no limit will be applied.
//
// See venv.Config's MaxPrunesPerSweep.
MaxPrunesPerSweep int
// MaxScriptPathLen, if > 0, is the maximum generated script path lengt. If
// a generated script is expected to exist longer than this, we will error.
//
// See venv.Config's MaxScriptPathLen.
MaxScriptPathLen int
// WithVerificationConfig, if not nil, runs the supplied callback with
// a Config instance to use for verification and the set of default
// verification scenarios.
//
// If nil, verification will only include the validation of the spec protobuf.
WithVerificationConfig func(context.Context, func(Config, []*vpythonAPI.PEP425Tag) error) error
}
type application struct {
*Config
// opts is the set of configured options.
opts vpython.Options
help bool
devMode bool
specPath string
logConfig logging.Config
}
func (a *application) mainDev(c context.Context, args []string) error {
app := cli.Application{
Name: "vpython",
Title: "VirtualEnv Python Bootstrap (Development Mode)",
Context: func(context.Context) context.Context {
// Discard the entry Context and use the one passed to us.
c := c
// Install our Config instance into the Context.
return withApplication(c, a)
},
Commands: []*subcommands.Command{
subcommands.CmdHelp,
subcommandInstall,
subcommandVerify,
subcommandDelete,
},
}
return ReturnCodeError(subcommands.Run(&app, args))
}
func (a *application) addToFlagSet(fs *flag.FlagSet) {
fs.BoolVar(&a.help, "help", a.help,
"Display help for 'vpython' top-level arguments.")
fs.BoolVar(&a.help, "h", a.help,
"Display help for 'vpython' top-level arguments (same as -help).")
fs.BoolVar(&a.devMode, "dev", a.devMode,
"Enter development / subcommand mode (use 'help' for more options).")
fs.StringVar(&a.opts.EnvConfig.Python, "python", a.opts.EnvConfig.Python,
"Path to system Python interpreter to use. Default is found on PATH.")
fs.StringVar(&a.opts.WorkDir, "workdir", a.opts.WorkDir,
"Working directory to run the Python interpreter in. Default is current working directory.")
fs.StringVar(&a.opts.EnvConfig.BaseDir, "root", a.opts.EnvConfig.BaseDir,
"Path to virtual environment root directory. Default is the working directory. "+
"If explicitly set to empty string, a temporary directory will be used and cleaned up "+
"on completion.")
fs.StringVar(&a.specPath, "spec", a.specPath,
"Path to environment specification file to load. Default probes for one.")
a.logConfig.AddFlags(fs)
}
func (a *application) mainImpl(c context.Context, argv0 string, args []string) error {
// Determine our VirtualEnv base directory.
if v, ok := a.opts.Environ.Get(VirtualEnvRootENV); ok {
a.opts.EnvConfig.BaseDir = v
} else {
hdir, err := homedir.Dir()
if err != nil {
return errors.Annotate(err, "failed to get user home directory").Err()
}
a.opts.EnvConfig.BaseDir = filepath.Join(hdir, ".vpython")
}
// Extract "vpython" arguments and parse them.
fs := flag.NewFlagSet("", flag.ExitOnError)
fs.SetOutput(os.Stdout) // Python uses STDOUT for help and flag information.
a.addToFlagSet(fs)
selfArgs, args := extractFlagsForSet(args, fs)
if err := fs.Parse(selfArgs); err != nil && err != flag.ErrHelp {
return errors.Annotate(err, "failed to parse flags").Err()
}
// Identify the "self" executable. Use this to construct a "lookPath", which
// will be used to locate the base Python interpreter.
lp := lookPath{
probeBase: prober.Probe{
RelativePathOverride: a.RelativePathOverride,
},
env: a.opts.Environ,
}
if err := lp.probeBase.ResolveSelf(argv0); err != nil {
logging.WithError(err).Warningf(c, "Failed to resolve 'self'")
}
a.opts.EnvConfig.LookPathFunc = lp.look
if a.help {
return a.showPythonHelp(c, fs, &lp)
}
c = a.logConfig.Set(c)
// If a spec path was manually specified, load and use it.
if a.specPath != "" {
var sp vpythonAPI.Spec
if err := spec.Load(a.specPath, &sp); err != nil {
return err
}
a.opts.EnvConfig.Spec = &sp
} else if specPath := a.opts.Environ.GetEmpty(DefaultSpecENV); specPath != "" {
if err := spec.Load(specPath, &a.opts.DefaultSpec); err != nil {
return errors.Annotate(err, "failed to load default specification file (%s) from %s",
DefaultSpecENV, specPath).Err()
}
}
// If an empty BaseDir was specified, use a temporary directory and clean it
// up on completion.
if a.opts.EnvConfig.BaseDir == "" {
tdir, err := ioutil.TempDir("", "vpython")
if err != nil {
return errors.Annotate(err, "failed to create temporary directory").Err()
}
defer func() {
logging.Debugf(c, "Removing temporary directory: %s", tdir)
if terr := filesystem.RemoveAll(tdir); terr != nil {
logging.WithError(terr).Warningf(c, "Failed to clean up temporary directory; leaking: %s", tdir)
}
}()
a.opts.EnvConfig.BaseDir = tdir
}
// Development mode (subcommands).
if a.devMode {
return a.mainDev(c, args)
}
a.opts.Args = args
if err := vpython.Run(c, a.opts); err != nil {
// If the process failed because of a non-zero return value, return that
// as our error.
if rc, has := exitcode.Get(errors.Unwrap(err)); has {
err = ReturnCodeError(rc)
}
return errors.Annotate(err, "").Err()
}
return nil
}
func (a *application) showPythonHelp(c context.Context, fs *flag.FlagSet, lp *lookPath) error {
self, err := os.Executable()
if err != nil {
self = "vpython"
}
vers, err := cipdVersion.GetStartupVersion()
if err == nil && vers.PackageName != "" && vers.InstanceID != "" {
self = fmt.Sprintf("%s (%s@%s)", self, vers.PackageName, vers.InstanceID)
}
fmt.Fprintf(os.Stdout, "Usage of %s:\n", self)
fs.SetOutput(os.Stdout)
fs.PrintDefaults()
i, err := python.Find(c, python.Version{}, lp.look)
if err != nil {
return errors.Annotate(err, "could not find Python interpreter for help").Err()
}
// Redirect all "--help" to Stdout for consistency.
fmt.Fprintf(os.Stdout, "\nPython help for %s:\n", i.Python)
cmd := i.IsolatedCommand(c, "--help")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stdout
if err := cmd.Run(); err != nil {
return errors.Annotate(err, "failed to dump Python help from: %s", i.Python).Err()
}
return nil
}
// Main is the main application entry point.
func (cfg *Config) Main(c context.Context) int {
// Implementation of "checkWrapper": if CheckWrapperENV is set, we immediately
// exit with a non-zero value.
env := environ.System()
if wrapperCheck(env) {
return 1
}
c = gologger.StdConfig.Use(c)
c = logging.SetLevel(c, logging.Error)
a := application{
Config: cfg,
opts: vpython.Options{
EnvConfig: venv.Config{
BaseDir: "", // (Determined below).
MaxHashLen: 6,
Package: cfg.VENVPackage,
PruneThreshold: cfg.PruneThreshold,
MaxPrunesPerSweep: cfg.MaxPrunesPerSweep,
MaxScriptPathLen: cfg.MaxScriptPathLen,
Loader: cfg.PackageLoader,
},
BaseWheels: cfg.BaseWheels,
WaitForEnv: true,
Environ: env,
},
logConfig: logging.Config{
Level: logging.Error,
},
}
return run(c, func(c context.Context) error {
return a.mainImpl(c, os.Args[0], os.Args[1:])
})
}