pulumi/sdk/python/python.go
Justin Van Patten ce4badd488
Update pip/setuptools/wheel in virtual environment (#5042)
Update pip, setuptools, and wheel in the virtual environment before installing dependencies as recommended by the Python documentation. This should help avoid failures when only source distributions are available for a package and pip attempts to build a wheel locally.
2020-07-23 13:33:09 -07:00

207 lines
6 KiB
Go

// Copyright 2016-2020, Pulumi Corporation.
//
// 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 python
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/pkg/errors"
)
const windows = "windows"
// Command returns an *exec.Cmd for running `python`. If the `PULUMI_PYTHON_CMD` variable is set
// it will be looked for on `PATH`, otherwise, `python3` and `python` will be looked for.
func Command(arg ...string) (*exec.Cmd, error) {
var err error
var pythonCmds []string
var pythonPath string
if pythonCmd := os.Getenv("PULUMI_PYTHON_CMD"); pythonCmd != "" {
pythonCmds = []string{pythonCmd}
} else {
// Look for "python3" by default, but fallback to `python` if not found as some Python 3
// distributions (in particular the default python.org Windows installation) do not include
// a `python3` binary.
pythonCmds = []string{"python3", "python"}
}
for _, pythonCmd := range pythonCmds {
pythonPath, err = exec.LookPath(pythonCmd)
// Break on the first cmd we find on the path (if any)
if err == nil {
break
}
}
if err != nil {
return nil, fmt.Errorf(
"Failed to locate any of %q on your PATH. Have you installed Python 3.6 or greater?",
pythonCmds)
}
return exec.Command(pythonPath, arg...), nil
}
// VirtualEnvCommand returns an *exec.Cmd for running a command from the specified virtual environment
// directory.
func VirtualEnvCommand(virtualEnvDir, name string, arg ...string) *exec.Cmd {
if runtime.GOOS == windows {
name = fmt.Sprintf("%s.exe", name)
}
cmdPath := filepath.Join(virtualEnvDir, virtualEnvBinDirName(), name)
return exec.Command(cmdPath, arg...)
}
// IsVirtualEnv returns true if the specified directory contains a python binary.
func IsVirtualEnv(dir string) bool {
pyBin := filepath.Join(dir, virtualEnvBinDirName(), "python")
if runtime.GOOS == windows {
pyBin = fmt.Sprintf("%s.exe", pyBin)
}
if info, err := os.Stat(pyBin); err == nil && !info.IsDir() {
return true
}
return false
}
// ActivateVirtualEnv takes an array of environment variables (same format as os.Environ()) and path to
// a virtual environment directory, and returns a new "activated" array with the virtual environment's
// "bin" dir ("Scripts" on Windows) prepended to the `PATH` environment variable and `PYTHONHOME` variable
// removed.
func ActivateVirtualEnv(environ []string, virtualEnvDir string) []string {
virtualEnvBin := filepath.Join(virtualEnvDir, virtualEnvBinDirName())
var hasPath bool
var result []string
for _, env := range environ {
if strings.HasPrefix(env, "PATH=") {
hasPath = true
// Prepend the virtual environment bin directory to PATH so any calls to run
// python or pip will use the binaries in the virtual environment.
originalValue := env[len("PATH="):]
path := fmt.Sprintf("PATH=%s%s%s", virtualEnvBin, string(os.PathListSeparator), originalValue)
result = append(result, path)
} else if strings.HasPrefix(env, "PYTHONHOME=") {
// Skip PYTHONHOME to "unset" this value.
} else {
result = append(result, env)
}
}
if !hasPath {
path := fmt.Sprintf("PATH=%s", virtualEnvBin)
result = append(result, path)
}
return result
}
// InstallDependencies will create a new virtual environment and install dependencies in the root directory.
func InstallDependencies(root string, showOutput bool, saveProj func(virtualenv string) error) error {
print := func(message string) {
if showOutput {
fmt.Println(message)
fmt.Println()
}
}
print("Creating virtual environment...")
// Create the virtual environment by running `python -m venv venv`.
venvDir := filepath.Join(root, "venv")
cmd, err := Command("-m", "venv", venvDir)
if err != nil {
return err
}
if output, err := cmd.CombinedOutput(); err != nil {
if len(output) > 0 {
os.Stdout.Write(output)
fmt.Println()
}
return errors.Wrapf(err, "creating virtual environment at %s", venvDir)
}
// Save project with venv info.
if err := saveProj("venv"); err != nil {
return err
}
print("Finished creating virtual environment")
runPipInstall := func(errorMsg string, arg ...string) error {
pipCmd := VirtualEnvCommand(venvDir, "python", append([]string{"-m", "pip", "install"}, arg...)...)
pipCmd.Dir = root
pipCmd.Env = ActivateVirtualEnv(os.Environ(), venvDir)
wrapError := func(err error) error {
return errors.Wrapf(err, "%s via '%s'", errorMsg, strings.Join(pipCmd.Args, " "))
}
if showOutput {
// Show stdout/stderr output.
pipCmd.Stdout = os.Stdout
pipCmd.Stderr = os.Stderr
if err := pipCmd.Run(); err != nil {
return wrapError(err)
}
} else {
// Otherwise, only show output if there is an error.
if output, err := pipCmd.CombinedOutput(); err != nil {
if len(output) > 0 {
os.Stdout.Write(output)
fmt.Println()
}
return wrapError(err)
}
}
return nil
}
print("Updating pip, setuptools, and wheel in virtual environment...")
err = runPipInstall("updating pip, setuptools, and wheel", "--upgrade", "pip", "setuptools", "wheel")
if err != nil {
return err
}
print("Finished updating")
// If `requirements.txt` doesn't exist, exit early.
requirementsPath := filepath.Join(root, "requirements.txt")
if _, err := os.Stat(requirementsPath); os.IsNotExist(err) {
return nil
}
print("Installing dependencies in virtual environment...")
err = runPipInstall("installing dependencies", "-r", "requirements.txt")
if err != nil {
return err
}
print("Finished installing dependencies")
return nil
}
func virtualEnvBinDirName() string {
if runtime.GOOS == windows {
return "Scripts"
}
return "bin"
}