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
36 changes: 29 additions & 7 deletions src/bun/NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,41 @@ Pin a specific version:
## Support

- Distros: `Debian` / `Ubuntu` and `Alpine`
- Architectures: `x86_64` and `arm64`
- Architectures: `x86_64` and `arm64` (based on available Bun releases)

## Tests

This feature is tested against various Distro:
This feature is tested against various Distros:

- `Ubuntu` (latest)
- `Debian` (latest)
- `Alpine` (latest)
- `Debian` with a pinned version of Bun (e.g., Bun `v1.1.38`)
- `Ubuntu` (glibc)
- `Debian` (glibc)
- `Alpine` (musl)
- `Debian` with a pinned version of Bun

*Note: Only architectures with actual Bun release assets are supported (x86_64 and arm64).* *

## Architecture and Asset Selection

This feature automatically selects the correct Bun release asset based on the detected architecture and libc:

- **x86_64**: Uses "baseline" builds for optimal CPU compatibility (e.g., `bun-linux-x64-baseline.zip`)
- **arm64**: Uses standard builds since "baseline" variants are not published for ARM64 (e.g., `bun-linux-aarch64.zip`)

The installation includes robust fallback logic:

- Primary: Preferred asset pattern for the detected architecture/libc combination
- Fallback: Alternative patterns if the primary asset is not found
- Error handling: Clear error messages if no suitable asset is available

This approach ensures compatibility across different platforms while gracefully handling potential future changes to Bun's release asset naming.

## Considerations / Future Enhancements

- If Bun’s release assets ever require explicit filtering, we can wire an `assetRegex` or pass `additionalFlags` to the `gh-release` helper. The helper already auto-filters by platform/arch, so no extra flags are set currently.
- Coexistence with Node.js: this feature only installs `bun` and a `bunx` shim and does not modify Node.js.
- PATH/profile: installation to `/usr/local/bin` avoids profile changes.
- Asset regex patterns: the fallback chain ensures compatibility even if Bun changes their release asset naming conventions.

## Debugging

- To enable additional debug logs during installation, set the environment variable `BUN_FEATURE_DEBUG=1`.
- Diagnostic logs from detection functions are emitted to stderr to keep return values clean.
2 changes: 1 addition & 1 deletion src/bun/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "bun",
"version": "1.0.0",
"version": "1.1.0",
"name": "Bun",
"documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/bun",
"description": "Bun is an all-in-one toolkit for JavaScript and TypeScript apps.",
Expand Down
263 changes: 218 additions & 45 deletions src/bun/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,240 @@ set -e

source ./library_scripts.sh

echo "Starting Bun installation"

# nanolayer is a cli utility which keeps container layers as small as possible
# source code: https://github.com/devcontainers-extra/nanolayer
# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations,
# and if missing - will download a temporary copy that automatically get deleted at the end
# of the script
echo "Ensuring nanolayer is available"
# Initialize variable to suppress shellcheck warning (assigned by ensure_nanolayer function)
nanolayer_location=""
ensure_nanolayer nanolayer_location "v0.5.6"

