Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ go.work.sum
/build
.idea/
.DS_Store
tmp/
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ The buildpack is published for consumption at
This buildpack participates if one of the following detection succeeds:

- (miniconda)[installers/pkg/minconda/README.md] -> Always
- (pip)[installers/ppkg/pip/README.md] -> Always
- (pipenv)[installers/ppkg/pipenv/README.md] -> Always
- (poetry)[installers/ppkg/poetry/README.md] -> `pyproject.toml` is present in the root folder
- (pip)[installers/pkg/pip/README.md] -> Always
- (pipenv)[installers/pkg/pipenv/README.md] -> Always
- (poetry)[installers/pkg/poetry/README.md] -> `pyproject.toml` is present in the root folder
- (uv)[installers/pkg/uv/README.md] -> `uv.lock` is present in the root folder

The buildpack will do the following:
* At build time:
Expand Down
2 changes: 1 addition & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ SPDX-FileCopyrightText = "© 2025 Idiap Research Institute <contact@idiap.ch>"
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = "scripts/.util/tools.json"
path = "scripts/**"
precedence = "override"
SPDX-FileCopyrightText = "Copyright (c) 2013-Present CloudFoundry.org Foundation, Inc. All Rights Reserved."
SPDX-License-Identifier = "Apache-2.0"
Expand Down
9 changes: 9 additions & 0 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
pip "github.com/paketo-buildpacks/python-installers/pkg/installers/pip"
pipenv "github.com/paketo-buildpacks/python-installers/pkg/installers/pipenv"
poetry "github.com/paketo-buildpacks/python-installers/pkg/installers/poetry"
uv "github.com/paketo-buildpacks/python-installers/pkg/installers/uv"

pythoninstallers "github.com/paketo-buildpacks/python-installers/pkg/installers/common"
)
Expand Down Expand Up @@ -75,6 +76,14 @@ func Build(

return validateResult(result, err)

case uv.Uv:
result, err := uv.Build(
parameters.(uv.UvBuildParameters),
commonBuildParameters,
)(context)

return validateResult(result, err)

default:
return packit.BuildResult{}, packit.Fail.WithMessage("unknown plan: %s", entry.Name)
}
Expand Down
49 changes: 49 additions & 0 deletions build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
pipenvfakes "github.com/paketo-buildpacks/python-installers/pkg/installers/pipenv/fakes"
poetry "github.com/paketo-buildpacks/python-installers/pkg/installers/poetry"
poetryfakes "github.com/paketo-buildpacks/python-installers/pkg/installers/poetry/fakes"
uv "github.com/paketo-buildpacks/python-installers/pkg/installers/uv"
uvfakes "github.com/paketo-buildpacks/python-installers/pkg/installers/uv/fakes"

. "github.com/onsi/gomega"
"github.com/sclevine/spec"
Expand Down Expand Up @@ -77,6 +79,10 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
poetryProcess *poetryfakes.InstallProcess
poetrySitePackageProcess *poetryfakes.SitePackageProcess

// uv
uvDependencyManager *uvfakes.DependencyManager
uvInstallProcess *uvfakes.InstallProcess

buildParameters pkgcommon.CommonBuildParameters

testPlans []TestPlan
Expand Down Expand Up @@ -217,6 +223,34 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
poetrySitePackageProcess = &poetryfakes.SitePackageProcess{}
poetrySitePackageProcess.ExecuteCall.Returns.String = filepath.Join(layersDir, "poetry", "lib", "python3.8", "site-packages")

// uv
uvDependencyManager = &uvfakes.DependencyManager{}
uvDependencyManager.ResolveCall.Returns.Dependency = postal.Dependency{
ID: "uv",
Name: "uv-dependency-name",
Checksum: "uv-dependency-sha",
Stacks: []string{"some-stack"},
URI: "uv-dependency-uri",
Version: "uv-dependency-version",
}

// Legacy SBOM
uvDependencyManager.GenerateBillOfMaterialsCall.Returns.BOMEntrySlice = []packit.BOMEntry{
{
Name: "uv",
Metadata: paketosbom.BOMMetadata{
Checksum: paketosbom.BOMChecksum{
Algorithm: paketosbom.SHA256,
Hash: "uv-dependency-sha",
},
URI: "uv-dependency-uri",
Version: "uv-dependency-version",
},
},
}

uvInstallProcess = &uvfakes.InstallProcess{}

buildParameters = pkgcommon.CommonBuildParameters{
SbomGenerator: pkgcommon.Generator{},
Clock: chronos.DefaultClock,
Expand All @@ -243,6 +277,10 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
InstallProcess: poetryProcess,
SitePackageProcess: poetrySitePackageProcess,
},
uv.Uv: uv.UvBuildParameters{
DependencyManager: uvDependencyManager,
InstallProcess: uvInstallProcess,
},
}

