Skip to content

Commit e59c234

Browse files
authored
feat: implement uv support (#45)
* feat: implement support for uv * refactor(integration): use python-installers for dependencies Since version 0.2.0, python-installers is self-sufficient for pipenv and poetry installation. This means it can replace all the other package manager installers.
1 parent 10a0202 commit e59c234

56 files changed

Lines changed: 2748 additions & 106 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/update-dependencies-from-metadata.yml

Lines changed: 378 additions & 0 deletions
Large diffs are not rendered by default.

REUSE.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ SPDX-FileCopyrightText = "© 2025 Idiap Research Institute <contact@idiap.ch>"
1212
SPDX-License-Identifier = "Apache-2.0"
1313

1414
[[annotations]]
15-
path = "scripts/.util/tools.json"
15+
path = "scripts/**"
1616
precedence = "override"
1717
SPDX-FileCopyrightText = "Copyright (c) 2013-Present CloudFoundry.org Foundation, Inc. All Rights Reserved."
1818
SPDX-License-Identifier = "Apache-2.0"

build.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
pipinstall "github.com/paketo-buildpacks/python-packagers/pkg/packagers/pip"
1414
pipenvinstall "github.com/paketo-buildpacks/python-packagers/pkg/packagers/pipenv"
1515
poetryinstall "github.com/paketo-buildpacks/python-packagers/pkg/packagers/poetry"
16+
uvinstall "github.com/paketo-buildpacks/python-packagers/pkg/packagers/uv"
1617

1718
pythonpackagers "github.com/paketo-buildpacks/python-packagers/pkg/packagers/common"
1819
)
@@ -107,6 +108,21 @@ func Build(
107108
} else {
108109
return packit.BuildResult{}, packit.Fail.WithMessage("missing plan for: %s", entry.Name)
109110
}
111+
case uvinstall.UvEnvPlanEntry:
112+
if parameters, ok := buildParameters[uvinstall.UvEnvPlanEntry]; ok {
113+
uvResult, err := uvinstall.Build(
114+
parameters.(uvinstall.UvBuildParameters),
115+
commonBuildParameters,
116+
)(context)
117+
118+
if err != nil {
119+
return packit.BuildResult{}, err
120+
}
121+
122+
layers = append(layers, uvResult.Layers...)
123+
} else {
124+
return packit.BuildResult{}, packit.Fail.WithMessage("missing plan for: %s", entry.Name)
125+
}
110126
default:
111127
return packit.BuildResult{}, packit.Fail.WithMessage("unknown plan: %s", entry.Name)
112128
}

build_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626
pipenvfakes "github.com/paketo-buildpacks/python-packagers/pkg/packagers/pipenv/fakes"
2727
poetryinstall "github.com/paketo-buildpacks/python-packagers/pkg/packagers/poetry"
2828
poetryfakes "github.com/paketo-buildpacks/python-packagers/pkg/packagers/poetry/fakes"
29+
uvinstall "github.com/paketo-buildpacks/python-packagers/pkg/packagers/uv"
30+
uvfakes "github.com/paketo-buildpacks/python-packagers/pkg/packagers/uv/fakes"
2931

3032
"github.com/sclevine/spec"
3133

@@ -65,6 +67,9 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
6567
poetryInstallProcess *poetryfakes.InstallProcess
6668
poetryPythonPathProcess *poetryfakes.PythonPathLookupProcess
6769

70+
// uv
71+
uvRunner *uvfakes.Runner
72+
6873
buildParameters pkgcommon.CommonBuildParameters
6974

7075
plans []packit.BuildpackPlan
@@ -105,6 +110,9 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
105110
poetryPythonPathProcess = &poetryfakes.PythonPathLookupProcess{}
106111
poetryPythonPathProcess.ExecuteCall.Returns.String = "some-python-path"
107112

113+
// uv
114+
uvRunner = &uvfakes.Runner{}
115+
108116
buildParameters = pkgcommon.CommonBuildParameters{
109117
SbomGenerator: pkgcommon.Generator{},
110118
Clock: chronos.DefaultClock,
@@ -129,6 +137,9 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
129137
InstallProcess: poetryInstallProcess,
130138
PythonPathLookupProcess: poetryPythonPathProcess,
131139
},
140+
uvinstall.UvEnvPlanEntry: uvinstall.UvBuildParameters{
141+
Runner: uvRunner,
142+
},
132143
}
133144

