Skip to content

Commit 43ad04b

Browse files
authored
feat: pixi support (#85)
feat: implement support for pixi Pixi is a drop-in replacement for conda See https://pixi.prefix.dev/
1 parent a35b333 commit 43ad04b

32 files changed

Lines changed: 1942 additions & 16 deletions

build.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
miniconda "github.com/paketo-buildpacks/python-installers/pkg/installers/miniconda"
1515
pip "github.com/paketo-buildpacks/python-installers/pkg/installers/pip"
1616
pipenv "github.com/paketo-buildpacks/python-installers/pkg/installers/pipenv"
17+
pixi "github.com/paketo-buildpacks/python-installers/pkg/installers/pixi"
1718
poetry "github.com/paketo-buildpacks/python-installers/pkg/installers/poetry"
1819
uv "github.com/paketo-buildpacks/python-installers/pkg/installers/uv"
1920

@@ -36,7 +37,14 @@ func Build(
3637

3738
var results []packit.BuildResult
3839

39-
orderedInstallers := []string{pip.Pip, pipenv.Pipenv, poetry.PoetryDependency, miniconda.Conda, uv.Uv}
40+
orderedInstallers := []string{
41+
pip.Pip,
42+
pipenv.Pipenv,
43+
poetry.PoetryDependency,
44+
miniconda.Conda,
45+
uv.Uv,
46+
pixi.Pixi,
47+
}
4048

4149
doneInstallers := []string{}
4250

@@ -110,6 +118,17 @@ func Build(
110118
}
111119
results = append(results, result)
112120

121+
case pixi.Pixi:
122+
result, err := pixi.Build(
123+
parameters.(pixi.PixiBuildParameters),
124+
commonBuildParameters,
125+
)(context)
126+
127+
if err != nil {
128+
return packit.BuildResult{}, err
129+
}
130+
results = append(results, result)
131+
113132
default:
114133
return packit.BuildResult{}, packit.Fail.WithMessage("unknown plan: %s", entry.Name)
115134
}

build_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import (
2929
pipfakes "github.com/paketo-buildpacks/python-installers/pkg/installers/pip/fakes"
3030
pipenv "github.com/paketo-buildpacks/python-installers/pkg/installers/pipenv"
3131
pipenvfakes "github.com/paketo-buildpacks/python-installers/pkg/installers/pipenv/fakes"
32+
pixi "github.com/paketo-buildpacks/python-installers/pkg/installers/pixi"
33+
pixifakes "github.com/paketo-buildpacks/python-installers/pkg/installers/pixi/fakes"
3234
poetry "github.com/paketo-buildpacks/python-installers/pkg/installers/poetry"
3335
poetryfakes "github.com/paketo-buildpacks/python-installers/pkg/installers/poetry/fakes"
3436
uv "github.com/paketo-buildpacks/python-installers/pkg/installers/uv"
@@ -83,6 +85,10 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
8385
uvDependencyManager *uvfakes.DependencyManager
8486
uvInstallProcess *uvfakes.InstallProcess
8587

88+
// pixi
89+
pixiDependencyManager *pixifakes.DependencyManager
90+
pixiInstallProcess *pixifakes.InstallProcess
91+
8692
buildParameters pkgcommon.CommonBuildParameters
8793

8894
testPlans []TestPlan
@@ -251,6 +257,34 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
251257

252258
uvInstallProcess = &uvfakes.InstallProcess{}
253259

260+
// pixi
261+
pixiDependencyManager = &pixifakes.DependencyManager{}
262+
pixiDependencyManager.ResolveCall.Returns.Dependency = postal.Dependency{
263+
ID: "pixi",
264+
Name: "pixi-dependency-name",
265+
Checksum: "pixi-dependency-sha",
266+
Stacks: []string{"some-stack"},
267+
URI: "pixi-dependency-uri",
268+
Version: "pixi-dependency-version",
269+
}
270+
271+
// Legacy SBOM
272+
pixiDependencyManager.GenerateBillOfMaterialsCall.Returns.BOMEntrySlice = []packit.BOMEntry{
273+
{
274+
Name: "pixi",
275+
Metadata: paketosbom.BOMMetadata{
276+
Checksum: paketosbom.BOMChecksum{
277+
Algorithm: paketosbom.SHA256,
278+
Hash: "pixi-dependency-sha",
279+
},
280+
URI: "pixi-dependency-uri",
281+
Version: "pixi-dependency-version",
282+
},
283+
},
284+
}
285+
286+
pixiInstallProcess = &pixifakes.InstallProcess{}
287+
254288
buildParameters = pkgcommon.CommonBuildParameters{
255289
SbomGenerator: pkgcommon.Generator{},
256290
Clock: chronos.DefaultClock,
@@ -272,6 +306,10 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
272306
InstallProcess: pipenvProcess,
273307
SitePackageProcess: pipenvSitePackageProcess,
274308
},
309+
pixi.Pixi: pixi.PixiBuildParameters{
310+
DependencyManager: pixiDependencyManager,
311+
InstallProcess: pixiInstallProcess,
312+
},
275313
poetry.PoetryDependency: poetry.PoetryBuildParameters{
276314
DependencyManager: poetryDependencyManager,
277315
InstallProcess: poetryProcess,
@@ -350,6 +388,16 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
350388
},
351389
1,
352390
},
391+
{
392+
packit.BuildpackPlan{
393+
Entries: []packit.BuildpackPlanEntry{
394+
{
395+
Name: pixi.Pixi,
396+
},
397+
},
398+
},
399+
1,
400+
},
353401
{
354402
packit.BuildpackPlan{
355403
Entries: []packit.BuildpackPlanEntry{
@@ -380,6 +428,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
380428
Expect(os.WriteFile(filepath.Join(workingDir, "x.py"), []byte{}, os.ModePerm)).To(Succeed())
381429
Expect(os.WriteFile(filepath.Join(workingDir, "pyproject.toml"), []byte(""), 0755)).To(Succeed())
382430
Expect(os.WriteFile(filepath.Join(workingDir, "uv.lock"), []byte(`python-requires = "3.13.0"`), 0755)).To(Succeed())
431+
Expect(os.WriteFile(filepath.Join(workingDir, "pixi.lock"), []byte(``), 0755)).To(Succeed())
383432
})
384433

385434
it("runs the build process and returns expected layers", func() {

buildpack.toml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,66 @@ api = "0.8"
363363
uri = "https://github.com/astral-sh/uv/releases/download/0.10.4/uv-aarch64-unknown-linux-gnu.tar.gz"
364364
version = "0.10.4"
365365

366+
[[metadata.dependencies]]
367+
"arch" = "amd64"
368+
"checksum" = "sha256:9333a89567e8235881b9357b35585f4b4da6255e100235559dbb4702f3c6d97c"
369+
"cpe" = "cpe:2.3:a:pixi:pixi:0.62.2:*:*:*:*:python:*:*"
370+
"purl" = "pkg:generic/pixi@0.62.2?checksum=sha256:0ed1f3796dc5cb6ba9ce4f3b1250b1ef8bf57202ce869bec68563861a8c627c2&download_url=https://github.com/prefix-dev/pixi/releases/download/v0.62.2/source.tar.gz"
371+
"id" = "pixi"
372+
"licenses" = ["BSD-3-Clause"]
373+
"name" = "pixi"
374+
"os" = "linux"
375+
"source" = "https://github.com/prefix-dev/pixi/releases/download/v0.62.2/source.tar.gz"
376+
"source-checksum" = "sha256:0ed1f3796dc5cb6ba9ce4f3b1250b1ef8bf57202ce869bec68563861a8c627c2"
377+
"stacks" = ["*"]
378+
"uri" = "https://github.com/prefix-dev/pixi/releases/download/v0.62.2/pixi-x86_64-unknown-linux-musl.tar.gz"
379+
"version" = "0.62.2"
380+
381+
[[metadata.dependencies]]
382+
"arch" = "arm64"
383+
"checksum" = "sha256:a21b55d9fd37ba9c8cbee335bff952e5e7022729dd6ff5b127aa2d7fad065b4d"
384+
"cpe" = "cpe:2.3:a:pixi:pixi:0.62.2:*:*:*:*:python:*:*"
385+
"purl" = "pkg:generic/pixi@0.62.2?checksum=sha256:0ed1f3796dc5cb6ba9ce4f3b1250b1ef8bf57202ce869bec68563861a8c627c2&download_url=https://github.com/prefix-dev/pixi/releases/download/v0.62.2/source.tar.gz"
386+
"id" = "pixi"
387+
"licenses" = ["BSD-3-Clause"]
388+
"name" = "pixi"
389+
"os" = "linux"
390+
"source" = "https://github.com/prefix-dev/pixi/releases/download/v0.62.2/source.tar.gz"
391+
"source-checksum" = "sha256:0ed1f3796dc5cb6ba9ce4f3b1250b1ef8bf57202ce869bec68563861a8c627c2"
392+
"stacks" = ["*"]
393+
"uri" = "https://github.com/prefix-dev/pixi/releases/download/v0.62.2/pixi-aarch64-unknown-linux-musl.tar.gz"
394+
"version" = "0.62.2"
395+
396+
[[metadata.dependencies]]
397+
"arch" = "amd64"
398+
"checksum" = "sha256:b2a9e26bb6c80fe00618a02e7198dec222e1fbcec61e04c11b6e6538089ab100"
399+
"cpe" = "cpe:2.3:a:pixi:pixi:0.63.2:*:*:*:*:python:*:*"
400+
"purl" = "pkg:generic/pixi@0.63.2?checksum=sha256:d63846f1ed0164a4c2f8d7f25263111e9c557604a4195f4bf4b351567ae4fb8f&download_url=https://github.com/prefix-dev/pixi/releases/download/v0.63.2/source.tar.gz"
401+
"id" = "pixi"
402+
"licenses" = ["BSD-3-Clause"]
403+
"name" = "pixi"
404+
"os" = "linux"
405+
"source" = "https://github.com/prefix-dev/pixi/releases/download/v0.63.2/source.tar.gz"
406+
"source-checksum" = "sha256:d63846f1ed0164a4c2f8d7f25263111e9c557604a4195f4bf4b351567ae4fb8f"
407+
"stacks" = ["*"]
408+
"uri" = "https://github.com/prefix-dev/pixi/releases/download/v0.63.2/pixi-x86_64-unknown-linux-musl.tar.gz"
409+
"version" = "0.63.2"
410+
411+
[[metadata.dependencies]]
412+
"arch" = "arm64"
413+
"checksum" = "sha256:dbde6dbc2806602171e17305ce005e1aed519f2f2461a7cafd0093e92b7e7681"
414+
"cpe" = "cpe:2.3:a:pixi:pixi:0.63.2:*:*:*:*:python:*:*"
415+
"purl" = "pkg:generic/pixi@0.63.2?checksum=sha256:d63846f1ed0164a4c2f8d7f25263111e9c557604a4195f4bf4b351567ae4fb8f&download_url=https://github.com/prefix-dev/pixi/releases/download/v0.63.2/source.tar.gz"
416+
"id" = "pixi"
417+
"licenses" = ["BSD-3-Clause"]
418+
"name" = "pixi"
419+
"os" = "linux"
420+
"source" = "https://github.com/prefix-dev/pixi/releases/download/v0.63.2/source.tar.gz"
421+
"source-checksum" = "sha256:d63846f1ed0164a4c2f8d7f25263111e9c557604a4195f4bf4b351567ae4fb8f"
422+
"stacks" = ["*"]
423+
"uri" = "https://github.com/prefix-dev/pixi/releases/download/v0.63.2/pixi-aarch64-unknown-linux-musl.tar.gz"
424+
"version" = "0.63.2"
425+
366426
[[metadata.dependency-constraints]]
367427
constraint = "*"
368428
id = "miniconda3"
@@ -413,6 +473,11 @@ api = "0.8"
413473
id = "uv"
414474
patches = 2
415475

476+
[[metadata.dependency-constraints]]
477+
constraint = "*"
478+
id = "pixi"
479+
patches = 2
480+
416481
[[stacks]]
417482
id = "*"
418483

dependency/retrieval/main.go

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func (release PyPiRelease) Version() *semver.Version {
5555
}
5656

5757
func getAllVersionsForInstaller(installer string) retrieve.GetAllVersionsFunc {
58+
fmt.Printf("Handling: %s\n", installer)
5859
if installer == "miniconda3" {
5960
return getAllMinicondaVersions
6061
}
@@ -63,6 +64,10 @@ func getAllVersionsForInstaller(installer string) retrieve.GetAllVersionsFunc {
6364
return getAllUvVersions
6465
}
6566

67+
if installer == "pixi" {
68+
return getAllPixiVersions
69+
}
70+
6671
return func() (versionology.VersionFetcherArray, error) {
6772

6873
var pypiMetadata PyPiProductMetadataRaw
@@ -349,7 +354,7 @@ func generateMinicondaMetadata(versionFetcher versionology.VersionFetcher) ([]ve
349354
}}, nil
350355
}
351356

352-
type UvRelease struct {
357+
type GitHubRelease struct {
353358
version *semver.Version
354359
Arch string
355360
SourceURL string
@@ -359,15 +364,15 @@ type UvRelease struct {
359364
BinarySHA256 string
360365
}
361366

362-
func (release UvRelease) Version() *semver.Version {
367+
func (release GitHubRelease) Version() *semver.Version {
363368
return release.version
364369
}
365370

366-
func getAllUvVersions() (versionology.VersionFetcherArray, error) {
371+
func getGitHubVersions(org string, project string, archAsset string) (versionology.VersionFetcherArray, error) {
367372
client := github.NewClient(nil)
368373

369-
opt := &github.ListOptions{Page: 1, PerPage: 2}
370-
releases, _, err := client.Repositories.ListReleases(context.Background(), "astral-sh", "uv", opt)
374+
opt := &github.ListOptions{Page: 1, PerPage: 4}
375+
releases, _, err := client.Repositories.ListReleases(context.Background(), org, project, opt)
371376

372377
if err != nil {
373378
return nil, err
@@ -376,7 +381,7 @@ func getAllUvVersions() (versionology.VersionFetcherArray, error) {
376381
var result versionology.VersionFetcherArray
377382

378383
for _, release := range releases {
379-
version, err := semver.NewVersion(*release.Name)
384+
version, err := semver.NewVersion(*release.TagName)
380385
if err != nil {
381386
return nil, err
382387
}
@@ -394,14 +399,12 @@ func getAllUvVersions() (versionology.VersionFetcherArray, error) {
394399
return nil, errors.New("Failed to find source asset")
395400
}
396401

397-
archAsset := "uv-%s-unknown-linux-gnu.tar.gz"
398-
399402
for inArch, outArch := range ArchMap {
400403
assetName := fmt.Sprintf(archAsset, inArch)
401404
for _, asset := range release.Assets {
402405
if *asset.Name == assetName {
403406
result = append(result,
404-
UvRelease{
407+
GitHubRelease{
405408
version: version,
406409
Arch: outArch,
407410
BinaryURL: *asset.BrowserDownloadURL,
@@ -419,11 +422,15 @@ func getAllUvVersions() (versionology.VersionFetcherArray, error) {
419422
return result, nil
420423
}
421424

425+
func getAllUvVersions() (versionology.VersionFetcherArray, error) {
426+
return getGitHubVersions("astral-sh", "uv", "uv-%s-unknown-linux-gnu.tar.gz")
427+
}
428+
422429
func generateUvMetadata(versionFetcher versionology.VersionFetcher) ([]versionology.Dependency, error) {
423430
version := versionFetcher.Version().String()
424-
uvRelease, ok := versionFetcher.(UvRelease)
431+
uvRelease, ok := versionFetcher.(GitHubRelease)
425432
if !ok {
426-
return nil, errors.New("expected a UvRelease")
433+
return nil, errors.New("expected a GitHubRelease")
427434
}
428435

429436
var licenseIDsAsInterface []interface{}
@@ -450,6 +457,43 @@ func generateUvMetadata(versionFetcher versionology.VersionFetcher) ([]versionol
450457
}}, nil
451458
}
452459

460+
func getAllPixiVersions() (versionology.VersionFetcherArray, error) {
461+
return getGitHubVersions("prefix-dev", "pixi", "pixi-%s-unknown-linux-musl.tar.gz")
462+
}
463+
464+
func generatePixiMetadata(versionFetcher versionology.VersionFetcher) ([]versionology.Dependency, error) {
465+
version := versionFetcher.Version().String()
466+
pixiRelease, ok := versionFetcher.(GitHubRelease)
467+
if !ok {
468+
return nil, errors.New("expected a GitHubRelease")
469+
}
470+
471+
fmt.Printf("version: %s\n", version)
472+
473+
var licenseIDsAsInterface []interface{}
474+
licenseIDsAsInterface = append(licenseIDsAsInterface, "BSD-3-Clause")
475+
configMetadataDependency := cargo.ConfigMetadataDependency{
476+
CPE: fmt.Sprintf("cpe:2.3:a:pixi:pixi:%s:*:*:*:*:python:*:*", version),
477+
Checksum: pixiRelease.BinarySHA256,
478+
ID: "pixi",
479+
Licenses: licenseIDsAsInterface,
480+
Name: "pixi",
481+
OS: "linux",
482+
Arch: pixiRelease.Arch,
483+
PURL: retrieve.GeneratePURL("pixi", version, pixiRelease.SourceSHA256, pixiRelease.SourceURL),
484+
Source: pixiRelease.SourceURL,
485+
SourceChecksum: pixiRelease.SourceSHA256,
486+
Stacks: []string{"*"},
487+
URI: pixiRelease.BinaryURL,
488+
Version: version,
489+
}
490+
491+
return []versionology.Dependency{{
492+
ConfigMetadataDependency: configMetadataDependency,
493+
SemverVersion: versionFetcher.Version(),
494+
}}, nil
495+
}
496+
453497
// Taken from libdependency.retrieve.retrieval
454498
// https://github.com/joshuatcasey/libdependency/blob/main/retrieve/retrieval.go
455499
func toWorkflowJson(item any) (string, error) {
@@ -492,6 +536,7 @@ func main() {
492536
"poetry": generatePoetryMetadata,
493537
"miniconda3": generateMinicondaMetadata,
494538
"uv": generateUvMetadata,
539+
"pixi": generatePixiMetadata,
495540
}
496541

497542
var dependencies []versionology.Dependency

detect.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
miniconda "github.com/paketo-buildpacks/python-installers/pkg/installers/miniconda"
1313
pip "github.com/paketo-buildpacks/python-installers/pkg/installers/pip"
1414
pipenv "github.com/paketo-buildpacks/python-installers/pkg/installers/pipenv"
15+
pixi "github.com/paketo-buildpacks/python-installers/pkg/installers/pixi"
1516
poetry "github.com/paketo-buildpacks/python-installers/pkg/installers/poetry"
1617
uv "github.com/paketo-buildpacks/python-installers/pkg/installers/uv"
1718
)
@@ -65,6 +66,14 @@ func Detect(logger scribe.Emitter, pyProjectParser poetry.PyProjectParser) packi
6566
logger.Detail("%s", err)
6667
}
6768

69+
pixiResult, err := pixi.Detect()(context)
70+
71+
if err == nil {
72+
plans = append(plans, pixiResult.Plan)
73+
} else {
74+
logger.Detail("%s", err)
75+
}
76+
6877
if len(plans) == 0 {
6978
return packit.DetectResult{}, packit.Fail.WithMessage("No python packager manager related files found")
7079
}

0 commit comments

Comments
 (0)