build = pythoninstallers.Build(logger, buildParameters, packagerParameters)
Expand Down Expand Up @@ -302,9 +340,20 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
},
1,
},
{
packit.BuildpackPlan{
Entries: []packit.BuildpackPlanEntry{
{
Name: uv.Uv,
},
},
},
1,
},
}
Expect(os.WriteFile(filepath.Join(workingDir, "x.py"), []byte{}, os.ModePerm)).To(Succeed())
Expect(os.WriteFile(filepath.Join(workingDir, "pyproject.toml"), []byte(""), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(workingDir, "uv.lock"), []byte(`python-requires = "3.13.0"`), 0755)).To(Succeed())
})

it("runs the build process and returns expected layers", func() {
Expand Down
58 changes: 58 additions & 0 deletions buildpack.toml
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,64 @@ api = "0.8"
id = "poetry"
patches = 2

# uv
[[metadata.dependencies]]
arch = "amd64"
os = "linux"
cpe = "cpe:2.3:a:uv:uv:0.9.21:*:*:*:*:python:*:*"
id = "uv"
name = "uv"
uri = "https://github.com/astral-sh/uv/releases/download/0.9.21/uv-x86_64-unknown-linux-gnu.tar.gz"
sha256 = "0a1ab27383c28ef1c041f85cbbc609d8e3752dfb4b238d2ad97b208a52232baf"
source = "https://github.com/astral-sh/uv/releases/download/0.9.21/source.tar.gz"
sha256_source = "4b0a5505280424dbde3e1c55203c4106f802d5c031a899a9d87e2256bc783cf0"
stacks = ["*"]
version = "0.9.21"

[[metadata.dependencies]]
arch = "arm64"
os = "linux"
cpe = "cpe:2.3:a:uv:uv:0.9.21:*:*:*:*:python:*:*"
id = "uv"
name = "uv"
uri = "https://github.com/astral-sh/uv/releases/download/0.9.21/uv-aarch64-unknown-linux-gnu.tar.gz"
sha256 = "416984484783a357170c43f98e7d2d203f1fb595d6b3b95131513c53e50986ef"
source = "https://github.com/astral-sh/uv/releases/download/0.9.21/source.tar.gz"
sha256_source = "4b0a5505280424dbde3e1c55203c4106f802d5c031a899a9d87e2256bc783cf0"
stacks = ["*"]
version = "0.9.21"

[[metadata.dependencies]]
arch = "amd64"
os = "linux"
cpe = "cpe:2.3:a:uv:uv:0.9.22:*:*:*:*:python:*:*"
id = "uv"
name = "uv"
uri = "https://github.com/astral-sh/uv/releases/download/0.9.22/uv-x86_64-unknown-linux-gnu.tar.gz"
sha256 = "e170aed70ac0225feee612e855d3a57ae73c61ffb22c7e52c3fd33b87c286508"
source = "https://github.com/astral-sh/uv/releases/download/0.9.22/source.tar.gz"
sha256_source = "0c683fac23fc414505adce589adf642d710798b691321672de554443b6938c6e"
stacks = ["*"]
version = "0.9.22"

[[metadata.dependencies]]
arch = "arm64"
os = "linux"
cpe = "cpe:2.3:a:uv:uv:0.9.22:*:*:*:*:python:*:*"
id = "uv"
name = "uv"
uri = "https://github.com/astral-sh/uv/releases/download/0.9.22/uv-aarch64-unknown-linux-gnu.tar.gz"
sha256 = "2f8716c407d5da21b8a3e8609ed358147216aaab28b96b1d6d7f48e9bcc6254e"
source = "https://github.com/astral-sh/uv/releases/download/0.9.22/source.tar.gz"
sha256_source = "0c683fac23fc414505adce589adf642d710798b691321672de554443b6938c6e"
stacks = ["*"]
version = "0.9.22"

[[metadata.dependency-constraints]]
constraint = "*"
id = "python-uv"
patches = 2

[[stacks]]
id = "*"

Expand Down
2 changes: 2 additions & 0 deletions dependency/retrieval/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ replace github.com/ekzhu/minhash-lsh => github.com/ekzhu/minhash-lsh v0.0.0-2017

require (
github.com/Masterminds/semver/v3 v3.4.0
github.com/google/go-github/v81 v81.0.0
github.com/joshuatcasey/libdependency v0.22.0
github.com/nfx/go-htmltable v0.4.0
github.com/paketo-buildpacks/packit/v2 v2.25.0
Expand All @@ -29,6 +30,7 @@ require (
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.16.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/hhatto/gorst v0.0.0-20181029133204-ca9f730cac5b // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jdkato/prose v1.2.1 // indirect
Expand Down
6 changes: 6 additions & 0 deletions dependency/retrieval/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,13 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v81 v81.0.0 h1:hTLugQRxSLD1Yei18fk4A5eYjOGLUBKAl/VCqOfFkZc=
github.com/google/go-github/v81 v81.0.0/go.mod h1:upyjaybucIbBIuxgJS7YLOZGziyvvJ92WX6WEBNE3sM=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hhatto/gorst v0.0.0-20181029133204-ca9f730cac5b h1:Jdu2tbAxkRouSILp2EbposIb8h4gO+2QuZEn3d9sKAc=
Expand Down Expand Up @@ -266,6 +271,7 @@ golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM=
gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o=
Expand Down
108 changes: 108 additions & 0 deletions dependency/retrieval/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package main

import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
Expand All @@ -23,6 +24,7 @@ import (
"time"

"github.com/Masterminds/semver/v3"
"github.com/google/go-github/v81/github"
"github.com/joshuatcasey/libdependency/buildpack_config"
"github.com/joshuatcasey/libdependency/retrieve"
"github.com/joshuatcasey/libdependency/upstream"
Expand Down Expand Up @@ -57,6 +59,10 @@ func getAllVersionsForInstaller(installer string) retrieve.GetAllVersionsFunc {
return getAllMinicondaVersions
}

if installer == "uv" {
return getAllUvVersions
}

return func() (versionology.VersionFetcherArray, error) {

var pypiMetadata PyPiProductMetadataRaw
Expand Down Expand Up @@ -343,6 +349,107 @@ func generateMinicondaMetadata(versionFetcher versionology.VersionFetcher) ([]ve
}}, nil
}

type UvRelease struct {
version *semver.Version
Arch string
SourceURL string
UploadTime time.Time
SourceSHA256 string
BinaryURL string
BinarySHA256 string
}

func (release UvRelease) Version() *semver.Version {
return release.version
}

func getAllUvVersions() (versionology.VersionFetcherArray, error) {
client := github.NewClient(nil)

opt := &github.ListOptions{Page: 1, PerPage: 2}
releases, _, err := client.Repositories.ListReleases(context.Background(), "astral-sh", "uv", opt)

if err != nil {
return nil, err
}

var result versionology.VersionFetcherArray

for _, release := range releases {
version, err := semver.NewVersion(*release.Name)
if err != nil {
return nil, err
}

var sourceURL *string
var sourceSHA256 *string
for _, asset := range release.Assets {
if *asset.Name == "source.tar.gz" {
sourceURL = asset.BrowserDownloadURL
sourceSHA256 = asset.Digest
break
}
}
if sourceURL == nil || sourceSHA256 == nil {
return nil, errors.New("Failed to find source asset")
}

archAsset := "uv-%s-unknown-linux-gnu.tar.gz"

for inArch, outArch := range ArchMap {
assetName := fmt.Sprintf(archAsset, inArch)
for _, asset := range release.Assets {
if *asset.Name == assetName {
result = append(result,
UvRelease{
version: version,
Arch: outArch,
BinaryURL: *asset.BrowserDownloadURL,
BinarySHA256: *asset.Digest,
SourceURL: *sourceURL,
SourceSHA256: *sourceSHA256,
UploadTime: *asset.UpdatedAt.GetTime(),
})
break
}
}
}
}

return result, nil
}

func generateUvMetadata(versionFetcher versionology.VersionFetcher) ([]versionology.Dependency, error) {
version := versionFetcher.Version().String()
uvRelease, ok := versionFetcher.(UvRelease)
if !ok {
return nil, errors.New("expected a UvRelease")
}

var licenseIDsAsInterface []interface{}
licenseIDsAsInterface = append(licenseIDsAsInterface, "Apache-2.0", "MIT")
configMetadataDependency := cargo.ConfigMetadataDependency{
CPE: fmt.Sprintf("cpe:2.3:a:uv:uv:%s:*:*:*:*:python:*:*", version),
Checksum: uvRelease.BinarySHA256,
ID: "uv",
Licenses: licenseIDsAsInterface,
Name: "uv",
OS: "linux",
Arch: uvRelease.Arch,
PURL: retrieve.GeneratePURL("uv", version, uvRelease.SourceSHA256, uvRelease.SourceURL),
Source: uvRelease.SourceURL,
SourceChecksum: uvRelease.SourceSHA256,
Stacks: []string{"*"},
URI: uvRelease.BinaryURL,
Version: version,
}

return []versionology.Dependency{{
ConfigMetadataDependency: configMetadataDependency,
SemverVersion: versionFetcher.Version(),
}}, nil
}

// Taken from libdependency.retrieve.retrieval
// https://github.com/joshuatcasey/libdependency/blob/main/retrieve/retrieval.go
func toWorkflowJson(item any) (string, error) {
Expand Down Expand Up @@ -384,6 +491,7 @@ func main() {
"pipenv": generatePipenvMetadata,
"poetry": generatePoetryMetadata,
"miniconda3": generateMinicondaMetadata,
"uv": generateUvMetadata,
}

var dependencies []versionology.Dependency
Expand Down
Loading
Loading