Skip to content

Commit 86c96b1

Browse files
authored
Assign PYTHONPATH instead of PYTHONUSERBASE (#105)
- Doing this will allow other buildpacks to use PYTHONUSERBASE without breaking the environment setup by this buildpack, as assigning PYTHONUSERBASE in this buildpack prevents subsequent buildpacks from adding modules to the application.
1 parent 68766b4 commit 86c96b1

18 files changed

Lines changed: 514 additions & 94 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This buildpack participates if `Pipfile` exists at the root the app.
1111
The buildpack will do the following:
1212
* At build time:
1313
- Installs the application packages to a layer made available to the app.
14-
- Sets the `PYTHONUSERBASE` to this layer.
14+
- Prepends the layer site-packages onto `PYTHONPATH`.
1515
- Prepends the layer's `bin` directory to the `PATH`.
1616
* At run time:
1717
- Does nothing
@@ -43,14 +43,14 @@ file that looks like the following:
4343
[requires.metadata]
4444

4545
# Setting the build flag to true will ensure that the site-packages
46-
# dependency is available on the $PYTHONUSERBASE/$PATH for subsequent
46+
# dependency is available on the $PYTHONPATH/$PATH for subsequent
4747
# buildpacks during their build phase. If you are writing a buildpack that
4848
# needs site-packages during its build process, this flag should be
4949
# set to true.
5050
build = true
5151

5252
# Setting the launch flag to true will ensure that the site-packages
53-
# dependency is available on the $PYTHONUSERBASE/$PATH for the running
53+
# dependency is available on the $PYTHONPATH/$PATH for the running
5454
# application. If you are writing an application that needs site-packages
5555
# at runtime, this flag should be set to true.
5656
launch = true

build.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package pipenvinstall
22

33
import (
44
"os"
5+
"path/filepath"
56
"time"
67

78
"github.com/paketo-buildpacks/packit"
@@ -12,25 +13,45 @@ import (
1213

1314
//go:generate faux --interface EntryResolver --output fakes/entry_resolver.go
1415
//go:generate faux --interface InstallProcess --output fakes/install_process.go
16+
//go:generate faux --interface SitePackagesProcess --output fakes/site_packages_process.go
17+
//go:generate faux --interface VenvDirLocator --output fakes/venv_dir_locator.go
1518

1619
// EntryResolver defines the interface for picking the most relevant entry from
1720
// the Buildpack Plan entries.
1821
type EntryResolver interface {
1922
MergeLayerTypes(name string, entries []packit.BuildpackPlanEntry) (launch, build bool)
2023
}
2124

25+
// SitePackagesProcess defines the interface for determining the site-packages path.
26+
type SitePackagesProcess interface {
27+
Execute(layerPath string) (sitePackagesPath string, err error)
28+
}
29+
2230
// InstallProcess defines the interface for installing the pipenv dependencies.
2331
type InstallProcess interface {
2432
Execute(workingDir string, targetLayer, cacheLayer packit.Layer) error
2533
}
2634

35+
// VenvDirLocator defines the interface for locating the virtual environment
36+
// directory under a given path
37+
type VenvDirLocator interface {
38+
LocateVenvDir(path string) (venvDir string, err error)
39+
}
40+
2741
// Build will return a packit.BuildFunc that will be invoked during the build
2842
// phase of the buildpack lifecycle.
2943
//
3044
// Build will install the pipenv dependencies by using the Pipfile to a
3145
// packages layer. It also makes use of a cache layer to reuse the pipenv
3246
// cache.
33-
func Build(entryResolver EntryResolver, installProcess InstallProcess, clock chronos.Clock, logger scribe.Emitter) packit.BuildFunc {
47+
func Build(
48+
entryResolver EntryResolver,
49+
installProcess InstallProcess,
50+
siteProcess SitePackagesProcess,
51+
venvDirLocator VenvDirLocator,
52+
clock chronos.Clock,
53+
logger scribe.Emitter,
54+
) packit.BuildFunc {
3455
return func(context packit.BuildContext) (packit.BuildResult, error) {
3556
logger.Title("%s %s", context.BuildpackInfo.Name, context.BuildpackInfo.Version)
3657

@@ -59,6 +80,22 @@ func Build(entryResolver EntryResolver, installProcess InstallProcess, clock chr
5980
logger.Action("Completed in %s", duration.Round(time.Millisecond))
6081
logger.Break()
6182

83+
venvDir, err := venvDirLocator.LocateVenvDir(packagesLayer.Path)
84+
if err != nil {
85+
return packit.BuildResult{}, err
86+
}
87+
88+
sitePackagesPath, err := siteProcess.Execute(packagesLayer.Path)
89+
if err != nil {
90+
return packit.BuildResult{}, err
91+
}
92+
93+
logger.Process("Configuring environment")
94+
packagesLayer.SharedEnv.Prepend("PATH", filepath.Join(venvDir, "bin"), ":")
95+
packagesLayer.SharedEnv.Prepend("PYTHONPATH", sitePackagesPath, string(os.PathListSeparator))
96+
logger.Subprocess("%s", scribe.NewFormattedMapFromEnvironment(packagesLayer.SharedEnv))
97+
logger.Break()
98+
6299
packagesLayer.Metadata = map[string]interface{}{
63100
"built_at": clock.Now().Format(time.RFC3339Nano),
64101
}

build_test.go

Lines changed: 104 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
2828
workingDir string
2929
cnbDir string
3030

31-
clock chronos.Clock
32-
timeStamp time.Time
33-
installProcess *fakes.InstallProcess
34-
buffer *bytes.Buffer
35-
logEmitter scribe.Emitter
36-
entryResolver *fakes.EntryResolver
31+
clock chronos.Clock
32+
timeStamp time.Time
33+
buffer *bytes.Buffer
34+
logEmitter scribe.Emitter
35+
36+
entryResolver *fakes.EntryResolver
37+
installProcess *fakes.InstallProcess
38+
sitePackagesProcess *fakes.SitePackagesProcess
39+
venvDirLocator *fakes.VenvDirLocator
3740

3841
build packit.BuildFunc
3942
)
@@ -49,8 +52,13 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
4952
cnbDir, err = ioutil.TempDir("", "cnb")
5053
Expect(err).NotTo(HaveOccurred())
5154

52-
installProcess = &fakes.InstallProcess{}
5355
entryResolver = &fakes.EntryResolver{}
56+
installProcess = &fakes.InstallProcess{}
57+
sitePackagesProcess = &fakes.SitePackagesProcess{}
58+
venvDirLocator = &fakes.VenvDirLocator{}
59+
60+
sitePackagesProcess.ExecuteCall.Returns.SitePackagesPath = "some-site-packages-path"
61+
venvDirLocator.LocateVenvDirCall.Returns.VenvDir = "some-venv-dir"
5462

5563
buffer = bytes.NewBuffer(nil)
5664
logEmitter = scribe.NewEmitter(buffer)
@@ -60,7 +68,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
6068
return timeStamp
6169
})
6270

63-
build = pipenvinstall.Build(entryResolver, installProcess, clock, logEmitter)
71+
build = pipenvinstall.Build(entryResolver, installProcess, sitePackagesProcess, venvDirLocator, clock, logEmitter)
6472
})
6573

6674
it.After(func() {
@@ -79,7 +87,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
7987
Plan: packit.BuildpackPlan{
8088
Entries: []packit.BuildpackPlanEntry{
8189
{
82-
Name: pipenvinstall.SitePackages,
90+
Name: "site-packages",
8391
},
8492
},
8593
},
@@ -92,9 +100,14 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
92100
Expect(result).To(Equal(packit.BuildResult{
93101
Layers: []packit.Layer{
94102
{
95-
Name: pipenvinstall.PackagesLayerName,
96-
Path: filepath.Join(layersDir, pipenvinstall.PackagesLayerName),
97-
SharedEnv: packit.Environment{},
103+
Name: "packages",
104+
Path: filepath.Join(layersDir, "packages"),
105+
SharedEnv: packit.Environment{
106+
"PATH.delim": ":",
107+
"PATH.prepend": filepath.Join("some-venv-dir", "bin"),
108+
"PYTHONPATH.delim": ":",
109+
"PYTHONPATH.prepend": "some-site-packages-path",
110+
},
98111
BuildEnv: packit.Environment{},
99112
LaunchEnv: packit.Environment{},
100113
ProcessLaunchEnv: map[string]packit.Environment{},
@@ -110,12 +123,12 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
110123
}))
111124

112125
Expect(installProcess.ExecuteCall.Receives.WorkingDir).To(Equal(workingDir))
113-
Expect(installProcess.ExecuteCall.Receives.TargetLayer.Path).To(Equal(filepath.Join(layersDir, pipenvinstall.PackagesLayerName)))
114-
Expect(installProcess.ExecuteCall.Receives.CacheLayer.Path).To(Equal(filepath.Join(layersDir, pipenvinstall.CacheLayerName)))
126+
Expect(installProcess.ExecuteCall.Receives.TargetLayer.Path).To(Equal(filepath.Join(layersDir, "packages")))
127+
Expect(installProcess.ExecuteCall.Receives.CacheLayer.Path).To(Equal(filepath.Join(layersDir, "cache")))
115128

116-
Expect(entryResolver.MergeLayerTypesCall.Receives.Name).To(Equal(pipenvinstall.SitePackages))
129+
Expect(entryResolver.MergeLayerTypesCall.Receives.Name).To(Equal("site-packages"))
117130
Expect(entryResolver.MergeLayerTypesCall.Receives.Entries).To(Equal([]packit.BuildpackPlanEntry{
118-
{Name: pipenvinstall.SitePackages},
131+
{Name: "site-packages"},
119132
}))
120133

121134
Expect(buffer.String()).To(ContainSubstring("Some Buildpack some-version"))
@@ -139,7 +152,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
139152
Plan: packit.BuildpackPlan{
140153
Entries: []packit.BuildpackPlanEntry{
141154
{
142-
Name: pipenvinstall.SitePackages,
155+
Name: "site-packages",
143156
},
144157
},
145158
},
@@ -152,9 +165,14 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
152165
Expect(result).To(Equal(packit.BuildResult{
153166
Layers: []packit.Layer{
154167
{
155-
Name: pipenvinstall.PackagesLayerName,
156-
Path: filepath.Join(layersDir, pipenvinstall.PackagesLayerName),
157-
SharedEnv: packit.Environment{},
168+
Name: "packages",
169+
Path: filepath.Join(layersDir, "packages"),
170+
SharedEnv: packit.Environment{
171+
"PATH.delim": ":",
172+
"PATH.prepend": filepath.Join("some-venv-dir", "bin"),
173+
"PYTHONPATH.delim": ":",
174+
"PYTHONPATH.prepend": "some-site-packages-path",
175+
},
158176
BuildEnv: packit.Environment{},
159177
LaunchEnv: packit.Environment{},
160178
ProcessLaunchEnv: map[string]packit.Environment{},
@@ -195,7 +213,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
195213
Plan: packit.BuildpackPlan{
196214
Entries: []packit.BuildpackPlanEntry{
197215
{
198-
Name: pipenvinstall.SitePackages,
216+
Name: "site-packages",
199217
},
200218
},
201219
},
@@ -208,9 +226,15 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
208226
Expect(result).To(Equal(packit.BuildResult{
209227
Layers: []packit.Layer{
210228
{
211-
Name: pipenvinstall.PackagesLayerName,
212-
Path: filepath.Join(layersDir, pipenvinstall.PackagesLayerName),
213-
SharedEnv: packit.Environment{},
229+
Name: "packages",
230+
Path: filepath.Join(layersDir, "packages"),
231+
SharedEnv: packit.Environment{
232+
"PATH.delim": ":",
233+
"PATH.prepend": filepath.Join("some-venv-dir", "bin"),
234+
"PYTHONPATH.delim": ":",
235+
"PYTHONPATH.prepend": "some-site-packages-path",
236+
},
237+
214238
BuildEnv: packit.Environment{},
215239
LaunchEnv: packit.Environment{},
216240
ProcessLaunchEnv: map[string]packit.Environment{},
@@ -223,8 +247,8 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
223247
},
224248
},
225249
{
226-
Name: pipenvinstall.CacheLayerName,
227-
Path: filepath.Join(layersDir, pipenvinstall.CacheLayerName),
250+
Name: "cache",
251+
Path: filepath.Join(layersDir, "cache"),
228252
SharedEnv: packit.Environment{},
229253
BuildEnv: packit.Environment{},
230254
LaunchEnv: packit.Environment{},
@@ -296,5 +320,59 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
296320
Expect(err).To(MatchError(ContainSubstring("some-error")))
297321
})
298322
})
323+
324+
context("when venv directory locator returns an error", func() {
325+
it.Before(func() {
326+
venvDirLocator.LocateVenvDirCall.Returns.Err = errors.New("some-venv-error")
327+
})
328+
329+
it("returns an error", func() {
330+
_, err := build(packit.BuildContext{
331+
WorkingDir: workingDir,
332+
CNBPath: cnbDir,
333+
Stack: "some-stack",
334+
BuildpackInfo: packit.BuildpackInfo{
335+
Name: "Some Buildpack",
336+
Version: "some-version",
337+
},
338+
Plan: packit.BuildpackPlan{
339+
Entries: []packit.BuildpackPlanEntry{
340+
{
341+
Name: "site-packages",
342+
},
343+
},
344+
},
345+
Layers: packit.Layers{Path: layersDir},
346+
})
347+
Expect(err).To(MatchError(ContainSubstring("some-venv-error")))
348+
})
349+
})
350+
351+
context("when site packages process locator returns an error", func() {
352+
it.Before(func() {
353+
sitePackagesProcess.ExecuteCall.Returns.Err = errors.New("some-site-error")
354+
})
355+
356+
it("returns an error", func() {
357+
_, err := build(packit.BuildContext{
358+
WorkingDir: workingDir,
359+
CNBPath: cnbDir,
360+
Stack: "some-stack",
361+
BuildpackInfo: packit.BuildpackInfo{
362+
Name: "Some Buildpack",
363+
Version: "some-version",
364+
},
365+
Plan: packit.BuildpackPlan{
366+
Entries: []packit.BuildpackPlanEntry{
367+
{
368+
Name: "site-packages",
369+
},
370+
},
371+
},
372+
Layers: packit.Layers{Path: layersDir},
373+
})
374+
Expect(err).To(MatchError(ContainSubstring("some-site-error")))
375+
})
376+
})
299377
})
300378
}

fakes/entry_resolver.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88

99
type EntryResolver struct {
1010
MergeLayerTypesCall struct {
11-
sync.Mutex
11+
mutex sync.Mutex
1212
CallCount int
1313
Receives struct {
1414
Name string
@@ -23,8 +23,8 @@ type EntryResolver struct {
2323
}
2424

2525
func (f *EntryResolver) MergeLayerTypes(param1 string, param2 []packit.BuildpackPlanEntry) (bool, bool) {
26-
f.MergeLayerTypesCall.Lock()
27-
defer f.MergeLayerTypesCall.Unlock()
26+
f.MergeLayerTypesCall.mutex.Lock()
27+
defer f.MergeLayerTypesCall.mutex.Unlock()
2828
f.MergeLayerTypesCall.CallCount++
2929
f.MergeLayerTypesCall.Receives.Name = param1
3030
f.MergeLayerTypesCall.Receives.Entries = param2

fakes/executable.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88

99
type Executable struct {
1010
ExecuteCall struct {
11-
sync.Mutex
11+
mutex sync.Mutex
1212
CallCount int
1313
Receives struct {
1414
Execution pexec.Execution
@@ -21,8 +21,8 @@ type Executable struct {
2121
}
2222

2323
func (f *Executable) Execute(param1 pexec.Execution) error {
24-
f.ExecuteCall.Lock()
25-
defer f.ExecuteCall.Unlock()
24+
f.ExecuteCall.mutex.Lock()
25+
defer f.ExecuteCall.mutex.Unlock()
2626
f.ExecuteCall.CallCount++
2727
f.ExecuteCall.Receives.Execution = param1
2828
if f.ExecuteCall.Stub != nil {

fakes/install_process.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88

99
type InstallProcess struct {
1010
ExecuteCall struct {
11-
sync.Mutex
11+
mutex sync.Mutex
1212
CallCount int
1313
Receives struct {
1414
WorkingDir string
@@ -23,8 +23,8 @@ type InstallProcess struct {
2323
}
2424

2525
func (f *InstallProcess) Execute(param1 string, param2 packit.Layer, param3 packit.Layer) error {
26-
f.ExecuteCall.Lock()
27-
defer f.ExecuteCall.Unlock()
26+
f.ExecuteCall.mutex.Lock()
27+
defer f.ExecuteCall.mutex.Unlock()
2828
f.ExecuteCall.CallCount++
2929
f.ExecuteCall.Receives.WorkingDir = param1
3030
f.ExecuteCall.Receives.TargetLayer = param2

fakes/parser.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import "sync"
44

55
type Parser struct {
66
ParseVersionCall struct {
7-
sync.Mutex
7+
mutex sync.Mutex
88
CallCount int
99
Receives struct {
1010
Path string
@@ -18,8 +18,8 @@ type Parser struct {
1818
}
1919

2020
func (f *Parser) ParseVersion(param1 string) (string, error) {
21-
f.ParseVersionCall.Lock()
22-
defer f.ParseVersionCall.Unlock()
21+
f.ParseVersionCall.mutex.Lock()
22+
defer f.ParseVersionCall.mutex.Unlock()
2323
f.ParseVersionCall.CallCount++
2424
f.ParseVersionCall.Receives.Path = param1
2525
if f.ParseVersionCall.Stub != nil {

0 commit comments

Comments
 (0)