You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

164 lines
4.6 KiB

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
}