diff --git a/src/bun/NOTES.md b/src/bun/NOTES.md new file mode 100644 index 00000000..966e8989 --- /dev/null +++ b/src/bun/NOTES.md @@ -0,0 +1,52 @@ +# Additional Notes + +- Bun Website: [https://bun.com/](https://bun.com/) +- Bun GitHub: [https://github.com/oven-sh/bun](https://github.com/oven-sh/bun) + +## Install Method + +This feature installs Bun via the repository's `gh-release` helper, which downloads the correct release asset for the current platform and architecture and places the `bun` binary in `/usr/local/bin`. This follows the repo’s best practices for reproducibility and small image layers. + +## bunx shim + +Bun supports executing binaries with `bun x `. Some environments/tools expect a `bunx` executable. To provide a consistent experience, this feature adds a small shim at `/usr/local/bin/bunx` that forwards to `bun x`. If the upstream release reliably includes a `bunx` binary in the future, the shim can be removed without breaking users. + +## Usage Examples + +Default (latest): + +```json +"features": { + "ghcr.io/devcontainers-extra/features/bun:1": {} +} +``` + +Pin a specific version: + +```json +"features": { + "ghcr.io/devcontainers-extra/features/bun:1": { + "version": "1.1.38" + } +} +``` + +## Support + +- Distros: `Debian` / `Ubuntu` and `Alpine` +- Architectures: `x86_64` and `arm64` + +## Tests + +This feature is tested against various Distro: + +- `Ubuntu` (latest) +- `Debian` (latest) +- `Alpine` (latest) +- `Debian` with a pinned version of Bun (e.g., Bun `v1.1.38`) + +## 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. diff --git a/src/bun/README.md b/src/bun/README.md new file mode 100644 index 00000000..c7fea1b7 --- /dev/null +++ b/src/bun/README.md @@ -0,0 +1,23 @@ +# Bun (via Github Releases) (bun) + +Installs the Bun JavaScript runtime and package manager via GitHub Releases using the gh-release helper. + +## Example Usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/bun:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| version | Select the version to install. | string | latest | + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/bun/devcontainer-feature.json b/src/bun/devcontainer-feature.json new file mode 100644 index 00000000..ba48f3b1 --- /dev/null +++ b/src/bun/devcontainer-feature.json @@ -0,0 +1,20 @@ +{ + "id": "bun", + "version": "1.0.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.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-extra/features/gh-release" + ] +} diff --git a/src/bun/install.sh b/src/bun/install.sh new file mode 100755 index 00000000..504fabe5 --- /dev/null +++ b/src/bun/install.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# 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 +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" + fi +fi + +# 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" +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$" + +# 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" + +# Provide a convenient bunx shim +if [ -x "/usr/local/bin/bun" ] && ! [ -x "/usr/local/bin/bunx" ]; then + cat >/usr/local/bin/bunx <<'EOF' +#!/usr/bin/env bash +exec bun x "$@" +EOF + chmod +x /usr/local/bin/bunx +fi + +echo 'Bun installation completed.' diff --git a/src/bun/library_scripts.sh b/src/bun/library_scripts.sh new file mode 100644 index 00000000..f6d0760d --- /dev/null +++ b/src/bun/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/test/bun/scenarios.json b/test/bun/scenarios.json new file mode 100644 index 00000000..24179467 --- /dev/null +++ b/test/bun/scenarios.json @@ -0,0 +1,28 @@ +{ + "test_ubuntu": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "bun": {} + } + }, + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "bun": {} + } + }, + "test_alpine": { + "image": "mcr.microsoft.com/devcontainers/base:alpine", + "features": { + "bun": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "bun": { + "version": "1.2.20" + } + } + } +} \ No newline at end of file diff --git a/test/bun/test.sh b/test/bun/test.sh new file mode 100755 index 00000000..0835be05 --- /dev/null +++ b/test/bun/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "bun is installed" bun --version + +reportResults diff --git a/test/bun/test_alpine.sh b/test/bun/test_alpine.sh new file mode 100644 index 00000000..0835be05 --- /dev/null +++ b/test/bun/test_alpine.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "bun is installed" bun --version + +reportResults diff --git a/test/bun/test_debian.sh b/test/bun/test_debian.sh new file mode 100755 index 00000000..0835be05 --- /dev/null +++ b/test/bun/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "bun is installed" bun --version + +reportResults diff --git a/test/bun/test_specific_version.sh b/test/bun/test_specific_version.sh new file mode 100755 index 00000000..7bfb9748 --- /dev/null +++ b/test/bun/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "bun version is equal to 1.2.20" sh -c "bun --version | grep '^1.2.20'" + +reportResults diff --git a/test/bun/test_ubuntu.sh b/test/bun/test_ubuntu.sh new file mode 100644 index 00000000..0835be05 --- /dev/null +++ b/test/bun/test_ubuntu.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "bun is installed" bun --version + +reportResults