diff --git a/features/src/python/README.md b/features/src/python/README.md index 4e8b0c3..074c69e 100755 --- a/features/src/python/README.md +++ b/features/src/python/README.md @@ -11,7 +11,11 @@ Installs Python. "downloadUrl": "", "pipIndex": "", "pipIndexUrl": "", - "pipTrustedHost": "" + "pipTrustedHost": "", + "uvVersion": "none", + "uvDownloadUrl": "", + "uvIndexUrl": "", + "uvExtraIndexUrl": "" } } ``` @@ -25,6 +29,10 @@ Installs Python. | pipIndex | The pip index to use (used by search). | string | <empty> | https://mycompany.com/artifactory/api/pypi/python/simple, https://mycompany.com/nexus/repository/pypi-group/pypi | | pipIndexUrl | The pip index URL to use (used by install). | string | <empty> | https://mycompany.com/artifactory/api/pypi/python/simple, https://mycompany.com/nexus/repository/pypi-group/simple | | pipTrustedHost | The pip trusted host to use. | string | <empty> | mycompany.com, artifactory.mycompany.com, nexus.mycompany.com | +| uvVersion | The version of uv to install. | string | none | none, latest, 0.8.14 | +| uvDownloadUrl | The download URL to use for uv. | string | <empty> | https://mycompany.com/artifactory/uv-generic-remote | +| uvIndexUrl | The uv index URL to use. | string | <empty> | https://mycompany.com/artifactory/api/pypi/python/simple | +| uvExtraIndexUrl | The additional uv index URL to use. | string | <empty> | https://mycompany.com/artifactory/api/pypi/python-extra/simple | ## Customizations diff --git a/features/src/python/devcontainer-feature.json b/features/src/python/devcontainer-feature.json index 6c7950c..2c37844 100644 --- a/features/src/python/devcontainer-feature.json +++ b/features/src/python/devcontainer-feature.json @@ -51,6 +51,40 @@ "nexus.mycompany.com" ], "description": "The pip trusted host to use." + }, + "uvVersion": { + "type": "string", + "proposals": [ + "none", + "latest", + "0.8.14" + ], + "default": "none", + "description": "The version of uv to install." + }, + "uvDownloadUrl": { + "type": "string", + "default": "", + "proposals": [ + "https://mycompany.com/artifactory/uv-generic-remote" + ], + "description": "The download URL to use for uv." + }, + "uvIndexUrl": { + "type": "string", + "default": "", + "proposals": [ + "https://mycompany.com/artifactory/api/pypi/python/simple" + ], + "description": "The uv index URL to use." + }, + "uvExtraIndexUrl": { + "type": "string", + "default": "", + "proposals": [ + "https://mycompany.com/artifactory/api/pypi/python-extra/simple" + ], + "description": "The additional uv index URL to use." } }, "customizations": { diff --git a/features/src/python/install.sh b/features/src/python/install.sh index 0ac43b8..7e8a29d 100755 --- a/features/src/python/install.sh +++ b/features/src/python/install.sh @@ -5,4 +5,8 @@ -downloadUrl="${DOWNLOADURL:-""}" \ -pipIndex="${PIP_INDEX:-""}" \ -pipIndexUrl="${PIP_INDEX_URL:-""}" \ - -pipTrustedHost="${PIP_TRUSTED_HOST:-""}" + -pipTrustedHost="${PIP_TRUSTED_HOST:-""}" \ + -uvVersion="${UV_VERSION:-"none"}" \ + -uvDownloadUrl="${UV_DOWNLOAD_URL:-""}" \ + -uvIndexUrl="${UV_INDEX_URL:-""}" \ + -uvExtraIndexUrl="${UV_EXTRA_INDEX_URL:-""}" diff --git a/features/src/python/installer.go b/features/src/python/installer.go index 8540944..59af74d 100644 --- a/features/src/python/installer.go +++ b/features/src/python/installer.go @@ -18,6 +18,7 @@ import ( ////////// var pythonVersionRegexp *regexp.Regexp = regexp.MustCompile(`^v(?P(\d+)\.(\d+)(?:\.(\d+))?(?:([a-z]+)(\d+))?)$`) +var uvVersionRegexp *regexp.Regexp = regexp.MustCompile(`^v(?P\d+\.\d+\.\d+)$`) ////////// // Main @@ -37,6 +38,10 @@ func runMain() error { pipIndex := flag.String("pipIndex", "", "") pipIndexUrl := flag.String("pipIndexUrl", "", "") pipTrustedHost := flag.String("pipTrustedHost", "", "") + uvVersion := flag.String("uvVersion", "none", "") + uvDownloadUrl := flag.String("uvDownloadUrl", "", "") + uvIndexUrl := flag.String("uvIndexUrl", "", "") + uvExtraIndexUrl := flag.String("uvExtraIndexUrl", "", "") flag.Parse() // Load settings from an external file @@ -48,6 +53,9 @@ func runMain() error { installer.HandleOverride(pipIndex, "", "python-pip-index") installer.HandleOverride(pipIndexUrl, "", "python-pip-index-url") installer.HandleOverride(pipTrustedHost, "", "python-pip-trusted-host") + installer.HandleOverride(uvDownloadUrl, "https://github.com/astral-sh/uv/releases/download", "python-uv-download-url") + installer.HandleOverride(uvIndexUrl, "", "python-uv-index-url") + installer.HandleOverride(uvExtraIndexUrl, "", "python-uv-extra-index-url") // Create and process the feature feature := installer.NewFeature("Python", false, @@ -58,10 +66,89 @@ func runMain() error { PipIndexUrl: *pipIndexUrl, PipTrustedHost: *pipTrustedHost, }, + &uvComponent{ + ComponentBase: installer.NewComponentBase("uv", *uvVersion), + DownloadUrl: *uvDownloadUrl, + IndexUrl: *uvIndexUrl, + ExtraIndexUrl: *uvExtraIndexUrl, + }, ) return feature.Process() } +type uvComponent struct { + *installer.ComponentBase + DownloadUrl string + IndexUrl string + ExtraIndexUrl string +} + +func (c *uvComponent) GetAllVersions() ([]*gover.Version, error) { + allTags, err := installer.Tools.GitHub.GetTags("astral-sh", "uv") + if err != nil { + return nil, err + } + return installer.Tools.Versioning.ParseVersionsFromList(allTags, uvVersionRegexp, true) +} + +func (c *uvComponent) InstallVersion(version *gover.Version) error { + archPart, err := installer.Tools.System.MapArchitecture(map[string]string{ + installer.AMD64: "x86_64", + installer.ARM64: "aarch64", + }) + if err != nil { + return err + } + target := "unknown-linux-gnu" + if installer.Tools.System.OsInfo().IsAlpine() { + target = "unknown-linux-musl" + } + asset := fmt.Sprintf("uv-%s-%s.tar.gz", archPart, target) + versionPart := fmt.Sprintf("v%s", version.Raw) + downloadUrl, err := installer.Tools.Http.BuildUrl(c.DownloadUrl, versionPart, asset) + if err != nil { + return err + } + tempDir, err := os.MkdirTemp("", "uv-download-*") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + archivePath := filepath.Join(tempDir, asset) + if err := installer.Tools.Download.ToFile(downloadUrl, archivePath, "uv"); err != nil { + return err + } + extractDir := filepath.Join(tempDir, "extract") + if err := installer.Tools.Compression.ExtractTarGz(archivePath, extractDir, true); err != nil { + return err + } + uvPath := filepath.Join(extractDir, fmt.Sprintf("uv-%s-%s", archPart, target), "uv") + if _, err := os.Stat(uvPath); err != nil { + uvPath = filepath.Join(extractDir, "uv") + } + if err := installer.Tools.System.InstallBinaryToUsrLocalBin(uvPath, "uv"); err != nil { + return err + } + return c.configure() +} + +func (c *uvComponent) configure() error { + if c.IndexUrl == "" && c.ExtraIndexUrl == "" { + return nil + } + if err := os.MkdirAll("/etc/uv", 0755); err != nil { + return err + } + config := "" + if c.IndexUrl != "" { + config += fmt.Sprintf("index-url = %q\n", c.IndexUrl) + } + if c.ExtraIndexUrl != "" { + config += fmt.Sprintf("extra-index-url = [%q]\n", c.ExtraIndexUrl) + } + return os.WriteFile("/etc/uv/uv.toml", []byte(config), 0644) +} + ////////// // Implementation ////////// diff --git a/features/test/python/install-uv.sh b/features/test/python/install-uv.sh new file mode 100755 index 0000000..729899b --- /dev/null +++ b/features/test/python/install-uv.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_command_exists uv +grep -q 'index-url = "https://pypi.org/simple"' /etc/uv/uv.toml +grep -q 'extra-index-url = \["https://example.com/simple"\]' /etc/uv/uv.toml diff --git a/features/test/python/scenarios.json b/features/test/python/scenarios.json index aa3add4..ddbc424 100644 --- a/features/test/python/scenarios.json +++ b/features/test/python/scenarios.json @@ -28,5 +28,18 @@ "version": "system-default" } } + }, + "install-uv": { + "build": { + "dockerfile": "Dockerfile" + }, + "features": { + "./python": { + "version": "system-default", + "uvVersion": "latest", + "uvIndexUrl": "https://pypi.org/simple", + "uvExtraIndexUrl": "https://example.com/simple" + } + } } } diff --git a/override-all.env b/override-all.env index fcaee5e..28944cb 100644 --- a/override-all.env +++ b/override-all.env @@ -93,6 +93,9 @@ PYTHON_DOWNLOAD_URL="" PYTHON_PIP_INDEX="" PYTHON_PIP_INDEX_URL="" PYTHON_PIP_TRUSTED_HOST="" +PYTHON_UV_DOWNLOAD_URL="" +PYTHON_UV_INDEX_URL="" +PYTHON_UV_EXTRA_INDEX_URL="" # terraform TERRAFORM_DOWNLOAD_URL=""