134145
build = pythonpackagers.Build(logger, buildParameters, packagerParameters)
@@ -162,6 +173,9 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
162173
{
163174
Name: poetryinstall.PoetryVenv,
164175
},
176+
{
177+
Name: uvinstall.UvEnvPlanEntry,
178+
},
165179
},
166180
},
167181
packit.BuildpackPlan{
@@ -194,6 +208,13 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
194208
},
195209
},
196210
},
211+
packit.BuildpackPlan{
212+
Entries: []packit.BuildpackPlanEntry{
213+
{
214+
Name: uvinstall.UvEnvPlanEntry,
215+
},
216+
},
217+
},
197218
}
198219
})
199220

detect.go

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
pipinstall "github.com/paketo-buildpacks/python-packagers/pkg/packagers/pip"
1414
pipenvinstall "github.com/paketo-buildpacks/python-packagers/pkg/packagers/pipenv"
1515
poetryinstall "github.com/paketo-buildpacks/python-packagers/pkg/packagers/poetry"
16+
uvinstall "github.com/paketo-buildpacks/python-packagers/pkg/packagers/uv"
1617
)
1718

1819
// Detect will return a packit.DetectFunc that will be invoked during the
@@ -22,64 +23,69 @@ import (
2223
// it will pass detection.
2324
func Detect(logger scribe.Emitter) packit.DetectFunc {
2425
return func(context packit.DetectContext) (packit.DetectResult, error) {
25-
plans := []packit.BuildPlan{}
26-
26+
logger.Title("Checking for pip")
2727
pipResult, err := pipinstall.Detect()(context)
2828

2929
if err == nil {
30-
plans = append(plans, pipResult.Plan)
30+
// plans = append(plans, pipResult.Plan)
31+
return packit.DetectResult{
32+
Plan: pipResult.Plan,
33+
}, nil
3134
} else {
3235
logger.Detail("%s", err)
3336
}
3437

38+
logger.Title("Checking for conda")
3539
condaResult, err := conda.Detect()(context)
3640

3741
if err == nil {
38-
plans = append(plans, condaResult.Plan)
42+
// plans = append(plans, condaResult.Plan)
43+
return packit.DetectResult{
44+
Plan: condaResult.Plan,
45+
}, nil
3946
} else {
4047
logger.Detail("%s", err)
4148
}
4249

50+
logger.Title("Checking for pipenv")
4351
pipenvResult, err := pipenvinstall.Detect(
4452
pipenvinstall.NewPipfileParser(),
4553
pipenvinstall.NewPipfileLockParser(),
4654
)(context)
4755

4856
if err == nil {
49-
plans = append(plans, pipenvResult.Plan)
57+
// plans = append(plans, pipenvResult.Plan)
58+
return packit.DetectResult{
59+
Plan: pipenvResult.Plan,
60+
}, nil
5061
} else {
5162
logger.Detail("%s", err)
5263
}
5364

54-
poetryResult, err := poetryinstall.Detect()(context)
65+
logger.Title("Checking for uv")
66+
uvResult, err := uvinstall.Detect()(context)
5567

5668
if err == nil {
57-
plans = append(plans, poetryResult.Plan)
69+
// plans = append(plans, uvResult.Plan)
70+
return packit.DetectResult{
71+
Plan: uvResult.Plan,
72+
}, nil
5873
} else {
5974
logger.Detail("%s", err)
6075
}
6176

62-
if len(plans) == 0 {
63-
return packit.DetectResult{}, packit.Fail.WithMessage("No python packager manager related files found")
64-
}
65-
66-
return packit.DetectResult{
67-
Plan: or(plans...),
68-
}, nil
69-
}
70-
}
71-
72-
func or(plans ...packit.BuildPlan) packit.BuildPlan {
73-
if len(plans) < 1 {
74-
return packit.BuildPlan{}
75-
}
76-
combinedPlan := plans[0]
77+
logger.Title("Checking for poetry")
78+
poetryResult, err := poetryinstall.Detect()(context)
7779

78-
for i := range plans {
79-
if i == 0 {
80-
continue
80+
if err == nil {
81+
// plans = append(plans, poetryResult.Plan)
82+
return packit.DetectResult{
83+
Plan: poetryResult.Plan,
84+
}, nil
85+
} else {
86+
logger.Detail("%s", err)
8187
}
82-
combinedPlan.Or = append(combinedPlan.Or, plans[i])
88+
89+
return packit.DetectResult{}, packit.Fail.WithMessage("No python packager manager related files found")
8390
}
84-
return combinedPlan
8591
}

detect_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
pip "github.com/paketo-buildpacks/python-packagers/pkg/packagers/pip"
1919
pipenv "github.com/paketo-buildpacks/python-packagers/pkg/packagers/pipenv"
2020
poetry "github.com/paketo-buildpacks/python-packagers/pkg/packagers/poetry"
21+
uv "github.com/paketo-buildpacks/python-packagers/pkg/packagers/uv"
2122

2223
"github.com/sclevine/spec"
2324

@@ -232,6 +233,65 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
232233
})
233234
})
234235