# Canonicalize VERSION for Bun's tag scheme when pinning
version_for_release="$VERSION"
if [ "${VERSION:-latest}" != "latest" ] && [ -n "$VERSION" ]; then
if [[ "$VERSION" =~ ^bun-v ]]; then
version_for_release="$VERSION"
elif [[ "$VERSION" =~ ^v ]]; then
version_for_release="bun-$VERSION"
else
version_for_release="bun-v$VERSION"
canonicalize_version() {
local version="$1"

if [ "$version" = "latest" ] || [ -z "$version" ]; then
echo "latest"
return 0
fi

# Remove any leading/trailing whitespace
version="$(echo "$version" | xargs)"

# Already in correct format
if [[ "$version" =~ ^bun-v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "$version"
return 0
fi
fi

# Remove 'bun-' prefix if present
if [[ "$version" =~ ^bun- ]]; then
version="${version#bun-}"
fi

# Handle versions starting with 'v'
if [[ "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "bun-$version"
return 0
fi

# Handle plain version numbers
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "bun-v$version"
return 0
fi

# Invalid version format
echo "WARNING: Invalid version format: '$version'. Using 'latest' instead." >&2
echo "latest"
}

# shellcheck disable=SC2153
# VERSION is provided by the devcontainer features framework
version_for_release="$(canonicalize_version "$VERSION")"
echo "Using Bun version: $version_for_release"

# Figure out arch and libc to disambiguate Bun's multiple Linux assets
arch_segment=""
case "$(uname -m)" in
x86_64)
arch_segment="x64"
;;
aarch64|arm64)
arch_segment="aarch64"
;;
*)
# Fallback to uname -m (unlikely used by bun release naming)
arch_segment="$(uname -m)"
;;
esac

# Detect musl (Alpine) vs glibc
libc_suffix=""
if [ -x "/sbin/apk" ]; then
libc_suffix="-musl"
detect_arch() {
local arch
arch="$(uname -m)"

case "$arch" in
x86_64|amd64)
echo "x64"
;;
aarch64|arm64)
echo "aarch64"
;;
*)
# Log warning for unsupported architectures
echo "WARNING: Architecture '$arch' is not supported by Bun" >&2
echo "WARNING: Bun only supports x64 and aarch64 architectures" >&2
echo "WARNING: Available assets can be found at: https://github.com/oven-sh/bun/releases" >&2
echo "$arch"
;;
esac
}

echo "Detecting system architecture"
if ! arch_segment="$(detect_arch)"; then
echo "ERROR: Failed to detect architecture" >&2
exit 1
fi

# Detect musl vs glibc (robust detection across multiple distros)
detect_libc() {
# Method 1: Check for apk (Alpine)
if [ -x "/sbin/apk" ]; then
echo "-musl"
return 0
fi

# Method 2: Check ldd output for musl
if command -v ldd >/dev/null 2>&1; then
if ldd --version 2>&1 | grep -qi musl; then
echo "-musl"
return 0
fi
fi

# Method 3: Check for musl library files
if [ -f "/lib/ld-musl-x86_64.so.1" ] || [ -f "/lib/ld-musl-aarch64.so.1" ]; then
echo "-musl"
return 0
fi

# Method 4: Check /proc/mounts for musl
if grep -qi musl /proc/mounts 2>/dev/null; then
echo "-musl"
return 0
fi

# Default to glibc
echo ""
}
Comment on lines 93 to 122
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it also makes sense to keep it in the library script, but let's keep it here for now, as it's crucial for the installation


echo "Detecting libc implementation"
if ! libc_suffix="$(detect_libc)"; then
echo "ERROR: Failed to detect libc implementation" >&2
exit 1
fi

# Prefer baseline builds for widest CPU compatibility; exclude profile variants
# Examples matched:
# - bun-linux-x64-baseline.zip
# - bun-linux-x64-musl-baseline.zip
# - bun-linux-aarch64-baseline.zip
# - bun-linux-aarch64-musl-baseline.zip
asset_regex="^bun-linux-${arch_segment}${libc_suffix}-baseline\\.zip$"
# Build asset regex based on architecture and libc
# - x64: Use baseline builds for widest CPU compatibility
# - aarch64: Use non-baseline builds since baseline doesn't exist for ARM64
if [ "$arch_segment" = "x64" ]; then
# For x64, try baseline first, then fallback to non-baseline
asset_regex="^bun-linux-x64${libc_suffix}-baseline\\.zip$"
else
# For aarch64, use non-baseline since baseline doesn't exist
asset_regex="^bun-linux-aarch64${libc_suffix}\\.zip$"
fi

# Bun tags are of the form 'bun-vX.Y.Z'; constrain tag discovery accordingly
release_tag_regex="^bun-v"

