From a78256f051299f41927bef07219aa0614dc30dae Mon Sep 17 00:00:00 2001 From: Bas Steins <592313+sebst@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:00:52 +0000 Subject: [PATCH 1/2] Revert "feat: Add `uv` feature (astral.sh/uv) (#62)" This reverts commit 2f5a12d17e5808dd2827f231df7ec6d3a2f598fd. --- src/uv/devcontainer-feature.json | 22 ---- src/uv/install.sh | 167 ------------------------------- 2 files changed, 189 deletions(-) delete mode 100644 src/uv/devcontainer-feature.json delete mode 100755 src/uv/install.sh diff --git a/src/uv/devcontainer-feature.json b/src/uv/devcontainer-feature.json deleted file mode 100644 index ec725fe..0000000 --- a/src/uv/devcontainer-feature.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "uv", - "id": "uv", - "version": "1.0.0", - "description": "Install \"uv\" and \"uvx\" binaries", - "documentationURL": "https://github.com/devcontainer-community/devcontainer-features/tree/main/src/uv", - "options": { - "version": { - "type": "string", - "default": "latest", - "proposals": [ - "latest" - ], - "description": "Version of \"uv\" to install." - }, - "shellautocompletion": { - "default": false, - "description": "Enable or disable uv and uvx autocompletion.", - "type": "boolean" - } - } -} \ No newline at end of file diff --git a/src/uv/install.sh b/src/uv/install.sh deleted file mode 100755 index 9b5eb09..0000000 --- a/src/uv/install.sh +++ /dev/null @@ -1,167 +0,0 @@ -#!/bin/bash -set -o errexit -set -o pipefail -set -o noclobber -set -o nounset -set -o allexport -readonly githubRepository='astral-sh/uv' -readonly binaryName='uv' -readonly versionArgument='--version' -readonly os="unknown-linux-musl" -readonly downloadUrlTemplate='https://github.com/${githubRepository}/releases/download/${version}/${binaryName}-${architecture}-${os}.tar.gz' -readonly downloadUrlLatestTemplate='https://github.com/${githubRepository}/releases/latest/download/${binaryName}-${architecture}-${os}.tar.gz' -readonly binaryTargetFolder='/usr/local/bin' -readonly name="${githubRepository##*/}" -readonly AUTOCOMPLETION="${SHELLAUTOCOMPLETION:-"true"}" -apt_get_update() { - if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then - echo "Running apt-get update..." - apt-get update -y - fi -} -apt_get_checkinstall() { - if ! dpkg -s "$@" >/dev/null 2>&1; then - apt_get_update - DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends --no-install-suggests --option 'Debug::pkgProblemResolver=true' --option 'Debug::pkgAcquire::Worker=1' "$@" - fi -} -apt_get_cleanup() { - apt-get clean - rm -rf /var/lib/apt/lists/* -} -check_curl_envsubst_file_untar_installed() { - declare -a requiredAptPackagesMissing=() - if ! [ -r '/etc/ssl/certs/ca-certificates.crt' ]; then - requiredAptPackagesMissing+=('ca-certificates') - fi - if ! command -v curl >/dev/null 2>&1; then - requiredAptPackagesMissing+=('curl') - fi - if ! command -v envsubst >/dev/null 2>&1; then - requiredAptPackagesMissing+=('gettext-base') - fi - if ! command -v file >/dev/null 2>&1; then - requiredAptPackagesMissing+=('file') - fi - if ! command -v tar >/dev/null 2>&1; then - requiredAptPackagesMissing+=('tar') - fi - declare -i requiredAptPackagesMissingCount=${#requiredAptPackagesMissing[@]} - if [ $requiredAptPackagesMissingCount -gt 0 ]; then - apt_get_update - apt_get_checkinstall "${requiredAptPackagesMissing[@]}" - apt_get_cleanup - fi -} -curl_check_url() { - local url=$1 - local status_code - status_code=$(curl -s -o /dev/null -w '%{http_code}' "$url") - if [ "$status_code" -ne 200 ] && [ "$status_code" -ne 302 ]; then - echo "Failed to download '$url'. Status code: $status_code." - return 1 - fi -} -curl_download_stdout() { - local url=$1 - curl \ - --silent \ - --location \ - --output '-' \ - --connect-timeout 5 \ - "$url" -} -curl_download_untar() { - local url=$1 - local strip=$2 - local target=$3 - shift 3 - # Remaining arguments are one or more paths within the archive to extract - curl_download_stdout "$url" | tar \ - -xz \ - -f '-' \ - --strip-components="$strip" \ - -C "$target" \ - "$@" -} -debian_get_arch() { - arch=$(uname -m) - if [[ "$arch" == "aarch64" ]]; then - arch="aarch64" - elif [[ "$arch" == "x86_64" ]]; then - arch="x86_64" - fi - echo "$arch" -# echo "$(dpkg --print-architecture)" --- IGNORE --- -} -echo_banner() { - local text="$1" - echo -e "\e[1m\e[97m\e[41m$text\e[0m" -} -github_list_releases() { - if [ -z "$1" ]; then - echo "Usage: list_github_releases " - return 1 - fi - local repo="$1" - local url="https://api.github.com/repos/$repo/releases" - curl -s "$url" | grep -Po '"tag_name": "\K.*?(?=")' | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^v//' -} -github_get_latest_release() { - if [ -z "$1" ]; then - echo "Usage: get_latest_github_release " - return 1 - fi - github_list_releases "$1" | head -n 1 -} -utils_check_version() { - local version=$1 - if ! [[ "${version:-}" =~ ^(latest|[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then - printf >&2 '=== [ERROR] Option "version" (value: "%s") is not "latest" or valid semantic version format "X.Y.Z" !\n' \ - "$version" - exit 1 - fi -} -enable_autocompletion() { - command=$1 - ${command} bash >> /usr/share/bash-completion/completions/uv - ${command} zsh >> /usr/share/zsh/vendor-completions/_uv - ${command} fish >> /usr/share/fish/completions/uv.fish -} -install() { - utils_check_version "$VERSION" - check_curl_envsubst_file_untar_installed - readonly architecture="$(debian_get_arch)" - readonly binaryTargetPathTemplate='${binaryTargetFolder}/${binaryName}' - if [ "$VERSION" == 'latest' ] || [ -z "$VERSION" ]; then - # Avoid GitHub API rate limits by using the latest/download URL - readonly downloadUrl="$(echo -n "$downloadUrlLatestTemplate" | envsubst)" - else - readonly version="${VERSION:?}" - readonly downloadUrl="$(echo -n "$downloadUrlTemplate" | envsubst)" - fi - curl_check_url "$downloadUrl" - # The archive contains files under a directory: uv-${architecture}-${os}/ - readonly uvPathInArchive="uv-${architecture}-${os}/$binaryName" - readonly uvxPathInArchive="uv-${architecture}-${os}/uvx" - readonly stripComponents="$(echo -n "$uvPathInArchive" | awk -F'/' '{print NF-1}')" - readonly binaryTargetPath="$(echo -n "$binaryTargetPathTemplate" | envsubst)" - readonly uvxTargetPath="${binaryTargetFolder}/uvx" - # Extract uv and uvx in a single download/untar - curl_download_untar "$downloadUrl" "$stripComponents" "$binaryTargetFolder" "$uvPathInArchive" "$uvxPathInArchive" - chmod 755 "$binaryTargetPath" - chmod 755 "$uvxTargetPath" - if [ "$AUTOCOMPLETION" = "true" ]; then - mkdir -p /usr/share/fish/completions/ - enable_autocompletion "uv generate-shell-completion" - - # compability with older uv versions - if command -v uvx &> /dev/null; then - enable_autocompletion "uvx --generate-shell-completion" - fi - fi -} -echo_banner "devcontainer.community" -echo "Installing $name..." -install "$@" -echo "(*) Done!" From acdaf40fd739c3e2080e80ba00edd819b944e233 Mon Sep 17 00:00:00 2001 From: Bas Steins <592313+sebst@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:04:02 +0000 Subject: [PATCH 2/2] feat: Add `uv` feature (astral.sh/uv) (#62) --- src/uv/devcontainer-feature.json | 22 ++++ src/uv/install.sh | 167 +++++++++++++++++++++++++++++++ test/uv/test.sh | 19 ++++ 3 files changed, 208 insertions(+) create mode 100644 src/uv/devcontainer-feature.json create mode 100755 src/uv/install.sh create mode 100755 test/uv/test.sh diff --git a/src/uv/devcontainer-feature.json b/src/uv/devcontainer-feature.json new file mode 100644 index 0000000..ec725fe --- /dev/null +++ b/src/uv/devcontainer-feature.json @@ -0,0 +1,22 @@ +{ + "name": "uv", + "id": "uv", + "version": "1.0.0", + "description": "Install \"uv\" and \"uvx\" binaries", + "documentationURL": "https://github.com/devcontainer-community/devcontainer-features/tree/main/src/uv", + "options": { + "version": { + "type": "string", + "default": "latest", + "proposals": [ + "latest" + ], + "description": "Version of \"uv\" to install." + }, + "shellautocompletion": { + "default": false, + "description": "Enable or disable uv and uvx autocompletion.", + "type": "boolean" + } + } +} \ No newline at end of file diff --git a/src/uv/install.sh b/src/uv/install.sh new file mode 100755 index 0000000..9b5eb09 --- /dev/null +++ b/src/uv/install.sh @@ -0,0 +1,167 @@ +#!/bin/bash +set -o errexit +set -o pipefail +set -o noclobber +set -o nounset +set -o allexport +readonly githubRepository='astral-sh/uv' +readonly binaryName='uv' +readonly versionArgument='--version' +readonly os="unknown-linux-musl" +readonly downloadUrlTemplate='https://github.com/${githubRepository}/releases/download/${version}/${binaryName}-${architecture}-${os}.tar.gz' +readonly downloadUrlLatestTemplate='https://github.com/${githubRepository}/releases/latest/download/${binaryName}-${architecture}-${os}.tar.gz' +readonly binaryTargetFolder='/usr/local/bin' +readonly name="${githubRepository##*/}" +readonly AUTOCOMPLETION="${SHELLAUTOCOMPLETION:-"true"}" +apt_get_update() { + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi +} +apt_get_checkinstall() { + if ! dpkg -s "$@" >/dev/null 2>&1; then + apt_get_update + DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends --no-install-suggests --option 'Debug::pkgProblemResolver=true' --option 'Debug::pkgAcquire::Worker=1' "$@" + fi +} +apt_get_cleanup() { + apt-get clean + rm -rf /var/lib/apt/lists/* +} +check_curl_envsubst_file_untar_installed() { + declare -a requiredAptPackagesMissing=() + if ! [ -r '/etc/ssl/certs/ca-certificates.crt' ]; then + requiredAptPackagesMissing+=('ca-certificates') + fi + if ! command -v curl >/dev/null 2>&1; then + requiredAptPackagesMissing+=('curl') + fi + if ! command -v envsubst >/dev/null 2>&1; then + requiredAptPackagesMissing+=('gettext-base') + fi + if ! command -v file >/dev/null 2>&1; then + requiredAptPackagesMissing+=('file') + fi + if ! command -v tar >/dev/null 2>&1; then + requiredAptPackagesMissing+=('tar') + fi + declare -i requiredAptPackagesMissingCount=${#requiredAptPackagesMissing[@]} + if [ $requiredAptPackagesMissingCount -gt 0 ]; then + apt_get_update + apt_get_checkinstall "${requiredAptPackagesMissing[@]}" + apt_get_cleanup + fi +} +curl_check_url() { + local url=$1 + local status_code + status_code=$(curl -s -o /dev/null -w '%{http_code}' "$url") + if [ "$status_code" -ne 200 ] && [ "$status_code" -ne 302 ]; then + echo "Failed to download '$url'. Status code: $status_code." + return 1 + fi +} +curl_download_stdout() { + local url=$1 + curl \ + --silent \ + --location \ + --output '-' \ + --connect-timeout 5 \ + "$url" +} +curl_download_untar() { + local url=$1 + local strip=$2 + local target=$3 + shift 3 + # Remaining arguments are one or more paths within the archive to extract + curl_download_stdout "$url" | tar \ + -xz \ + -f '-' \ + --strip-components="$strip" \ + -C "$target" \ + "$@" +} +debian_get_arch() { + arch=$(uname -m) + if [[ "$arch" == "aarch64" ]]; then + arch="aarch64" + elif [[ "$arch" == "x86_64" ]]; then + arch="x86_64" + fi + echo "$arch" +# echo "$(dpkg --print-architecture)" --- IGNORE --- +} +echo_banner() { + local text="$1" + echo -e "\e[1m\e[97m\e[41m$text\e[0m" +} +github_list_releases() { + if [ -z "$1" ]; then + echo "Usage: list_github_releases " + return 1 + fi + local repo="$1" + local url="https://api.github.com/repos/$repo/releases" + curl -s "$url" | grep -Po '"tag_name": "\K.*?(?=")' | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^v//' +} +github_get_latest_release() { + if [ -z "$1" ]; then + echo "Usage: get_latest_github_release " + return 1 + fi + github_list_releases "$1" | head -n 1 +} +utils_check_version() { + local version=$1 + if ! [[ "${version:-}" =~ ^(latest|[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + printf >&2 '=== [ERROR] Option "version" (value: "%s") is not "latest" or valid semantic version format "X.Y.Z" !\n' \ + "$version" + exit 1 + fi +} +enable_autocompletion() { + command=$1 + ${command} bash >> /usr/share/bash-completion/completions/uv + ${command} zsh >> /usr/share/zsh/vendor-completions/_uv + ${command} fish >> /usr/share/fish/completions/uv.fish +} +install() { + utils_check_version "$VERSION" + check_curl_envsubst_file_untar_installed + readonly architecture="$(debian_get_arch)" + readonly binaryTargetPathTemplate='${binaryTargetFolder}/${binaryName}' + if [ "$VERSION" == 'latest' ] || [ -z "$VERSION" ]; then + # Avoid GitHub API rate limits by using the latest/download URL + readonly downloadUrl="$(echo -n "$downloadUrlLatestTemplate" | envsubst)" + else + readonly version="${VERSION:?}" + readonly downloadUrl="$(echo -n "$downloadUrlTemplate" | envsubst)" + fi + curl_check_url "$downloadUrl" + # The archive contains files under a directory: uv-${architecture}-${os}/ + readonly uvPathInArchive="uv-${architecture}-${os}/$binaryName" + readonly uvxPathInArchive="uv-${architecture}-${os}/uvx" + readonly stripComponents="$(echo -n "$uvPathInArchive" | awk -F'/' '{print NF-1}')" + readonly binaryTargetPath="$(echo -n "$binaryTargetPathTemplate" | envsubst)" + readonly uvxTargetPath="${binaryTargetFolder}/uvx" + # Extract uv and uvx in a single download/untar + curl_download_untar "$downloadUrl" "$stripComponents" "$binaryTargetFolder" "$uvPathInArchive" "$uvxPathInArchive" + chmod 755 "$binaryTargetPath" + chmod 755 "$uvxTargetPath" + if [ "$AUTOCOMPLETION" = "true" ]; then + mkdir -p /usr/share/fish/completions/ + enable_autocompletion "uv generate-shell-completion" + + # compability with older uv versions + if command -v uvx &> /dev/null; then + enable_autocompletion "uvx --generate-shell-completion" + fi + fi +} +echo_banner "devcontainer.community" +echo "Installing $name..." +install "$@" +echo "(*) Done!" diff --git a/test/uv/test.sh b/test/uv/test.sh new file mode 100755 index 0000000..063e0b3 --- /dev/null +++ b/test/uv/test.sh @@ -0,0 +1,19 @@ +#!/bin/bash + + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +# See https://github.com/devcontainers/cli/blob/HEAD/docs/features/test.md#dev-container-features-test-lib +# Provides the 'check' and 'reportResults' commands. +source dev-container-features-test-lib + +# Feature-specific tests +# The 'check' command comes from the dev-container-features-test-lib. Syntax is... +# check