Skip to content

Commit bc34539

Browse files
authored
feat: Add uv feature (astral.sh/uv) (#62)
* Revert "feat: Add `uv` feature (astral.sh/uv) (#62)" This reverts commit 2f5a12d. * feat: Add `uv` feature (astral.sh/uv) (#62)
1 parent a78256f commit bc34539

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

src/uv/devcontainer-feature.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "uv",
3+
"id": "uv",
4+
"version": "1.0.0",
5+
"description": "Install \"uv\" and \"uvx\" binaries",
6+
"documentationURL": "https://github.com/devcontainer-community/devcontainer-features/tree/main/src/uv",
7+
"options": {
8+
"version": {
9+
"type": "string",
10+
"default": "latest",
11+
"proposals": [
12+
"latest"
13+
],
14+
"description": "Version of \"uv\" to install."
15+
},
16+
"shellautocompletion": {
17+
"default": false,
18+
"description": "Enable or disable uv and uvx autocompletion.",
19+
"type": "boolean"
20+
}
21+
}
22+
}

src/uv/install.sh

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/bin/bash
2+
set -o errexit
3+
set -o pipefail
4+
set -o noclobber
5+
set -o nounset
6+
set -o allexport
7+
readonly githubRepository='astral-sh/uv'
8+
readonly binaryName='uv'
9+
readonly versionArgument='--version'
10+
readonly os="unknown-linux-musl"
11+
readonly downloadUrlTemplate='https://github.com/${githubRepository}/releases/download/${version}/${binaryName}-${architecture}-${os}.tar.gz'
12+
readonly downloadUrlLatestTemplate='https://github.com/${githubRepository}/releases/latest/download/${binaryName}-${architecture}-${os}.tar.gz'
13+
readonly binaryTargetFolder='/usr/local/bin'
14+
readonly name="${githubRepository##*/}"
15+
readonly AUTOCOMPLETION="${SHELLAUTOCOMPLETION:-"true"}"
16+
apt_get_update() {
17+
if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then
18+
echo "Running apt-get update..."
19+
apt-get update -y
20+
fi
21+
}
22+
apt_get_checkinstall() {
23+
if ! dpkg -s "$@" >/dev/null 2>&1; then
24+
apt_get_update
25+
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends --no-install-suggests --option 'Debug::pkgProblemResolver=true' --option 'Debug::pkgAcquire::Worker=1' "$@"
26+
fi
27+
}
28+
apt_get_cleanup() {
29+
apt-get clean
30+
rm -rf /var/lib/apt/lists/*
31+
}
32+
check_curl_envsubst_file_untar_installed() {
33+
declare -a requiredAptPackagesMissing=()
34+
if ! [ -r '/etc/ssl/certs/ca-certificates.crt' ]; then
35+
requiredAptPackagesMissing+=('ca-certificates')
36+
fi
37+
if ! command -v curl >/dev/null 2>&1; then
38+
requiredAptPackagesMissing+=('curl')
39+
fi
40+
if ! command -v envsubst >/dev/null 2>&1; then
41+
requiredAptPackagesMissing+=('gettext-base')
42+
fi
43+
if ! command -v file >/dev/null 2>&1; then
44+
requiredAptPackagesMissing+=('file')
45+
fi
46+
if ! command -v tar >/dev/null 2>&1; then
47+
requiredAptPackagesMissing+=('tar')
48+
fi
49+
declare -i requiredAptPackagesMissingCount=${#requiredAptPackagesMissing[@]}
50+
if [ $requiredAptPackagesMissingCount -gt 0 ]; then
51+
apt_get_update
52+
apt_get_checkinstall "${requiredAptPackagesMissing[@]}"
53+
apt_get_cleanup
54+
fi
55+
}
56+
curl_check_url() {
57+
local url=$1
58+
local status_code
59+
status_code=$(curl -s -o /dev/null -w '%{http_code}' "$url")
60+
if [ "$status_code" -ne 200 ] && [ "$status_code" -ne 302 ]; then
61+
echo "Failed to download '$url'. Status code: $status_code."
62+
return 1
63+
fi
64+
}
65+
curl_download_stdout() {
66+
local url=$1
67+
curl \
68+
--silent \
69+
--location \
70+
--output '-' \
71+
--connect-timeout 5 \
72+
"$url"
73+
}
74+
curl_download_untar() {
75+
local url=$1
76+
local strip=$2
77+
local target=$3
78+
shift 3
79+
# Remaining arguments are one or more paths within the archive to extract
80+
curl_download_stdout "$url" | tar \
81+
-xz \
82+
-f '-' \
83+
--strip-components="$strip" \
84+
-C "$target" \
85+
"$@"
86+
}
87+
debian_get_arch() {
88+
arch=$(uname -m)
89+
if [[ "$arch" == "aarch64" ]]; then
90+
arch="aarch64"
91+
elif [[ "$arch" == "x86_64" ]]; then
92+
arch="x86_64"
93+
fi
94+
echo "$arch"
95+
# echo "$(dpkg --print-architecture)" --- IGNORE ---
96+
}
97+
echo_banner() {
98+
local text="$1"
99+
echo -e "\e[1m\e[97m\e[41m$text\e[0m"
100+
}
101+
github_list_releases() {
102+
if [ -z "$1" ]; then
103+
echo "Usage: list_github_releases <owner/repo>"
104+
return 1
105+
fi
106+
local repo="$1"
107+
local url="https://api.github.com/repos/$repo/releases"
108+
curl -s "$url" | grep -Po '"tag_name": "\K.*?(?=")' | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^v//'
109+
}
110+
github_get_latest_release() {
111+
if [ -z "$1" ]; then
112+
echo "Usage: get_latest_github_release <owner/repo>"
113+
return 1
114+
fi
115+
github_list_releases "$1" | head -n 1
116+
}
117+
utils_check_version() {
118+
local version=$1
119+
if ! [[ "${version:-}" =~ ^(latest|[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
120+
printf >&2 '=== [ERROR] Option "version" (value: "%s") is not "latest" or valid semantic version format "X.Y.Z" !\n' \
121+
"$version"
122+
exit 1
123+
fi
124+
}
125+
enable_autocompletion() {
126+
command=$1
127+
${command} bash >> /usr/share/bash-completion/completions/uv
128+
${command} zsh >> /usr/share/zsh/vendor-completions/_uv
129+
${command} fish >> /usr/share/fish/completions/uv.fish
130+
}
131+
install() {
132+
utils_check_version "$VERSION"
133+
check_curl_envsubst_file_untar_installed
134+
readonly architecture="$(debian_get_arch)"
135+
readonly binaryTargetPathTemplate='${binaryTargetFolder}/${binaryName}'
136+
if [ "$VERSION" == 'latest' ] || [ -z "$VERSION" ]; then
137+
# Avoid GitHub API rate limits by using the latest/download URL
138+
readonly downloadUrl="$(echo -n "$downloadUrlLatestTemplate" | envsubst)"
139+
else
140+
readonly version="${VERSION:?}"
141+
readonly downloadUrl="$(echo -n "$downloadUrlTemplate" | envsubst)"
142+
fi
143+
curl_check_url "$downloadUrl"
144+
# The archive contains files under a directory: uv-${architecture}-${os}/
145+
readonly uvPathInArchive="uv-${architecture}-${os}/$binaryName"
146+
readonly uvxPathInArchive="uv-${architecture}-${os}/uvx"
147+
readonly stripComponents="$(echo -n "$uvPathInArchive" | awk -F'/' '{print NF-1}')"
148+
readonly binaryTargetPath="$(echo -n "$binaryTargetPathTemplate" | envsubst)"
149+
readonly uvxTargetPath="${binaryTargetFolder}/uvx"
150+
# Extract uv and uvx in a single download/untar
151+
curl_download_untar "$downloadUrl" "$stripComponents" "$binaryTargetFolder" "$uvPathInArchive" "$uvxPathInArchive"
152+
chmod 755 "$binaryTargetPath"
153+
chmod 755 "$uvxTargetPath"
154+
if [ "$AUTOCOMPLETION" = "true" ]; then
155+
mkdir -p /usr/share/fish/completions/
156+
enable_autocompletion "uv generate-shell-completion"
157+
158+
# compability with older uv versions
159+
if command -v uvx &> /dev/null; then
160+
enable_autocompletion "uvx --generate-shell-completion"
161+
fi
162+
fi
163+
}
164+
echo_banner "devcontainer.community"
165+
echo "Installing $name..."
166+
install "$@"
167+
echo "(*) Done!"

test/uv/test.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/bash
2+
3+
4+
set -e
5+
6+
# Optional: Import test library bundled with the devcontainer CLI
7+
# See https://github.com/devcontainers/cli/blob/HEAD/docs/features/test.md#dev-container-features-test-lib
8+
# Provides the 'check' and 'reportResults' commands.
9+
source dev-container-features-test-lib
10+
11+
# Feature-specific tests
12+
# The 'check' command comes from the dev-container-features-test-lib. Syntax is...
13+
# check <LABEL> <cmd> [args...]
14+
check "execute command" bash -c "uv --version | grep 'uv'"
15+
check "execute command" bash -c "uvx --version | grep 'uvx'"
16+
17+
# Report results
18+
# If any of the checks above exited with a non-zero exit code, the test will fail.
19+
reportResults

0 commit comments

Comments
 (0)