Skip to content

Commit a45d325

Browse files
kardolusDanny Joyce
andcommitted
Fix bug in order to make pipenv install deterministic
[#161267508] Co-authored-by: Danny Joyce <[email protected]>
1 parent 58606ce commit a45d325

11 files changed

Lines changed: 133 additions & 70 deletions

File tree

fixtures/flask_python_3_pipenv/Pipfile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ url = "https://pypi.python.org/simple"
33
verify_ssl = true
44

55
[packages]
6-
Flask = "==0.10.1"
7-
gunicorn = "==19.3.0"
6+
Flask = "==1.0.2"
7+
gunicorn = "*"
88
itsdangerous = "==0.24"
99
Jinja2 = "==2.7.2"
10-
MarkupSafe = "==0.21"
1110
Werkzeug = "==0.10.4"
1211

1312
[requires]

fixtures/flask_python_3_pipenv/server.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from flask import Flask, request
22
import subprocess
3+
import gunicorn
4+
35

46
app = Flask(__name__)
57

@@ -13,4 +15,9 @@ def execute():
1315
f.write(request.values.get('code'))
1416
return subprocess.check_output(["python", "runtime.py"])
1517

18+
@app.route('/versions')
19+
def versions():
20+
version = gunicorn.__version__
21+
return "Gunicorn version: " + version
22+
1623
app.debug=True

fixtures/flask_python_3_pipenv_vendored_incomplete/Pipfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ url = "https://pypi.python.org/simple"
33
verify_ssl = true
44

55
[packages]
6-
Flask = "==0.10.1"
6+
Flask = "==1.0.2"
77
gunicorn = "==19.3.0"
88
itsdangerous = "==0.24"
99
Jinja2 = "==2.9.6"

fixtures/flask_python_3_pipenv_vendored_incomplete/Pipfile.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/python/brats/brats_suite_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,4 @@ func ApiHasStackAssociation() bool {
7777
supported, err := cutlass.ApiGreaterThan("2.113.0")
7878
Expect(err).NotTo(HaveOccurred())
7979
return supported
80-
}
80+
}

src/python/hooks/appdynamics_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import (
88

99
"path/filepath"
1010

11+
"fmt"
1112
"github.com/cloudfoundry/libbuildpack"
1213
"github.com/cloudfoundry/libbuildpack/ansicleaner"
1314
. "github.com/onsi/ginkgo"
1415
. "github.com/onsi/gomega"
15-
"fmt"
1616
)
1717