# Install Bun via gh-release helper with explicit asset/tag filters
$nanolayer_location \
install \
devcontainer-feature \
"ghcr.io/devcontainers-extra/features/gh-release:1" \
--option repo='oven-sh/bun' \
--option binaryNames='bun' \
--option version="$version_for_release" \
--option assetRegex="$asset_regex" \
--option releaseTagRegex="$release_tag_regex"
# Try multiple asset regex patterns for robust fallback support
# Build the ordered list of asset regex patterns.
# For x64, we prefer "baseline" first for widest CPU compatibility across
# older/varied machines, then fall back to non-baseline. For aarch64, Bun does
# not publish baseline variants, so we use standard builds.
build_asset_patterns() {
local arch="$1"
local libc="$2"

case "$arch" in
x64)
if [ "$libc" = "-musl" ]; then
echo "^bun-linux-x64-musl-baseline\\.zip$"
echo "^bun-linux-x64-musl\\.zip$"
else
echo "^bun-linux-x64-baseline\\.zip$"
echo "^bun-linux-x64\\.zip$"
fi
return 0
;;
aarch64)
if [ "$libc" = "-musl" ]; then
echo "^bun-linux-aarch64-musl\\.zip$"
else
echo "^bun-linux-aarch64\\.zip$"
fi
return 0
;;
*)
echo "ERROR: Unsupported architecture '$arch'" >&2
echo "ERROR: Bun only supports x64 and aarch64 architectures" >&2
echo "ERROR: Available assets can be found at: https://github.com/oven-sh/bun/releases" >&2
return 1
;;
esac
}

# Build asset patterns for current architecture
if ! asset_patterns_output=$(build_asset_patterns "$arch_segment" "$libc_suffix"); then
echo "ERROR: Failed to build asset patterns for arch=$arch_segment, libc_suffix=$libc_suffix" >&2
exit 1
fi

# Convert to array - use a more reliable method
# Split by newlines and create array
asset_patterns=()
while IFS= read -r line; do
if [ -n "$line" ]; then
asset_patterns+=("$line")
fi
done <<< "$asset_patterns_output"

# Verify we have asset patterns
if [ ${#asset_patterns[@]} -eq 0 ]; then
echo "ERROR: No asset patterns generated for arch=$arch_segment, libc_suffix=$libc_suffix" >&2
echo "ERROR: asset_patterns_output was: '$asset_patterns_output'" >&2
exit 1
fi

# Try each asset pattern until one succeeds
install_bun() {
if [ ${#asset_patterns[@]} -eq 0 ]; then
echo "ERROR: install_bun: No asset patterns provided!" >&2
return 1
fi

for asset_regex in "${asset_patterns[@]}"; do
if $nanolayer_location \
install \
devcontainer-feature \
"ghcr.io/devcontainers-extra/features/gh-release:1" \
--option repo='oven-sh/bun' \
--option binaryNames='bun' \
--option version="$version_for_release" \
--option assetRegex="$asset_regex" \
--option releaseTagRegex="$release_tag_regex" 2>&1; then
return 0
fi
done

echo "ERROR: Failed to install Bun. No asset patterns matched." >&2
echo "ERROR: Troubleshooting information:" >&2
echo "ERROR: - Architecture: $arch_segment" >&2
echo "ERROR: - Libc suffix: ${libc_suffix:-(none/glibc)}" >&2
echo "ERROR: - Version: $version_for_release" >&2
echo "ERROR: - Asset patterns tried: ${asset_patterns[*]}" >&2
echo "ERROR: Check https://github.com/oven-sh/bun/releases for available assets" >&2
return 1
}

# Attempt installation
if ! install_bun; then
echo "ERROR: install_bun function failed" >&2
exit 1
fi

echo "Bun installation process completed"

# Provide a convenient bunx shim
if [ -x "/usr/local/bin/bun" ] && ! [ -x "/usr/local/bin/bunx" ]; then
Expand All @@ -75,4 +248,4 @@ EOF
chmod +x /usr/local/bin/bunx
fi

echo 'Bun installation completed.'
echo "Bun installation completed successfully"
2 changes: 1 addition & 1 deletion test/bun/scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"image": "mcr.microsoft.com/devcontainers/base:debian",
"features": {
"bun": {
"version": "1.2.20"
"version": "1.3.8"
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion test/bun/test_specific_version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

set -e

# shellcheck disable=SC1091
# dev-container-features-test-lib is provided by the test framework at runtime
source dev-container-features-test-lib

check "bun version is equal to 1.2.20" sh -c "bun --version | grep '^1.2.20'"
check "bun version is equal to 1.3.8" sh -c "bun --version | grep '^1.3.8'"

reportResults