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

  1. package config
  2. import (
  3. "gerrit.wikimedia.org/r/blubber/build"
  4. )
  5. // PythonLibPrefix is the path to installed dependency wheels.
  6. //
  7. const PythonLibPrefix = LocalLibPrefix + "/python"
  8. // PythonSitePackages is the path to installed Python packages.
  9. //
  10. const PythonSitePackages = PythonLibPrefix + "/site-packages"
  11. // PythonSiteBin is the path to installed Python packages bin files.
  12. //
  13. const PythonSiteBin = PythonSitePackages + "/bin"
  14. // PythonConfig holds configuration fields related to pre-installation of project
  15. // dependencies via PIP.
  16. //
  17. type PythonConfig struct {
  18. Version string `json:"version"` // Python binary to use when installing dependencies
  19. Requirements []string `json:"requirements"` // install requirements from given files
  20. UseSystemFlag bool `json:"use-system-flag"` // Inject the --system flag into the install command (T227919)
  21. }
  22. // Merge takes another PythonConfig and merges its fields into this one's,
  23. // overwriting both the dependencies flag and requirements.
  24. //
  25. func (pc *PythonConfig) Merge(pc2 PythonConfig) {
  26. if pc2.Version != "" {
  27. pc.Version = pc2.Version
  28. }
  29. if pc2.Requirements != nil {
  30. pc.Requirements = pc2.Requirements
  31. }
  32. if pc2.UseSystemFlag {
  33. pc.UseSystemFlag = true
  34. }
  35. }
  36. // InstructionsForPhase injects instructions into the build related to Python
  37. // dependency installation.
  38. //
  39. // PhasePrivileged
  40. //
  41. // Ensures that the newest versions of setuptools, wheel, tox, and pip are
  42. // installed.
  43. //
  44. // PhasePreInstall
  45. //
  46. // Sets up Python wheels under the shared library directory (/opt/lib/python)
  47. // for dependencies found in the declared requirements files. Installing
  48. // dependencies during the build.PhasePreInstall phase allows a compiler
  49. // implementation (e.g. Docker) to produce cache-efficient output so only
  50. // changes to the given requirements files will invalidate these steps of the
  51. // image build.
  52. //
  53. // Injects build.Env instructions for PIP_WHEEL_DIR and PIP_FIND_LINKS that
  54. // will cause future executions of `pip install` (and by extension, `tox`) to
  55. // consider packages from the shared library directory first.
  56. //
  57. // PhasePostInstall
  58. //
  59. // Injects a build.Env instruction for PIP_NO_INDEX that will cause future
  60. // executions of `pip install` and `tox` to consider _only_ packages from the
  61. // shared library directory, helping to speed up image builds by reducing
  62. // network requests from said commands.
  63. //
  64. func (pc PythonConfig) InstructionsForPhase(phase build.Phase) []build.Instruction {
  65. if pc.Version != "" {
  66. switch phase {
  67. case build.PhasePrivileged:
  68. if pc.Requirements != nil {
  69. return []build.Instruction{build.RunAll{[]build.Run{
  70. {pc.version(), []string{"-m", "easy_install", "pip"}},
  71. {pc.version(), []string{"-m", "pip", "install", "-U", "setuptools", "wheel", "tox"}},
  72. }}}
  73. }
  74. case build.PhasePreInstall:
  75. if pc.Requirements != nil {
  76. ins := []build.Instruction{
  77. build.Env{map[string]string{
  78. "PIP_WHEEL_DIR": PythonLibPrefix,
  79. "PIP_FIND_LINKS": "file://" + PythonLibPrefix,
  80. }},
  81. build.CreateDirectory(PythonLibPrefix),
  82. }
  83. ins = append(ins, build.SyncFiles(pc.Requirements, ".")...)
  84. if args := pc.RequirementsArgs(); len(args) > 0 {
  85. installCmd := append([]string{"-m", "pip", "install", "--target"}, PythonSitePackages)
  86. if pc.UseSystemFlag {
  87. installCmd = InsertElement(installCmd, "--system", PosOf(installCmd, "install") + 1)
  88. }
  89. ins = append(ins, build.RunAll{[]build.Run{
  90. {pc.version(), append([]string{"-m", "pip", "wheel"}, args...)},
  91. {pc.version(), append(installCmd, args...)},
  92. }})
  93. }
  94. return ins
  95. }
  96. case build.PhasePostInstall:
  97. env := build.Env{map[string]string{
  98. "PYTHONPATH": PythonSitePackages,
  99. "PATH": PythonSiteBin + ":${PATH}",
  100. }}
  101. if pc.Requirements != nil {
  102. env.Definitions["PIP_NO_INDEX"] = "1"
  103. }
  104. return []build.Instruction{env}
  105. }
  106. }
  107. return []build.Instruction{}
  108. }
  109. // RequirementsArgs returns the configured requirements as pip `-r` arguments.
  110. //
  111. func (pc PythonConfig) RequirementsArgs() []string {
  112. args := make([]string, len(pc.Requirements)*2)
  113. for i, req := range pc.Requirements {
  114. args[i*2] = "-r"
  115. args[(i*2)+1] = req
  116. }
  117. return args
  118. }
  119. func (pc PythonConfig) version() string {
  120. if pc.Version == "" {
  121. return "python"
  122. }
  123. return pc.Version
  124. }
  125. // InsertElement - insert el into slice at pos
  126. func InsertElement(slice []string, el string, pos int) []string {
  127. slice = append(slice, "")
  128. copy(slice[pos+1:], slice[pos:])
  129. slice[pos] = el
  130. return slice
  131. }
  132. // PosOf - find position of an element in a slice
  133. func PosOf(slice []string, el string) int {
  134. for p, v := range slice {
  135. if v == el {
  136. return p
  137. }
  138. }
  139. return -1
  140. }