blob: b87e19ca8658a9793aeeaebafd912b4c3adadc1a [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
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package python
import (
// Interpreter represents a system Python interpreter. It exposes the ability
// to use common functionality of that interpreter.
type Interpreter struct {
// Python is the path to the system Python interpreter.
Python string
// cachedVersion is the cached Version for this interpreter. It is populated
// on the first GetVersion call.
cachedVersion *Version
cachedVersionMu sync.Mutex
// testCommandHook, if not nil, is called on generated Command results prior
// to returning them.
testCommandHook func(*exec.Cmd)
// Normalize normalizes the Interpreter configuration by resolving relative
// paths into absolute paths.
func (i *Interpreter) Normalize() error {
return filesystem.AbsPath(&i.Python)
// IsolatedCommand has an *exec.Cmd, as well as the temporary directory
// created for this Cmd.
type IsolatedCommand struct {
dir string
// Cleanup must be called after the IsolatedCommand is no longer needed.
func (iso IsolatedCommand) Cleanup() {
if err := os.RemoveAll(iso.dir); err != nil {
panic(errors.Annotate(err, "removing IsolatedCommand's directory").Err())
// MkIsolatedCommand returns a configurable exec.Cmd structure bound to this
// Interpreter.
// The supplied arguments have several Python isolation flags prepended to them
// to remove environmental factors such as:
// - The user's "".
// - The current PYTHONPATH environment variable.
// - The current working directory (i.e. avoids `import foo` picking up local
// - Compiled ".pyc/.pyo" files.
// The caller MUST call IsolatedCommand.Cleanup when they no longer need the
// IsolatedCommand.
func (i *Interpreter) MkIsolatedCommand(c context.Context, target Target, args ...string) IsolatedCommand {
// Isolate the supplied arguments.
cl := CommandLine{
Target: target,
Args: args,
cl.AddSingleFlag("B") // Don't compile "pyo" binaries.
cl.AddSingleFlag("E") // Don't use PYTHON* environment variables.
cl.AddSingleFlag("s") // Don't use user ''.
cmd := exec.CommandContext(c, i.Python, cl.BuildArgs()...)
dir, err := ioutil.TempDir("", "")
if err != nil {
panic(errors.Annotate(err, "creating IsolatedCommand's directory").Err())
cmd.Dir = dir
defer func() {
if i.testCommandHook != nil {
return IsolatedCommand{cmd, dir}
// GetVersion runs the specified Python interpreter to extract its version
// from `platform.python_version` and maps it to a known specification version.
func (i *Interpreter) GetVersion(c context.Context) (v Version, err error) {
defer i.cachedVersionMu.Unlock()
// Check again, under write-lock.
if i.cachedVersion != nil {
v = *i.cachedVersion
cmd := i.MkIsolatedCommand(c, CommandTarget{
"import platform, sys; sys.stdout.write(platform.python_version())",
defer cmd.Cleanup()
out, err := cmd.Output()
if err != nil {
err = errors.Annotate(err, "").Err()
if v, err = ParseVersion(string(out)); err != nil {
if v.IsZero() {
err = errors.Reason("unknown version output").Err()
i.cachedVersion = &v
// IsolateEnvironment mutates e to remove any environmental influence over
// the Python interpreter.
// If keepPythonPath is true, PYTHONPATH will not be cleared. This is used
// by the actual VirtualEnv Python invocation to preserve PYTHONPATH since it is
// a form of user input.
// If e is nil, no operation will be performed.
func IsolateEnvironment(e *environ.Env, keepPythonPath bool) {
if e == nil {
// Remove PYTHONPATH if instructed.
if !keepPythonPath {
// Remove PYTHONHOME from the environment. PYTHONHOME is used to set the
// location of standard Python libraries, which we make a point of overriding.
// set PYTHONNOUSERSITE, which prevents a user's "site" configuration
// from influencing Python startup. The system "site" should already be
// ignored b/c we're using the VirtualEnv Python interpreter.