236+
context("When only a uv.lock file is present", func() {
237+
it.Before(func() {
238+
Expect(os.RemoveAll(filepath.Join(workingDir, "x.py"))).To(Succeed())
239+
Expect(os.WriteFile(filepath.Join(workingDir, "uv.lock"), []byte{}, os.ModePerm)).To(Succeed())
240+
})
241+
242+
it("passes detection", func() {
243+
result, err := detect(packit.DetectContext{
244+
WorkingDir: workingDir,
245+
})
246+
Expect(err).NotTo(HaveOccurred())
247+
Expect(result.Plan).To(Equal(packit.BuildPlan{
248+
Provides: []packit.BuildPlanProvision{
249+
{
250+
Name: uv.UvEnvPlanEntry,
251+
},
252+
},
253+
Requires: []packit.BuildPlanRequirement{
254+
{
255+
Name: uv.UvPlanEntry,
256+
Metadata: map[string]interface{}{
257+
"build": true,
258+
},
259+
},
260+
},
261+
}))
262+
})
263+
})
264+
265+
context("When a uv.lock and pyproject.toml file is present", func() {
266+
it.Before(func() {
267+
Expect(os.RemoveAll(filepath.Join(workingDir, "x.py"))).To(Succeed())
268+
Expect(os.WriteFile(filepath.Join(workingDir, "pyproject.toml"), []byte{}, os.ModePerm)).To(Succeed())
269+
Expect(os.WriteFile(filepath.Join(workingDir, "uv.lock"), []byte{}, os.ModePerm)).To(Succeed())
270+
})
271+
272+
it("passes detection", func() {
273+
result, err := detect(packit.DetectContext{
274+
WorkingDir: workingDir,
275+
})
276+
Expect(err).NotTo(HaveOccurred())
277+
Expect(result.Plan).To(Equal(packit.BuildPlan{
278+
Provides: []packit.BuildPlanProvision{
279+
{
280+
Name: uv.UvEnvPlanEntry,
281+
},
282+
},
283+
Requires: []packit.BuildPlanRequirement{
284+
{
285+
Name: uv.UvPlanEntry,
286+
Metadata: map[string]interface{}{
287+
"build": true,
288+
},
289+
},
290+
},
291+
}))
292+
})
293+
})
294+
235295
context("When no python related files are present", func() {
236296
it.Before(func() {
237297
Expect(os.RemoveAll(filepath.Join(workingDir, "x.py"))).To(Succeed())

integration.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44
"index.docker.io/paketobuildpacks/builder-jammy-buildpackless-base:latest",
55
"index.docker.io/paketobuildpacks/ubuntu-noble-builder-buildpackless:latest"
66
],
7-
"miniconda": "index.docker.io/paketobuildpacks/miniconda",
87
"cpython": "index.docker.io/paketobuildpacks/cpython",
9-
"pip": "index.docker.io/paketobuildpacks/pip",
10-
"pipenv": "index.docker.io/paketobuildpacks/pipenv",
11-
"poetry": "index.docker.io/paketobuildpacks/poetry",
8+
"python-installers": "github.com/idiap/python-installers",
129
"build-plan": "index.docker.io/paketocommunity/build-plan"
1310
}

integration/helpers.go

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,14 @@ type BuildpackInfo struct {
2727
type TestSettings struct {
2828
Buildpacks struct {
2929
// Dependency buildpacks
30-
Miniconda struct {
31-
Online string
32-
Offline string
33-
}
3430
CPython struct {
3531
Online string
3632
Offline string
3733
}
38-
Pip struct {
39-
Online string
40-
Offline string
41-
}
42-
Pipenv struct {
34+
PythonInstallers struct {
4335
Online string
4436
Offline string
4537
}
46-
Poetry struct {
47-
Online string
48-
}
4938
BuildPlan struct {
5039
Online string
5140
}
@@ -57,11 +46,8 @@ type TestSettings struct {
5746
}
5847

5948
Config struct {
60-
Miniconda string `json:"miniconda"`
61-
CPython string `json:"cpython"`
62-
Pip string `json:"pip"`
63-
Pipenv string `json:"pipenv"`
64-
Poetry string `json:"poetry"`
65-
BuildPlan string `json:"build-plan"`
49+
CPython string `json:"cpython"`
50+
PythonInstallers string `json:"python-installers"`
51+
BuildPlan string `json:"build-plan"`
6652
}
6753
}

integration/packagers/conda_default_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func condaTestDefault(t *testing.T, context spec.G, it spec.S) {
6262
image, logs, err = pack.WithNoColor().Build.
6363
WithPullPolicy("never").
6464
WithBuildpacks(
65-
settings.Buildpacks.Miniconda.Online,
65+
settings.Buildpacks.PythonInstallers.Online,
6666
settings.Buildpacks.PythonPackagers.Online,
6767
settings.Buildpacks.BuildPlan.Online,
6868
).
@@ -106,7 +106,7 @@ func condaTestDefault(t *testing.T, context spec.G, it spec.S) {
106106
image, logs, err = pack.WithNoColor().Build.
107107
WithPullPolicy("never").
108108
WithBuildpacks(
109-
settings.Buildpacks.Miniconda.Online,
109+
settings.Buildpacks.PythonInstallers.Online,
110110
settings.Buildpacks.PythonPackagers.Online,
111111
settings.Buildpacks.BuildPlan.Online,
112112
).

integration/packagers/conda_layer_reuse_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func condaTestLayerReuse(t *testing.T, context spec.G, it spec.S) {
7474
firstImage, logs, err = pack.WithNoColor().Build.
7575
WithPullPolicy("never").
7676
WithBuildpacks(
77-
settings.Buildpacks.Miniconda.Online,
77+
settings.Buildpacks.PythonInstallers.Online,
7878
settings.Buildpacks.PythonPackagers.Online,
7979
settings.Buildpacks.BuildPlan.Online,
8080
).
@@ -98,7 +98,7 @@ func condaTestLayerReuse(t *testing.T, context spec.G, it spec.S) {
9898
secondImage, logs, err = pack.WithNoColor().Build.
9999
WithPullPolicy("never").
100100
WithBuildpacks(
101-
settings.Buildpacks.Miniconda.Online,
101+
settings.Buildpacks.PythonInstallers.Online,
102102
settings.Buildpacks.PythonPackagers.Online,
103103
settings.Buildpacks.BuildPlan.Online,
104104
).
@@ -139,7 +139,7 @@ func condaTestLayerReuse(t *testing.T, context spec.G, it spec.S) {
139139
firstImage, logs, err = pack.WithNoColor().Build.
140140
WithPullPolicy("never").
141141
WithBuildpacks(
142-
settings.Buildpacks.Miniconda.Online,
142+
settings.Buildpacks.PythonInstallers.Online,
143143
settings.Buildpacks.PythonPackagers.Online,
144144
settings.Buildpacks.BuildPlan.Online,
145145
).
@@ -163,7 +163,7 @@ func condaTestLayerReuse(t *testing.T, context spec.G, it spec.S) {
163163
secondImage, logs, err = pack.WithNoColor().Build.
164164
WithPullPolicy("never").
165165
WithBuildpacks(
166-
settings.Buildpacks.Miniconda.Online,
166+
settings.Buildpacks.PythonInstallers.Online,
167167
settings.Buildpacks.PythonPackagers.Online,
168168
settings.Buildpacks.BuildPlan.Online,
169169
).

0 commit comments

Comments
 (0)