package config
|
|
|
|
import (
|
|
"gerrit.wikimedia.org/r/blubber/build"
|
|
)
|
|
|
|
// PythonLibPrefix is the path to installed dependency wheels.
|
|
//
|
|
const PythonLibPrefix = LocalLibPrefix + "/python"
|
|
|
|
// PythonSitePackages is the path to installed Python packages.
|
|
//
|
|
const PythonSitePackages = PythonLibPrefix + "/site-packages"
|
|
|
|
// PythonSiteBin is the path to installed Python packages bin files.
|
|
//
|
|
const PythonSiteBin = PythonSitePackages + "/bin"
|
|
|
|
// PythonConfig holds configuration fields related to pre-installation of project
|
|
// dependencies via PIP.
|
|
//
|
|
type PythonConfig struct {
|
|
Version string `json:"version"` // Python binary to use when installing dependencies
|
|
Requirements []string `json:"requirements"` // install requirements from given files
|
|
UseSystemFlag bool `json:"use-system-flag"` // Inject the --system flag into the install command (T227919)
|
|
}
|
|
|
|
// Merge takes another PythonConfig and merges its fields into this one's,
|
|
// overwriting both the dependencies flag and requirements.
|
|
//
|
|
func (pc *PythonConfig) Merge(pc2 PythonConfig) {
|
|
if pc2.Version != "" {
|
|
pc.Version = pc2.Version
|
|
}
|
|
|
|
if pc2.Requirements != nil {
|
|
pc.Requirements = pc2.Requirements
|
|
}
|
|
|
|
if pc2.UseSystemFlag {
|
|
pc.UseSystemFlag = true
|
|
}
|
|
}
|
|
|
|
// InstructionsForPhase injects instructions into the build related to Python
|
|
// dependency installation.
|
|
//
|
|
// PhasePrivileged
|
|
//
|
|
// Ensures that the newest versions of setuptools, wheel, tox, and pip are
|
|
// installed.
|
|
//
|
|
// PhasePreInstall
|
|
//
|
|
// Sets up Python wheels under the shared library directory (/opt/lib/python)
|
|
// for dependencies found in the declared requirements files. Installing
|
|
// dependencies during the build.PhasePreInstall phase allows a compiler
|
|
// implementation (e.g. Docker) to produce cache-efficient output so only
|
|
// changes to the given requirements files will invalidate these steps of the
|
|
// image build.
|
|
//
|
|
// Injects build.Env instructions for PIP_WHEEL_DIR and PIP_FIND_LINKS that
|
|
// will cause future executions of `pip install` (and by extension, `tox`) to
|
|
// consider packages from the shared library directory first.
|
|
//
|
|
// PhasePostInstall
|
|
//
|
|
// Injects a build.Env instruction for PIP_NO_INDEX that will cause future
|
|
// executions of `pip install` and `tox` to consider _only_ packages from the
|
|
// shared library directory, helping to speed up image builds by reducing
|
|
// network requests from said commands.
|
|
//
|
|
func (pc PythonConfig) InstructionsForPhase(phase build.Phase) []build.Instruction {
|
|
if pc.Version != "" {
|
|
switch phase {
|
|
case build.PhasePrivileged:
|
|
if pc.Requirements != nil {
|
|
return []build.Instruction{build.RunAll{[]build.Run{
|
|
{pc.version(), []string{"-m", "easy_install", "pip"}},
|
|
{pc.version(), []string{"-m", "pip", "install", "-U", "setuptools", "wheel", "tox"}},
|
|
}}}
|
|
}
|
|
|
|
case build.PhasePreInstall:
|
|
if pc.Requirements != nil {
|
|
ins := []build.Instruction{
|
|
build.Env{map[string]string{
|
|
"PIP_WHEEL_DIR": PythonLibPrefix,
|
|
"PIP_FIND_LINKS": "file://" + PythonLibPrefix,
|
|
}},
|
|
build.CreateDirectory(PythonLibPrefix),
|
|
}
|
|
|
|
ins = append(ins, build.SyncFiles(pc.Requirements, ".")...)
|
|
|
|
if args := pc.RequirementsArgs(); len(args) > 0 {
|
|
installCmd := append([]string{"-m", "pip", "install", "--target"}, PythonSitePackages)
|
|
if pc.UseSystemFlag {
|
|
installCmd = InsertElement(installCmd, "--system", PosOf(installCmd, "install") + 1)
|
|
}
|
|
ins = append(ins, build.RunAll{[]build.Run{
|
|
{pc.version(), append([]string{"-m", "pip", "wheel"}, args...)},
|
|
{pc.version(), append(installCmd, args...)},
|
|
}})
|
|
}
|
|
|
|
return ins
|
|
}
|
|
|
|
case build.PhasePostInstall:
|
|
env := build.Env{map[string]string{
|
|
"PYTHONPATH": PythonSitePackages,
|
|
"PATH": PythonSiteBin + ":${PATH}",
|
|
}}
|
|
|
|
if pc.Requirements != nil {
|
|
env.Definitions["PIP_NO_INDEX"] = "1"
|
|
}
|
|
|
|
return []build.Instruction{env}
|
|
}
|
|
}
|
|
|
|
return []build.Instruction{}
|
|
}
|
|
|
|
// RequirementsArgs returns the configured requirements as pip `-r` arguments.
|
|
//
|
|
func (pc PythonConfig) RequirementsArgs() []string {
|
|
args := make([]string, len(pc.Requirements)*2)
|
|
|
|
for i, req := range pc.Requirements {
|
|
args[i*2] = "-r"
|
|
args[(i*2)+1] = req
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
func (pc PythonConfig) version() string {
|
|
if pc.Version == "" {
|
|
return "python"
|
|
}
|
|
|
|
return pc.Version
|
|
}
|
|
|
|
// InsertElement - insert el into slice at pos
|
|
func InsertElement(slice []string, el string, pos int) []string {
|
|
slice = append(slice, "")
|
|
copy(slice[pos+1:], slice[pos:])
|
|
slice[pos] = el
|
|
return slice
|
|
}
|
|
|
|
// PosOf - find position of an element in a slice
|
|
func PosOf(slice []string, el string) int {
|
|
for p, v := range slice {
|
|
if v == el {
|
|
return p
|
|
}
|
|
}
|
|
return -1
|
|
}
|