1818
func createFile(dir, filename, command string, perm os.FileMode) error {

src/python/integration/deploy_python_app_pipenv_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ var _ = Describe("deploying a flask web app", func() {
2525
PushAppAndConfirm(app)
2626
})
2727

28-
It("gets the python version from pipfile.lock and generates a runtime.txt", func() {
28+
It("deploys a pipenv app", func() {
29+
By("getting the python version from pipfile.lock")
2930
Expect(app.Stdout.String()).To(ContainSubstring("Installing python 3.6."))
30-
Expect(app.Stdout.String()).To(ContainSubstring("Installing pipenv"))
3131
Expect(app.GetBody("/")).To(ContainSubstring("Hello, World with pipenv!"))
32-
Expect(app.Stdout.String()).To(ContainSubstring("Dir checksum unchanged"))
33-
})
3432

35-
It("generates a requirements.txt", func() {
36-
Expect(app.Stdout.String()).To(ContainSubstring("Generating 'requirements.txt' with pipenv"))
33+
By("generating a requirements.txt without updating the pipfile.lock packages")
34+
Expect(app.Stdout.String()).To(ContainSubstring("Generating 'requirements.txt' from Pipfile.lock"))
35+
Expect(app.GetBody("/versions")).To(ContainSubstring("Gunicorn version: 19.3.0"))
36+
Expect(app.Stdout.String()).To(ContainSubstring("Dir checksum unchanged"))
3737
})
3838
})
3939

src/python/integration/deploy_python_app_with_appdynamics_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import (
66

77
"github.com/cloudfoundry/libbuildpack/cutlass"
88

9+
"encoding/json"
910
"fmt"
1011
. "github.com/onsi/ginkgo"
1112
. "github.com/onsi/gomega"
12-
"encoding/json"
1313
)
1414

1515
var _ = Describe("appdynamics", func() {

src/python/integration/fake_supply_before_python_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,17 @@ var _ = Describe("running supply buildpacks before the python buildpack", func()
6262

6363
It("pushes successfully both times", func() {
6464
app.Buildpacks = []string{
65-
"https://buildpacks.cloudfoundry.org/fixtures/supply-cache-new.zip",
66-
"python_buildpack",
67-
}
65+
"https://buildpacks.cloudfoundry.org/fixtures/supply-cache-new.zip",
66+
"python_buildpack",
67+
}
6868
PushAppAndConfirm(app)
6969
Expect(app.GetBody("/")).To(ContainSubstring("Hello, World!"))
7070

7171
app.Buildpacks = []string{
72-
"https://github.com/cloudfoundry/binary-buildpack#develop",
73-
"https://buildpacks.cloudfoundry.org/fixtures/supply-cache-new.zip",
74-
"python_buildpack",
75-
}
72+
"https://github.com/cloudfoundry/binary-buildpack#develop",
73+
"https://buildpacks.cloudfoundry.org/fixtures/supply-cache-new.zip",
74+
"python_buildpack",
75+
}
7676
PushAppAndConfirm(app)
7777
Expect(app.GetBody("/")).To(ContainSubstring("Hello, World!"))
7878
})

src/python/supply/supply.go

Lines changed: 87 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package supply
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"fmt"
67
"io"
78
"io/ioutil"
@@ -47,14 +48,14 @@ type Command interface {
4748
}
4849

4950
type Supplier struct {
50-
PythonVersion string
51-
Manifest Manifest
52-
Installer Installer
53-
Stager Stager
54-
Command Command
55-
Log *libbuildpack.Logger
56-
Logfile *os.File
57-
HasNltkData bool
51+
PythonVersion string
52+
Manifest Manifest
53+
Installer Installer
54+
Stager Stager
55+
Command Command
56+
Log *libbuildpack.Logger
57+
Logfile *os.File
58+
HasNltkData bool
5859
removeRequirementsText bool
5960
}
6061

@@ -340,55 +341,103 @@ func (s *Supplier) InstallPipEnv() error {
340341
requirementstxtExists, err := libbuildpack.FileExists(filepath.Join(s.Stager.BuildDir(), "requirements.txt"))
341342
if err != nil {
342343
return err
344+
} else if requirementstxtExists {
345+
return nil
343346
}
344347

345348
pipfileExists, err := libbuildpack.FileExists(filepath.Join(s.Stager.BuildDir(), "Pipfile"))
346349
if err != nil {
347350
return err
351+
} else if !pipfileExists {
352+
return nil
348353
}
349354

350-
if pipfileExists && !requirementstxtExists {
351-
s.Log.Info("Installing pipenv")
352-
if err := s.Installer.InstallOnlyVersion("pipenv", filepath.Join("/tmp", "pipenv")); err != nil {
353-
return err
355+
hasLockFile, err := libbuildpack.FileExists(filepath.Join(s.Stager.BuildDir(), "Pipfile.lock"))
356+
if err != nil {
357+
return fmt.Errorf("could not check Pipfile.lock existence: %v", err)
358+
} else if hasLockFile {
359+
s.Log.Info("Generating 'requirements.txt' from Pipfile.lock")
360+
requirementsContents, err := pipfileToRequirements(filepath.Join(s.Stager.BuildDir(), "Pipfile.lock"))
361+
if err != nil {
362+
return fmt.Errorf("failed to write `requirement.txt` from Pipfile.lock: %s", err.Error())
354363
}
355364

356-
if err := s.installFfi(); err != nil {
357-
return err
358-
}
365+
return s.writeTempRequirementsTxt(requirementsContents)
366+
}
359367

360-
for _, dep := range []string{"setuptools_scm", "pytest-runner", "parver", "invoke", "pipenv", "wheel"} {
361-
s.Log.Info("Installing %s", dep)
362-
out := &bytes.Buffer{}
363-
stderr := &bytes.Buffer{}
364-
if err := s.Command.Execute(s.Stager.BuildDir(), out, stderr, "pip", "install", dep, "--exists-action=w", "--no-index", fmt.Sprintf("--find-links=%s", filepath.Join("/tmp", "pipenv"))); err != nil {
365-
return fmt.Errorf("Failed to install %s: %v.\nStdout: %v\nStderr: %v", dep, err, out, stderr)
366-
}
368+
s.Log.Info("Installing pipenv")
369+
if err := s.Installer.InstallOnlyVersion("pipenv", filepath.Join("/tmp", "pipenv")); err != nil {
370+
return err
371+
}
372+
373+
if err := s.installFfi(); err != nil {
374+
return err
375+
}
376+
377+
for _, dep := range []string{"setuptools_scm", "pytest-runner", "parver", "invoke", "pipenv", "wheel"} {
378+
s.Log.Info("Installing %s", dep)
379+
out := &bytes.Buffer{}
380+
stderr := &bytes.Buffer{}
381+
if err := s.Command.Execute(s.Stager.BuildDir(), out, stderr, "pip", "install", dep, "--exists-action=w", "--no-index", fmt.Sprintf("--find-links=%s", filepath.Join("/tmp", "pipenv"))); err != nil {
382+
return fmt.Errorf("Failed to install %s: %v.\nStdout: %v\nStderr: %v", dep, err, out, stderr)
367383
}
368-
s.Stager.LinkDirectoryInDepDir(filepath.Join(s.Stager.DepDir(), "python", "bin"), "bin")
369-
s.Log.Info("Generating 'requirements.txt' with pipenv")
384+
}
385+
s.Stager.LinkDirectoryInDepDir(filepath.Join(s.Stager.DepDir(), "python", "bin"), "bin")
370386

371-
cmd := exec.Command("pipenv", "lock", "--requirements")
372-
cmd.Dir = s.Stager.BuildDir()
373-
cmd.Env = append(os.Environ(), "VIRTUALENV_NEVER_DOWNLOAD=true")
374-
output, err := s.Command.RunWithOutput(cmd)
375-
outputString := string(output)
387+
s.Log.Info("Generating 'requirements.txt' with pipenv")
388+
cmd := exec.Command("pipenv", "lock", "--requirements")
389+
cmd.Dir = s.Stager.BuildDir()
390+
cmd.Env = append(os.Environ(), "VIRTUALENV_NEVER_DOWNLOAD=true")
391+
output, err := s.Command.RunWithOutput(cmd)
392+
if err != nil {
393+
return err
394+
}
395+
outputString := string(output)
376396

377-
if err != nil {
378-
return err
397+
// Remove output due to virtualenv
398+
if strings.HasPrefix(outputString, "Using ") {
399+
reqs := strings.SplitN(outputString, "\n", 2)
400+
if len(reqs) > 0 {
401+
outputString = reqs[1]
379402
}
403+
}
380404

381-
// Remove output due to virtualenv
382-
if strings.HasPrefix(outputString, "Using ") {
383-
reqs := strings.SplitN(outputString, "\n", 2)
384-
if len(reqs) > 0 {
385-
outputString = reqs[1]
405+
return s.writeTempRequirementsTxt(outputString)
406+
}
407+
408+
func pipfileToRequirements(lockFilePath string) (string, error) {
409+
var lockFile struct {
410+
Meta struct {
411+
Sources []struct {
412+
URL string
386413
}
414+
} `json:"_meta"`
415+
Default map[string]struct {
416+
Version string
387417
}
418+
}
388419

389-
return s.writeTempRequirementsTxt(outputString)
420+
lockContents, err := ioutil.ReadFile(lockFilePath)
421+
if err != nil {
422+
return "", err
390423
}
391-
return nil
424+
425+
err = json.Unmarshal(lockContents, &lockFile)
426+
if err != nil {
427+
return "", err
428+
}
429+
430+
buf := &bytes.Buffer{}
431+
432+
for _, source := range lockFile.Meta.Sources {
433+
fmt.Fprintf(buf, "-i %s\n", source.URL)
434+
}
435+
436+
for pkg, obj := range lockFile.Default {
437+
fmt.Fprintf(buf, "%s%s\n", pkg, obj.Version)
438+
}
439+
440+
return buf.String(), nil
392441
}
393442

394443
func (s *Supplier) HandlePylibmc() error {

0 commit comments

Comments
 (0)