From 2442dc58444d5d3beac3a9809a385df9e9ce28ac Mon Sep 17 00:00:00 2001 From: Zach Mullen Date: Mon, 20 Oct 2025 09:29:23 -0400 Subject: [PATCH 01/71] Fix heroku-cli install script on ARM64 (aarch64) CPUs Fixes #134 --- src/heroku-cli/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/heroku-cli/install.sh b/src/heroku-cli/install.sh index 7e715f56f..9700864f1 100755 --- a/src/heroku-cli/install.sh +++ b/src/heroku-cli/install.sh @@ -20,10 +20,10 @@ rm -rf /var/lib/apt/lists/* ARCH="$(uname -m)" if [ "$ARCH" == "x86_64" ]; then ARCH=x64 +elif [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then + ARCH=arm64 elif [[ "$ARCH" == aarch* ]]; then ARCH=arm -elif [[ "$ARCH" == "arm64" ]]; then - ARCH=arm64 else echo -e "unsupported arch: $ARCH" exit 1 From e697a920f7f3e2b0f8a4296d0d3e099a87756473 Mon Sep 17 00:00:00 2001 From: Zach Mullen Date: Mon, 20 Oct 2025 14:06:40 -0400 Subject: [PATCH 02/71] Bump version of heroku-cli feature to 1.0.5 --- src/heroku-cli/devcontainer-feature.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/heroku-cli/devcontainer-feature.json b/src/heroku-cli/devcontainer-feature.json index ea55b2f28..4c7b3abab 100644 --- a/src/heroku-cli/devcontainer-feature.json +++ b/src/heroku-cli/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "Heroku CLI (via cli-assets.heroku.com)", "id": "heroku-cli", - "version": "1.0.4", + "version": "1.0.5", "description": "Heroku CLI allows you to interact with the Heroku API via the command line (version 8 and above)", "documentationURL": "https://github.com/devcontainers-extra/features/tree/main/src/heroku-cli", "options": { From 15427b07f371d63322b55afca68e437f8c1e1c21 Mon Sep 17 00:00:00 2001 From: Tom Plant Date: Fri, 31 Oct 2025 12:59:16 +0000 Subject: [PATCH 03/71] Add Dart Signed-off-by: Tom Plant --- src/dart/devcontainer-feature.json | 20 ++++ src/dart/install.sh | 21 ++++ src/dart/library_scripts.sh | 173 +++++++++++++++++++++++++++++ test/dart/scenarios.json | 16 +++ test/dart/test.sh | 9 ++ test/dart/test_debian.sh | 9 ++ test/dart/test_specific_version.sh | 9 ++ 7 files changed, 257 insertions(+) create mode 100644 src/dart/devcontainer-feature.json create mode 100755 src/dart/install.sh create mode 100644 src/dart/library_scripts.sh create mode 100644 test/dart/scenarios.json create mode 100755 test/dart/test.sh create mode 100755 test/dart/test_debian.sh create mode 100755 test/dart/test_specific_version.sh diff --git a/src/dart/devcontainer-feature.json b/src/dart/devcontainer-feature.json new file mode 100644 index 000000000..878009f44 --- /dev/null +++ b/src/dart/devcontainer-feature.json @@ -0,0 +1,20 @@ +{ + "id": "dart", + "version": "1.0.0", + "name": "dart (via asdf)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/dart", + "description": "Dart is an approachable, portable, and productive programming language for high-quality apps on any platform", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-extra/features/asdf-package" + ] +} \ No newline at end of file diff --git a/src/dart/install.sh b/src/dart/install.sh new file mode 100755 index 000000000..b19da22a7 --- /dev/null +++ b/src/dart/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/asdf-package:1.0.10" \ + --option plugin='dart' --option version="$VERSION" + +echo 'Done!' diff --git a/src/dart/library_scripts.sh b/src/dart/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/dart/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/dart/scenarios.json b/test/dart/scenarios.json new file mode 100644 index 000000000..08ea01b67 --- /dev/null +++ b/test/dart/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "dart": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "dart": { + "version": "3.9.4" + } + } + } +} \ No newline at end of file diff --git a/test/dart/test.sh b/test/dart/test.sh new file mode 100755 index 000000000..1fe0590eb --- /dev/null +++ b/test/dart/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "dart is installed" dart --version + +reportResults diff --git a/test/dart/test_debian.sh b/test/dart/test_debian.sh new file mode 100755 index 000000000..8a04fbb07 --- /dev/null +++ b/test/dart/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "dart --version" dart --version + +reportResults diff --git a/test/dart/test_specific_version.sh b/test/dart/test_specific_version.sh new file mode 100755 index 000000000..39729d722 --- /dev/null +++ b/test/dart/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "dart version is equal to 3.9.4" sh -c "dart --version | grep '3.9.4'" + +reportResults From a9efdc79d1246812da58108ba20a1a27fbbc5981 Mon Sep 17 00:00:00 2001 From: Tom Plant Date: Sun, 2 Nov 2025 11:11:26 +1000 Subject: [PATCH 04/71] Use non-latest version for pinned version test Co-authored-by: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> Signed-off-by: Tom Plant --- test/dart/scenarios.json | 2 +- test/dart/test_specific_version.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dart/scenarios.json b/test/dart/scenarios.json index 08ea01b67..f561c70df 100644 --- a/test/dart/scenarios.json +++ b/test/dart/scenarios.json @@ -9,7 +9,7 @@ "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { "dart": { - "version": "3.9.4" + "version": "3.9.3" } } } diff --git a/test/dart/test_specific_version.sh b/test/dart/test_specific_version.sh index 39729d722..765cf9503 100755 --- a/test/dart/test_specific_version.sh +++ b/test/dart/test_specific_version.sh @@ -4,6 +4,6 @@ set -e source dev-container-features-test-lib -check "dart version is equal to 3.9.4" sh -c "dart --version | grep '3.9.4'" +check "dart version is equal to 3.9.3" sh -c "dart --version | grep '3.9.3'" reportResults From 93697ebf9cc312bf9123323ea3940d42250d1f6d Mon Sep 17 00:00:00 2001 From: patrickcrocker Date: Sun, 2 Nov 2025 12:21:39 +0000 Subject: [PATCH 05/71] fix(uv): correct variable name mismatch in architecture detection Fix variable naming inconsistency in install.sh where 'assetRegex' was declared but 'asset_regex' was referenced, causing incorrect architecture detection on ARM64 systems. The mismatch resulted in an empty asset regex being passed to the gh-release feature, leading to wrong architecture binary downloads and failures when running 'uv python install' on ARM64 processors. Fixes #155 --- src/uv/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uv/install.sh b/src/uv/install.sh index c544d7b65..53cf00ee9 100755 --- a/src/uv/install.sh +++ b/src/uv/install.sh @@ -25,7 +25,7 @@ case "$(uname -m)" in arch_segment="$(uname -m)" ;; esac -assetRegex="^uv-${arch_segment}-unknown-linux-gnu\\.tar\\.gz$" +asset_regex="^uv-${arch_segment}-unknown-linux-gnu\\.tar\\.gz$" # Example nanolayer installation via devcontainer-feature $nanolayer_location \ From 18fcf75590a0ce1d92528d0d0e4937c463f6b8e1 Mon Sep 17 00:00:00 2001 From: Patrick Crocker Date: Sun, 2 Nov 2025 15:11:14 -0600 Subject: [PATCH 06/71] Apply suggestion from @koralowiec Co-authored-by: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> Signed-off-by: Patrick Crocker --- src/uv/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uv/install.sh b/src/uv/install.sh index 53cf00ee9..a2d76746a 100755 --- a/src/uv/install.sh +++ b/src/uv/install.sh @@ -16,7 +16,7 @@ ensure_nanolayer nanolayer_location "v0.5.6" arch_segment="" case "$(uname -m)" in x86_64) - arch_segment="x64" + arch_segment="x86_64" ;; aarch64|arm64) arch_segment="aarch64" From a778c9ac3ef21cefb6e1fe481efed966fa9aab98 Mon Sep 17 00:00:00 2001 From: patrickcrocker Date: Sun, 2 Nov 2025 21:18:07 +0000 Subject: [PATCH 07/71] chore(uv): bump version to 1.0.2 Increment uv feature version from 1.0.1 to 1.0.2. Fixes #15 --- src/uv/devcontainer-feature.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uv/devcontainer-feature.json b/src/uv/devcontainer-feature.json index 7f266c238..ce9995d06 100644 --- a/src/uv/devcontainer-feature.json +++ b/src/uv/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "uv", - "version": "1.0.1", + "version": "1.0.2", "name": "uv (via GitHub Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/uv", "description": "An extremely fast Python package and project manager, written in Rust.", From df9bc98a435536ba85a85bc7057835328fbe1947 Mon Sep 17 00:00:00 2001 From: patrickcrocker Date: Sun, 2 Nov 2025 21:55:57 +0000 Subject: [PATCH 08/71] fix(btm): improved arch detection and filter out nightly builds - Add dynamic architecture and C library type detection (musl vs gnu) - Add release tag regex to exclude nightly prereleases (fixed downloading latest version) - Update nanolayer version from v0.5.4 to v0.5.6 - Update gh-release dependency from 1.0.25 to 1.0.26 - Update install.sh and library_scripts.sh with latest template folder versions - Add test scenario for specific version (0.11.0) - Bump feature version to 1.0.17 Fixes #15 --- archive/src/btm/devcontainer-feature.json | 2 +- archive/src/btm/install.sh | 35 +++++++++-- archive/src/btm/library_scripts.sh | 71 ++++++++++++----------- archive/test/btm/scenarios.json | 8 +++ archive/test/btm/test_specific_version.sh | 9 +++ 5 files changed, 84 insertions(+), 41 deletions(-) create mode 100644 archive/test/btm/test_specific_version.sh diff --git a/archive/src/btm/devcontainer-feature.json b/archive/src/btm/devcontainer-feature.json index 8e9048345..e384107bd 100644 --- a/archive/src/btm/devcontainer-feature.json +++ b/archive/src/btm/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "btm", - "version": "1.0.16", + "version": "1.0.17", "name": "bottom (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/btm", "description": "bottom is a customizable cross-platform graphical process/system monitor for the terminal.", diff --git a/archive/src/btm/install.sh b/archive/src/btm/install.sh index 56871f3ba..081aac365 100755 --- a/archive/src/btm/install.sh +++ b/archive/src/btm/install.sh @@ -1,21 +1,46 @@ +#!/usr/bin/env bash set -e -. ./library_scripts.sh +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.4" - +ensure_nanolayer nanolayer_location "v0.5.6" + +# Detect C library type +if [ -f "/lib/ld-musl-x86_64.so.1" ] || [ -f "/lib/ld-musl-aarch64.so.1" ]; then + clib_type=musl +else + clib_type=gnu +fi + +# Detect architecture +architecture="$(uname -m)" +case $architecture in + arm64) + arch="aarch64" + ;; + armv7) + clib_type="${clib_type}eabihf" + ;; + *) + arch="$architecture" + ;; +esac +asset_regex="^bottom_${arch}-unknown-linux-${clib_type}\\.tar\\.gz$" + +# Exclude nightly prereleases, only match semantic version tags +release_tag_regex="^(?!.*nightly)[0-9]+\\.[0-9]+\\.[0-9]+$" $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/gh-release:1.0.25" \ - --option repo='ClementTsang/bottom' --option binaryNames='btm' --option version="$VERSION" --option assetRegex='^(?!.*(2-17))' + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='ClementTsang/bottom' --option binaryNames='btm' --option version="$VERSION" --option assetRegex="$asset_regex" --option releaseTagRegex="$release_tag_regex" diff --git a/archive/src/btm/library_scripts.sh b/archive/src/btm/library_scripts.sh index 9e1072eca..f6d0760d7 100644 --- a/archive/src/btm/library_scripts.sh +++ b/archive/src/btm/library_scripts.sh @@ -1,4 +1,4 @@ - +#!/usr/bin/env bash clean_download() { # The purpose of this function is to download a file with minimal impact on container layer size @@ -15,7 +15,7 @@ clean_download() { tempdir=$(mktemp -d) downloader_installed="" - _apt_get_install() { + function _apt_get_install() { tempdir=$1 # copy current state of apt list - in order to revert back later (minimize contianer layer size) @@ -24,7 +24,7 @@ clean_download() { apt-get -y install --no-install-recommends wget ca-certificates } - _apt_get_cleanup() { + function _apt_get_cleanup() { tempdir=$1 echo "removing wget" @@ -35,15 +35,15 @@ clean_download() { rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists } - _apk_install() { + 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 + apk add --no-cache wget } - _apk_cleanup() { + function _apk_cleanup() { tempdir=$1 echo "removing wget" @@ -59,10 +59,10 @@ clean_download() { fi # in case none of them is installed, install wget temporarly - if [ -z $downloader ] ; then - if [ -x "/usr/bin/apt-get" ] ; then + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then _apt_get_install $tempdir - elif [ -x "/sbin/apk" ] ; then + elif [ -x "/sbin/apk" ]; then _apk_install $tempdir else echo "distro not supported" @@ -72,7 +72,7 @@ clean_download() { downloader_installed="true" fi - if [ $downloader = "wget" ] ; then + if [ $downloader = "wget" ]; then wget -q $url -O $output_location else curl -sfL $url -o $output_location @@ -80,10 +80,10 @@ clean_download() { # 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 + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then _apt_get_cleanup $tempdir - elif [ -x "/sbin/apk" ] ; then + elif [ -x "/sbin/apk" ]; then _apk_cleanup $tempdir else echo "distro not supported" @@ -93,56 +93,60 @@ clean_download() { } - 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="" + 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 [[ -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 + 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" + 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 + if ! [[ -z "${nanolayer_location}" ]]; then local current_version - current_version=$($__nanolayer_location --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="" + 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 [[ -z "${nanolayer_location}" ]]; then - if [ "$(uname -sm)" = 'Linux x86_64' ] || [ "$(uname -sm)" = "Linux aarch64" ]; then + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) - clean_up () { + clean_up() { ARG=$? rm -rf $tmp_dir exit $ARG } trap clean_up EXIT - - if [ -x "/sbin/apk" ] ; then + if [ -x "/sbin/apk" ]; then clib_type=musl else clib_type=gnu @@ -155,8 +159,7 @@ ensure_nanolayer() { tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" chmod a+x $tmp_dir/nanolayer - __nanolayer_location=$tmp_dir/nanolayer - + nanolayer_location=$tmp_dir/nanolayer else echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" @@ -165,8 +168,6 @@ ensure_nanolayer() { fi # Expose outside the resolved location - export ${variable_name}=$__nanolayer_location + declare -g ${variable_name}=$nanolayer_location } - - diff --git a/archive/test/btm/scenarios.json b/archive/test/btm/scenarios.json index 56389dc03..88b24d836 100644 --- a/archive/test/btm/scenarios.json +++ b/archive/test/btm/scenarios.json @@ -4,5 +4,13 @@ "features": { "btm": {} } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "btm": { + "version": "0.11.0" + } + } } } \ No newline at end of file diff --git a/archive/test/btm/test_specific_version.sh b/archive/test/btm/test_specific_version.sh new file mode 100644 index 000000000..2476ea3a8 --- /dev/null +++ b/archive/test/btm/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "btm version is equal to 0.11.0" sh -c "btm --version | grep '0.11.0'" + +reportResults From 0122ed86c7a2b748482416f9475ffe5622eb490a Mon Sep 17 00:00:00 2001 From: patrickcrocker Date: Sun, 2 Nov 2025 21:58:34 +0000 Subject: [PATCH 09/71] fix(btm): pin gh-release feature to version 1.0.26 Fixes #15 --- archive/src/btm/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archive/src/btm/install.sh b/archive/src/btm/install.sh index 081aac365..b6cc158ad 100755 --- a/archive/src/btm/install.sh +++ b/archive/src/btm/install.sh @@ -39,7 +39,7 @@ release_tag_regex="^(?!.*nightly)[0-9]+\\.[0-9]+\\.[0-9]+$" $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/gh-release:1" \ + "ghcr.io/devcontainers-extra/features/gh-release:1.0.26" \ --option repo='ClementTsang/bottom' --option binaryNames='btm' --option version="$VERSION" --option assetRegex="$asset_regex" --option releaseTagRegex="$release_tag_regex" From 88cc10c411edc2d64e68ee02307091defb3e6956 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:27:50 +0100 Subject: [PATCH 10/71] feat(claude-code): init --- src/claude-code/README.md | 30 ++++ src/claude-code/devcontainer-feature.json | 25 ++++ src/claude-code/install.sh | 21 +++ src/claude-code/library_scripts.sh | 173 ++++++++++++++++++++++ test/claude-code/scenarios.json | 16 ++ test/claude-code/test.sh | 9 ++ test/claude-code/test_debian.sh | 9 ++ test/claude-code/test_specific_version.sh | 9 ++ 8 files changed, 292 insertions(+) create mode 100644 src/claude-code/README.md create mode 100644 src/claude-code/devcontainer-feature.json create mode 100644 src/claude-code/install.sh create mode 100644 src/claude-code/library_scripts.sh create mode 100644 test/claude-code/scenarios.json create mode 100644 test/claude-code/test.sh create mode 100644 test/claude-code/test_debian.sh create mode 100644 test/claude-code/test_specific_version.sh diff --git a/src/claude-code/README.md b/src/claude-code/README.md new file mode 100644 index 000000000..321958cd2 --- /dev/null +++ b/src/claude-code/README.md @@ -0,0 +1,30 @@ + +# claude-code (via npm) (claude-code) + +Claude Code is an agentic coding tool that lives in your terminal + +## Example Usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/claude-code:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| version | Select the version to install. | string | latest | + +## Customizations + +### VS Code Extensions + +- `anthropic.claude-code` + + + +--- + +_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/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json new file mode 100644 index 000000000..281bec53c --- /dev/null +++ b/src/claude-code/devcontainer-feature.json @@ -0,0 +1,25 @@ +{ + "id": "claude-code", + "version": "1.0.0", + "name": "claude-code (via npm)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/claude-code", + "description": "Claude Code is an agentic coding tool that lives in your terminal", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "anthropic.claude-code" + ] + } + }, + "installsAfter": [] +} \ No newline at end of file diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh new file mode 100644 index 000000000..adc2d3820 --- /dev/null +++ b/src/claude-code/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/npm-package:1.0.4" \ + --option package='@anthropic-ai/claude-code' --option version="$VERSION" + +echo 'Done!' diff --git a/src/claude-code/library_scripts.sh b/src/claude-code/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/claude-code/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/claude-code/scenarios.json b/test/claude-code/scenarios.json new file mode 100644 index 000000000..6f06352cc --- /dev/null +++ b/test/claude-code/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "claude-code": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "claude-code": { + "version": "2.0.30" + } + } + } +} \ No newline at end of file diff --git a/test/claude-code/test.sh b/test/claude-code/test.sh new file mode 100644 index 000000000..d8c00b08c --- /dev/null +++ b/test/claude-code/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "claude is installed" claude --version + +reportResults diff --git a/test/claude-code/test_debian.sh b/test/claude-code/test_debian.sh new file mode 100644 index 000000000..d8c00b08c --- /dev/null +++ b/test/claude-code/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "claude is installed" claude --version + +reportResults diff --git a/test/claude-code/test_specific_version.sh b/test/claude-code/test_specific_version.sh new file mode 100644 index 000000000..5a0b564f2 --- /dev/null +++ b/test/claude-code/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "claude version is equal to 2.0.30" sh -c "claude --version | grep '2.0.30'" + +reportResults From 22653d1f9ba2b80193438513ae926c3139841c1b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Nov 2025 12:03:55 +0000 Subject: [PATCH 11/71] chore: archive istioctl feature Moved istioctl feature from src/ and test/ to archive/ directories. --- {src => archive/src}/istioctl/README.md | 0 {src => archive/src}/istioctl/devcontainer-feature.json | 0 {src => archive/src}/istioctl/install.sh | 0 {src => archive/src}/istioctl/library_scripts.sh | 0 {test => archive/test}/istioctl/scenarios.json | 0 {test => archive/test}/istioctl/test_defaults_debian.sh | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {src => archive/src}/istioctl/README.md (100%) rename {src => archive/src}/istioctl/devcontainer-feature.json (100%) rename {src => archive/src}/istioctl/install.sh (100%) rename {src => archive/src}/istioctl/library_scripts.sh (100%) rename {test => archive/test}/istioctl/scenarios.json (100%) rename {test => archive/test}/istioctl/test_defaults_debian.sh (100%) diff --git a/src/istioctl/README.md b/archive/src/istioctl/README.md similarity index 100% rename from src/istioctl/README.md rename to archive/src/istioctl/README.md diff --git a/src/istioctl/devcontainer-feature.json b/archive/src/istioctl/devcontainer-feature.json similarity index 100% rename from src/istioctl/devcontainer-feature.json rename to archive/src/istioctl/devcontainer-feature.json diff --git a/src/istioctl/install.sh b/archive/src/istioctl/install.sh similarity index 100% rename from src/istioctl/install.sh rename to archive/src/istioctl/install.sh diff --git a/src/istioctl/library_scripts.sh b/archive/src/istioctl/library_scripts.sh similarity index 100% rename from src/istioctl/library_scripts.sh rename to archive/src/istioctl/library_scripts.sh diff --git a/test/istioctl/scenarios.json b/archive/test/istioctl/scenarios.json similarity index 100% rename from test/istioctl/scenarios.json rename to archive/test/istioctl/scenarios.json diff --git a/test/istioctl/test_defaults_debian.sh b/archive/test/istioctl/test_defaults_debian.sh similarity index 100% rename from test/istioctl/test_defaults_debian.sh rename to archive/test/istioctl/test_defaults_debian.sh From 456e220c40d4d7e44202609eb6c726a54e0a7c95 Mon Sep 17 00:00:00 2001 From: patrickcrocker Date: Fri, 7 Nov 2025 15:26:59 +0000 Subject: [PATCH 12/71] fix(btm): unarchived btm feature Fixes #15 --- {archive/src => src}/btm/README.md | 0 {archive/src => src}/btm/devcontainer-feature.json | 0 {archive/src => src}/btm/install.sh | 0 {archive/src => src}/btm/library_scripts.sh | 0 {archive/test => test}/btm/scenarios.json | 0 {archive/test => test}/btm/test_defaults_debian.sh | 0 {archive/test => test}/btm/test_specific_version.sh | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {archive/src => src}/btm/README.md (100%) rename {archive/src => src}/btm/devcontainer-feature.json (100%) rename {archive/src => src}/btm/install.sh (100%) rename {archive/src => src}/btm/library_scripts.sh (100%) rename {archive/test => test}/btm/scenarios.json (100%) rename {archive/test => test}/btm/test_defaults_debian.sh (100%) rename {archive/test => test}/btm/test_specific_version.sh (100%) diff --git a/archive/src/btm/README.md b/src/btm/README.md similarity index 100% rename from archive/src/btm/README.md rename to src/btm/README.md diff --git a/archive/src/btm/devcontainer-feature.json b/src/btm/devcontainer-feature.json similarity index 100% rename from archive/src/btm/devcontainer-feature.json rename to src/btm/devcontainer-feature.json diff --git a/archive/src/btm/install.sh b/src/btm/install.sh similarity index 100% rename from archive/src/btm/install.sh rename to src/btm/install.sh diff --git a/archive/src/btm/library_scripts.sh b/src/btm/library_scripts.sh similarity index 100% rename from archive/src/btm/library_scripts.sh rename to src/btm/library_scripts.sh diff --git a/archive/test/btm/scenarios.json b/test/btm/scenarios.json similarity index 100% rename from archive/test/btm/scenarios.json rename to test/btm/scenarios.json diff --git a/archive/test/btm/test_defaults_debian.sh b/test/btm/test_defaults_debian.sh similarity index 100% rename from archive/test/btm/test_defaults_debian.sh rename to test/btm/test_defaults_debian.sh diff --git a/archive/test/btm/test_specific_version.sh b/test/btm/test_specific_version.sh similarity index 100% rename from archive/test/btm/test_specific_version.sh rename to test/btm/test_specific_version.sh From 09884827462bead61fa1ecd078512357cf76aea9 Mon Sep 17 00:00:00 2001 From: patrickcrocker Date: Sat, 8 Nov 2025 04:32:28 +0000 Subject: [PATCH 13/71] feat: add uv-tool feature for installing Python tools via uv Supports options: package, from, with, with-executables-from, python, and uv_version to provide full uv tool install functionality. Closes #162 --- src/uv-tool/devcontainer-feature.json | 42 +++++ src/uv-tool/install.sh | 79 ++++++++ src/uv-tool/library_scripts.sh | 173 ++++++++++++++++++ test/uv-tool/install_tool_using_from.sh | 9 + .../install_tool_using_multiple_with.sh | 11 ++ test/uv-tool/install_tool_using_uv_version.sh | 10 + test/uv-tool/install_tool_using_with.sh | 9 + ..._using_with_executables_from_and_python.sh | 13 ++ test/uv-tool/scenarios.json | 48 +++++ 9 files changed, 394 insertions(+) create mode 100644 src/uv-tool/devcontainer-feature.json create mode 100755 src/uv-tool/install.sh create mode 100644 src/uv-tool/library_scripts.sh create mode 100644 test/uv-tool/install_tool_using_from.sh create mode 100644 test/uv-tool/install_tool_using_multiple_with.sh create mode 100644 test/uv-tool/install_tool_using_uv_version.sh create mode 100644 test/uv-tool/install_tool_using_with.sh create mode 100644 test/uv-tool/install_tool_using_with_executables_from_and_python.sh create mode 100644 test/uv-tool/scenarios.json diff --git a/src/uv-tool/devcontainer-feature.json b/src/uv-tool/devcontainer-feature.json new file mode 100644 index 000000000..2af62076b --- /dev/null +++ b/src/uv-tool/devcontainer-feature.json @@ -0,0 +1,42 @@ +{ + "id": "uv-tool", + "version": "1.0.0", + "name": "Python tool (via uv)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/uv-tool", + "description": "Installs a Python tool using uv.", + "options": { + "uv_version": { + "default": "latest", + "description": "The version of uv to install. Set to 'latest' to always get the latest version.", + "type": "string" + }, + "package": { + "default": "", + "description": "The package to install commands from.", + "type": "string" + }, + "from": { + "default": "", + "description": "Use the given package to provide the command. By default, the package name is assumed to match the command name.", + "type": "string" + }, + "with": { + "default": "", + "description": "Space-separated list of additional packages to include as dependencies (does not install their executables). Supports version constraints, e.g., 'mkdocs-material mkdocs-minify-plugin>=0.7,<0.8'", + "type": "string" + }, + "with-executables-from": { + "default": "", + "description": "Comma-separated list of packages to include as dependencies and install their executables, e.g., 'ansible-core,ansible-lint'", + "type": "string" + }, + "python": { + "default": "", + "description": "Specify the Python version to use.", + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-extra/features/uv" + ] +} diff --git a/src/uv-tool/install.sh b/src/uv-tool/install.sh new file mode 100755 index 000000000..0f1782401 --- /dev/null +++ b/src/uv-tool/install.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +set -e + +UV_VERSION="${UV_VERSION:-"latest"}" +PACKAGE="${PACKAGE:-""}" +FROM="${FROM:-""}" +WITH="${WITH:-""}" +WITH_EXECUTABLES_FROM="${WITH_EXECUTABLES_FROM:-""}" +PYTHON="${PYTHON:-""}" + +USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}" + +# Validate required parameters +if [ -z "${PACKAGE}" ]; then + echo "Error: PACKAGE option is required" + exit 1 +fi + +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" + +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/uv:1" \ + --option version="$UV_VERSION" + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + possible_users=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for current_user in "${possible_users[@]}"; do + if id -u "${current_user}" > /dev/null 2>&1; then + USERNAME=${current_user} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u "${USERNAME}" > /dev/null 2>&1; then + USERNAME=root +fi + +uv_args=("${PACKAGE}") + +[ -n "${FROM}" ] && uv_args+=(--from "${FROM}") +[ -n "${WITH_EXECUTABLES_FROM}" ] && uv_args+=(--with-executables-from "${WITH_EXECUTABLES_FROM}") +[ -n "${PYTHON}" ] && uv_args+=(--python "${PYTHON}") + +# Convert space-delimited WITH packages to multiple --with flags +if [ -n "${WITH}" ]; then + read -ra with_array <<<"${WITH}" + for pkg in "${with_array[@]}"; do + [ -n "$pkg" ] && uv_args+=(--with "$pkg") + done +fi + +if [ "${USERNAME}" != "root" ] && [ "${USERNAME}" != "" ]; then + echo "Installing ${PACKAGE} for user ${USERNAME}..." + # Export the uv_args array as a properly quoted string + uv_cmd="uv tool install $(printf '%q ' "${uv_args[@]}")" + sudo -u "${USERNAME}" bash -c " + export PATH=\"/home/${USERNAME}/.local/bin:\$PATH\" + ${uv_cmd} + " +else + echo "Installing ${PACKAGE} for root..." + uv tool install "${uv_args[@]}" +fi + +echo 'Done!' diff --git a/src/uv-tool/library_scripts.sh b/src/uv-tool/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/uv-tool/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/uv-tool/install_tool_using_from.sh b/test/uv-tool/install_tool_using_from.sh new file mode 100644 index 000000000..1ccff31af --- /dev/null +++ b/test/uv-tool/install_tool_using_from.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "httpie is installed" http --version + +reportResults diff --git a/test/uv-tool/install_tool_using_multiple_with.sh b/test/uv-tool/install_tool_using_multiple_with.sh new file mode 100644 index 000000000..5bc9199a5 --- /dev/null +++ b/test/uv-tool/install_tool_using_multiple_with.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "mkdocs is installed" mkdocs --version +check "mkdocs-material theme is available" sh -c "cd $(uv tool dir)/mkdocs && uv pip list | grep mkdocs-material" +check "mkdocs-minify-plugin is available" sh -c "cd $(uv tool dir)/mkdocs && uv pip list | grep mkdocs-minify-plugin" + +reportResults diff --git a/test/uv-tool/install_tool_using_uv_version.sh b/test/uv-tool/install_tool_using_uv_version.sh new file mode 100644 index 000000000..a29cc784d --- /dev/null +++ b/test/uv-tool/install_tool_using_uv_version.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "uv tool version" sh -c "uv --version | grep 0.9.6" +check "httpie is installed" http --version + +reportResults diff --git a/test/uv-tool/install_tool_using_with.sh b/test/uv-tool/install_tool_using_with.sh new file mode 100644 index 000000000..2c8171f04 --- /dev/null +++ b/test/uv-tool/install_tool_using_with.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "mkdocs-material theme is available" sh -c "cd $(uv tool dir)/mkdocs && uv pip list | grep mkdocs-material" + +reportResults diff --git a/test/uv-tool/install_tool_using_with_executables_from_and_python.sh b/test/uv-tool/install_tool_using_with_executables_from_and_python.sh new file mode 100644 index 000000000..863dabb4f --- /dev/null +++ b/test/uv-tool/install_tool_using_with_executables_from_and_python.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ansible is installed" ansible --version +check "ansible-core executables are available" ansible-playbook --version +check "ansible-lint is available" ansible-lint --version + +check "ansible uses Python 3.13" sh -c "ansible --version | grep 'python version = 3.13'" + +reportResults diff --git a/test/uv-tool/scenarios.json b/test/uv-tool/scenarios.json new file mode 100644 index 000000000..aa7425c9a --- /dev/null +++ b/test/uv-tool/scenarios.json @@ -0,0 +1,48 @@ +{ + "install_tool_using_uv_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "uv-tool": { + "uv_version": "0.9.6", + "package": "httpie" + } + } + }, + "install_tool_using_from": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "uv-tool": { + "package": "httpie", + "from": "git+https://github.com/httpie/cli" + } + } + }, + "install_tool_using_with": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "uv-tool": { + "package": "mkdocs", + "with": "mkdocs-material" + } + } + }, + "install_tool_using_multiple_with": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "uv-tool": { + "package": "mkdocs", + "with": "mkdocs-material mkdocs-minify-plugin" + } + } + }, + "install_tool_using_with_executables_from_and_python": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "uv-tool": { + "package": "ansible", + "with-executables-from": "ansible-core,ansible-lint", + "python": "3.13" + } + } + } +} From f64f7dfa3e49803e999ffeb8b238c31d1b6d1642 Mon Sep 17 00:00:00 2001 From: Carsten Buchberger Date: Wed, 19 Nov 2025 09:56:56 +0000 Subject: [PATCH 14/71] feat(age-keygen): (Re-)Adds age-keygen --- {archive/src => src}/age-keygen/README.md | 0 .../src => src}/age-keygen/devcontainer-feature.json | 2 +- {archive/src => src}/age-keygen/install.sh | 9 ++------- {archive/src => src}/age-keygen/library_scripts.sh | 0 {archive/test => test}/age-keygen/scenarios.json | 0 .../test => test}/age-keygen/test_defaults_debian.sh | 0 6 files changed, 3 insertions(+), 8 deletions(-) rename {archive/src => src}/age-keygen/README.md (100%) rename {archive/src => src}/age-keygen/devcontainer-feature.json (95%) rename {archive/src => src}/age-keygen/install.sh (83%) rename {archive/src => src}/age-keygen/library_scripts.sh (100%) rename {archive/test => test}/age-keygen/scenarios.json (100%) rename {archive/test => test}/age-keygen/test_defaults_debian.sh (100%) diff --git a/archive/src/age-keygen/README.md b/src/age-keygen/README.md similarity index 100% rename from archive/src/age-keygen/README.md rename to src/age-keygen/README.md diff --git a/archive/src/age-keygen/devcontainer-feature.json b/src/age-keygen/devcontainer-feature.json similarity index 95% rename from archive/src/age-keygen/devcontainer-feature.json rename to src/age-keygen/devcontainer-feature.json index b40ee2be1..58cea6e48 100644 --- a/archive/src/age-keygen/devcontainer-feature.json +++ b/src/age-keygen/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "age-keygen", - "version": "1.0.15", + "version": "1.0.16", "name": "age-keygen (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/age-keygen", "description": "age-keygen generate a key pair for use with age.", diff --git a/archive/src/age-keygen/install.sh b/src/age-keygen/install.sh similarity index 83% rename from archive/src/age-keygen/install.sh rename to src/age-keygen/install.sh index 2a0e98201..3e8c94a30 100755 --- a/archive/src/age-keygen/install.sh +++ b/src/age-keygen/install.sh @@ -1,4 +1,3 @@ - set -e . ./library_scripts.sh @@ -8,16 +7,12 @@ set -e # `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.4" - +ensure_nanolayer nanolayer_location "v0.5.6" $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/gh-release:1.0.25" \ + "ghcr.io/devcontainers-extra/features/gh-release:1.0.26" \ --option repo='filosottile/age' --option binaryNames='age-keygen' --option version="$VERSION" - - echo 'Done!' - diff --git a/archive/src/age-keygen/library_scripts.sh b/src/age-keygen/library_scripts.sh similarity index 100% rename from archive/src/age-keygen/library_scripts.sh rename to src/age-keygen/library_scripts.sh diff --git a/archive/test/age-keygen/scenarios.json b/test/age-keygen/scenarios.json similarity index 100% rename from archive/test/age-keygen/scenarios.json rename to test/age-keygen/scenarios.json diff --git a/archive/test/age-keygen/test_defaults_debian.sh b/test/age-keygen/test_defaults_debian.sh similarity index 100% rename from archive/test/age-keygen/test_defaults_debian.sh rename to test/age-keygen/test_defaults_debian.sh From 3571c1eb5d3386048e14267d36b728a989900632 Mon Sep 17 00:00:00 2001 From: leonbli Date: Wed, 10 Dec 2025 18:35:20 +0800 Subject: [PATCH 15/71] feat(codebuddy-code): init --- src/codebuddy-code/README.md | 23 +++ src/codebuddy-code/devcontainer-feature.json | 18 ++ src/codebuddy-code/install.sh | 21 +++ src/codebuddy-code/library_scripts.sh | 173 +++++++++++++++++++ test/codebuddy-code/scenarios.json | 16 ++ test/codebuddy-code/test.sh | 9 + test/codebuddy-code/test_debian.sh | 9 + test/codebuddy-code/test_specific_version.sh | 9 + 8 files changed, 278 insertions(+) create mode 100644 src/codebuddy-code/README.md create mode 100644 src/codebuddy-code/devcontainer-feature.json create mode 100755 src/codebuddy-code/install.sh create mode 100644 src/codebuddy-code/library_scripts.sh create mode 100644 test/codebuddy-code/scenarios.json create mode 100755 test/codebuddy-code/test.sh create mode 100755 test/codebuddy-code/test_debian.sh create mode 100755 test/codebuddy-code/test_specific_version.sh diff --git a/src/codebuddy-code/README.md b/src/codebuddy-code/README.md new file mode 100644 index 000000000..e2458e3c9 --- /dev/null +++ b/src/codebuddy-code/README.md @@ -0,0 +1,23 @@ + +# codebuddy-code (via npm) (codebuddy-code) + +CodeBuddy Code is an agentic coding tool that lives in your terminal + +## Example Usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/codebuddy-code:1": {} +} +``` + +## Options + +| 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/codebuddy-code/devcontainer-feature.json b/src/codebuddy-code/devcontainer-feature.json new file mode 100644 index 000000000..d283580ec --- /dev/null +++ b/src/codebuddy-code/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "codebuddy-code", + "version": "1.0.0", + "name": "CodeBuddy Code (via npm)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/codebuddy-code", + "description": "CodeBuddy Code is an agentic coding tool that lives in your terminal", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} \ No newline at end of file diff --git a/src/codebuddy-code/install.sh b/src/codebuddy-code/install.sh new file mode 100755 index 000000000..fb77e2a9c --- /dev/null +++ b/src/codebuddy-code/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/npm-package:1.0.4" \ + --option package='@tencent-ai/codebuddy-code' --option version="$VERSION" + +echo 'Done!' diff --git a/src/codebuddy-code/library_scripts.sh b/src/codebuddy-code/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/codebuddy-code/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/codebuddy-code/scenarios.json b/test/codebuddy-code/scenarios.json new file mode 100644 index 000000000..2e9e50421 --- /dev/null +++ b/test/codebuddy-code/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "codebuddy-code": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "codebuddy-code": { + "version": "2.16.0" + } + } + } +} \ No newline at end of file diff --git a/test/codebuddy-code/test.sh b/test/codebuddy-code/test.sh new file mode 100755 index 000000000..89f009cc6 --- /dev/null +++ b/test/codebuddy-code/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "CodeBuddy is installed" codebuddy --version + +reportResults diff --git a/test/codebuddy-code/test_debian.sh b/test/codebuddy-code/test_debian.sh new file mode 100755 index 000000000..89f009cc6 --- /dev/null +++ b/test/codebuddy-code/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "CodeBuddy is installed" codebuddy --version + +reportResults diff --git a/test/codebuddy-code/test_specific_version.sh b/test/codebuddy-code/test_specific_version.sh new file mode 100755 index 000000000..3f66754bf --- /dev/null +++ b/test/codebuddy-code/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "CodeBuddy version is equal to 2.16.0" sh -c "codebuddy --version | grep '2.16.0'" + +reportResults From 0f3cf8d0b989d405e2901a91fc59609f81b6acdf Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sat, 13 Dec 2025 09:19:33 +0000 Subject: [PATCH 16/71] ci: add archive feature --- .github/workflows/archive-feature.yaml | 50 ++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/archive-feature.yaml diff --git a/.github/workflows/archive-feature.yaml b/.github/workflows/archive-feature.yaml new file mode 100644 index 000000000..d7c27578e --- /dev/null +++ b/.github/workflows/archive-feature.yaml @@ -0,0 +1,50 @@ +name: Archive Feature + +on: + workflow_dispatch: + inputs: + feature_name: + description: 'Name of the feature to archive' + required: true + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + archive: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install just + uses: extractions/setup-just@v3 + + - name: Create branch for archiving + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b archive/${{ inputs.feature_name }} + + - name: Archive feature + run: just archive ${{ inputs.feature_name }} + + - name: Commit changes + run: | + git add -A + git commit -m "chore: archive feature ${{ inputs.feature_name }}" + + - name: Push changes + run: git push origin archive/${{ inputs.feature_name }} + + - name: Create Pull Request + env: + GH_TOKEN: ${{ github. token }} + run: | + gh pr create \ + --title "Archive feature: ${{ inputs.feature_name }}" \ + --body "This PR archives the \`${{ inputs.feature_name }}\` feature by moving it to the archive directory." \ + --base main \ + --head archive/${{ inputs.feature_name }} From b8688b111c3ec9eae3589f725b96c43d386f1735 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sat, 13 Dec 2025 10:31:50 +0100 Subject: [PATCH 17/71] ci: use token for PR creation --- .github/workflows/archive-feature.yaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/archive-feature.yaml b/.github/workflows/archive-feature.yaml index d7c27578e..56cc8c65d 100644 --- a/.github/workflows/archive-feature.yaml +++ b/.github/workflows/archive-feature.yaml @@ -8,16 +8,14 @@ on: required: true type: string -permissions: - contents: write - pull-requests: write - jobs: archive: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 + with: + token: ${{ secrets.ARCHIVE_TOKEN }} - name: Install just uses: extractions/setup-just@v3 @@ -41,7 +39,7 @@ jobs: - name: Create Pull Request env: - GH_TOKEN: ${{ github. token }} + GH_TOKEN: ${{ secrets.ARCHIVE_TOKEN }} run: | gh pr create \ --title "Archive feature: ${{ inputs.feature_name }}" \ From 8b6a6bf8e91cc90e7fdce9b28890b91ed7da5426 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 Dec 2025 09:34:31 +0000 Subject: [PATCH 18/71] chore: archive feature elasticsearch-asdf --- {src => archive/src}/elasticsearch-asdf/README.md | 0 {src => archive/src}/elasticsearch-asdf/devcontainer-feature.json | 0 {src => archive/src}/elasticsearch-asdf/install.sh | 0 {src => archive/src}/elasticsearch-asdf/library_scripts.sh | 0 {test => archive/test}/elasticsearch-asdf/scenarios.json | 0 {test => archive/test}/elasticsearch-asdf/test.sh | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {src => archive/src}/elasticsearch-asdf/README.md (100%) rename {src => archive/src}/elasticsearch-asdf/devcontainer-feature.json (100%) rename {src => archive/src}/elasticsearch-asdf/install.sh (100%) rename {src => archive/src}/elasticsearch-asdf/library_scripts.sh (100%) rename {test => archive/test}/elasticsearch-asdf/scenarios.json (100%) rename {test => archive/test}/elasticsearch-asdf/test.sh (100%) diff --git a/src/elasticsearch-asdf/README.md b/archive/src/elasticsearch-asdf/README.md similarity index 100% rename from src/elasticsearch-asdf/README.md rename to archive/src/elasticsearch-asdf/README.md diff --git a/src/elasticsearch-asdf/devcontainer-feature.json b/archive/src/elasticsearch-asdf/devcontainer-feature.json similarity index 100% rename from src/elasticsearch-asdf/devcontainer-feature.json rename to archive/src/elasticsearch-asdf/devcontainer-feature.json diff --git a/src/elasticsearch-asdf/install.sh b/archive/src/elasticsearch-asdf/install.sh similarity index 100% rename from src/elasticsearch-asdf/install.sh rename to archive/src/elasticsearch-asdf/install.sh diff --git a/src/elasticsearch-asdf/library_scripts.sh b/archive/src/elasticsearch-asdf/library_scripts.sh similarity index 100% rename from src/elasticsearch-asdf/library_scripts.sh rename to archive/src/elasticsearch-asdf/library_scripts.sh diff --git a/test/elasticsearch-asdf/scenarios.json b/archive/test/elasticsearch-asdf/scenarios.json similarity index 100% rename from test/elasticsearch-asdf/scenarios.json rename to archive/test/elasticsearch-asdf/scenarios.json diff --git a/test/elasticsearch-asdf/test.sh b/archive/test/elasticsearch-asdf/test.sh similarity index 100% rename from test/elasticsearch-asdf/test.sh rename to archive/test/elasticsearch-asdf/test.sh From 18255bcdbb6315f353d32fcc49f61cbe5d3c1807 Mon Sep 17 00:00:00 2001 From: Benjamin Etheredge Date: Sat, 20 Dec 2025 01:16:18 -0600 Subject: [PATCH 19/71] fix(talosctl): filter out bundles --- src/talosctl/devcontainer-feature.json | 2 +- src/talosctl/install.sh | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/talosctl/devcontainer-feature.json b/src/talosctl/devcontainer-feature.json index 25d9c6134..7d7e070c9 100644 --- a/src/talosctl/devcontainer-feature.json +++ b/src/talosctl/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "talosctl", - "version": "1.0.0", + "version": "1.0.1", "name": "talosctl (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/talosctl", "description": "A CLI tool that interfaces with the Talos Linux API.", diff --git a/src/talosctl/install.sh b/src/talosctl/install.sh index 70de85293..fd7dfe95a 100644 --- a/src/talosctl/install.sh +++ b/src/talosctl/install.sh @@ -16,6 +16,10 @@ $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1" \ - --option repo='siderolabs/talos' --option binaryNames='talosctl' --option version="$VERSION" --option releaseTagRegex='^(?!.*(alpha|beta)).*$' + --option repo='siderolabs/talos' \ + --option binaryNames='talosctl' \ + --option assetRegex='^.*(? Date: Sat, 20 Dec 2025 15:46:48 +0000 Subject: [PATCH 20/71] feat(boilerplate): init --- src/boilerplate/devcontainer-feature.json | 18 +++ src/boilerplate/install.sh | 21 +++ src/boilerplate/library_scripts.sh | 173 ++++++++++++++++++++++ test/boilerplate/scenarios.json | 16 ++ test/boilerplate/test.sh | 9 ++ test/boilerplate/test_debian.sh | 9 ++ test/boilerplate/test_specific_version.sh | 9 ++ 7 files changed, 255 insertions(+) create mode 100644 src/boilerplate/devcontainer-feature.json create mode 100755 src/boilerplate/install.sh create mode 100644 src/boilerplate/library_scripts.sh create mode 100644 test/boilerplate/scenarios.json create mode 100755 test/boilerplate/test.sh create mode 100755 test/boilerplate/test_debian.sh create mode 100755 test/boilerplate/test_specific_version.sh diff --git a/src/boilerplate/devcontainer-feature.json b/src/boilerplate/devcontainer-feature.json new file mode 100644 index 000000000..78b2f99a2 --- /dev/null +++ b/src/boilerplate/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "boilerplate", + "version": "1.0.0", + "name": "boilerplate (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/boilerplate", + "description": "A tool for generating files and folders (boilerplate) from a set of templates", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/boilerplate/install.sh b/src/boilerplate/install.sh new file mode 100755 index 000000000..bedab30bb --- /dev/null +++ b/src/boilerplate/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='gruntwork-io/boilerplate' --option binaryNames='boilerplate' --option version="$VERSION" + +echo 'Done!' diff --git a/src/boilerplate/library_scripts.sh b/src/boilerplate/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/boilerplate/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/boilerplate/scenarios.json b/test/boilerplate/scenarios.json new file mode 100644 index 000000000..facce5d10 --- /dev/null +++ b/test/boilerplate/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "boilerplate": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "boilerplate": { + "version": "0.10.0" + } + } + } +} diff --git a/test/boilerplate/test.sh b/test/boilerplate/test.sh new file mode 100755 index 000000000..c6f6a6404 --- /dev/null +++ b/test/boilerplate/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "boilerplate is installed" boilerplate --version + +reportResults diff --git a/test/boilerplate/test_debian.sh b/test/boilerplate/test_debian.sh new file mode 100755 index 000000000..c6f6a6404 --- /dev/null +++ b/test/boilerplate/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "boilerplate is installed" boilerplate --version + +reportResults diff --git a/test/boilerplate/test_specific_version.sh b/test/boilerplate/test_specific_version.sh new file mode 100755 index 000000000..fc712fba7 --- /dev/null +++ b/test/boilerplate/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "boilerplate version is equal to 0.10.0" sh -c "boilerplate --version | grep '0.10.0'" + +reportResults From da84b3168da99c1e0206968bf7895f6ed2263871 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sat, 20 Dec 2025 16:05:49 +0000 Subject: [PATCH 21/71] feat: add gh-release template --- .../{ => bak}/src/devcontainer-feature.json | 0 templates/{ => bak}/src/install.sh | 0 templates/{ => bak}/src/library_scripts.sh | 0 templates/bak/test/scenarios.json | 16 ++ templates/bak/test/test.sh | 9 + templates/bak/test/test_debian.sh | 9 + templates/bak/test/test_specific_version.sh | 9 + templates/gh-release/boilerplate.yml | 19 ++ .../gh-release/devcontainer-feature.json | 18 ++ templates/gh-release/install.sh | 21 +++ templates/gh-release/library_scripts.sh | 173 ++++++++++++++++++ templates/test/boilerplate.yml | 13 ++ templates/test/scenarios.json | 8 +- templates/test/test.sh | 13 +- templates/test/test_debian.sh | 12 +- templates/test/test_specific_version.sh | 14 +- 16 files changed, 327 insertions(+), 7 deletions(-) rename templates/{ => bak}/src/devcontainer-feature.json (100%) rename templates/{ => bak}/src/install.sh (100%) rename templates/{ => bak}/src/library_scripts.sh (100%) create mode 100644 templates/bak/test/scenarios.json create mode 100755 templates/bak/test/test.sh create mode 100755 templates/bak/test/test_debian.sh create mode 100755 templates/bak/test/test_specific_version.sh create mode 100644 templates/gh-release/boilerplate.yml create mode 100644 templates/gh-release/devcontainer-feature.json create mode 100755 templates/gh-release/install.sh create mode 100644 templates/gh-release/library_scripts.sh create mode 100644 templates/test/boilerplate.yml diff --git a/templates/src/devcontainer-feature.json b/templates/bak/src/devcontainer-feature.json similarity index 100% rename from templates/src/devcontainer-feature.json rename to templates/bak/src/devcontainer-feature.json diff --git a/templates/src/install.sh b/templates/bak/src/install.sh similarity index 100% rename from templates/src/install.sh rename to templates/bak/src/install.sh diff --git a/templates/src/library_scripts.sh b/templates/bak/src/library_scripts.sh similarity index 100% rename from templates/src/library_scripts.sh rename to templates/bak/src/library_scripts.sh diff --git a/templates/bak/test/scenarios.json b/templates/bak/test/scenarios.json new file mode 100644 index 000000000..48ca39c54 --- /dev/null +++ b/templates/bak/test/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "": { + "version": "x.y.z" + } + } + } +} \ No newline at end of file diff --git a/templates/bak/test/test.sh b/templates/bak/test/test.sh new file mode 100755 index 000000000..b2873a73c --- /dev/null +++ b/templates/bak/test/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something is installed" something --version + +reportResults diff --git a/templates/bak/test/test_debian.sh b/templates/bak/test/test_debian.sh new file mode 100755 index 000000000..b2873a73c --- /dev/null +++ b/templates/bak/test/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something is installed" something --version + +reportResults diff --git a/templates/bak/test/test_specific_version.sh b/templates/bak/test/test_specific_version.sh new file mode 100755 index 000000000..e4858fa21 --- /dev/null +++ b/templates/bak/test/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something version is equal to x.y.z" sh -c "something --version | grep 'x.y.z'" + +reportResults diff --git a/templates/gh-release/boilerplate.yml b/templates/gh-release/boilerplate.yml new file mode 100644 index 000000000..16ec14aa3 --- /dev/null +++ b/templates/gh-release/boilerplate.yml @@ -0,0 +1,19 @@ +variables: + - name: ID + description: The unique identifier for the feature (e.g., akamai-cli). + + - name: Description + description: A brief description of the feature. + + - name: Repository + description: The GitHub repository in the format "owner/repo" (e.g., akamai/cli). + + - name: BinaryNames + description: The names of the binaries to install from the release (comma-separated if multiple). + + # - name: BinaryVersionCommands + # description: The commands to check the version of the installed binaries (comma-separated if multiple). + # default: "--version" + + # - name: BinaryNonLatestVersions + # description: Specific non-latest versions for test scenarios (comma-separated if multiple). For example, if 1.2.3 is the latest version, you should use 1.2.2 here. diff --git a/templates/gh-release/devcontainer-feature.json b/templates/gh-release/devcontainer-feature.json new file mode 100644 index 000000000..466db7e1e --- /dev/null +++ b/templates/gh-release/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "{{.ID}}", + "version": "1.0.0", + "name": "{{.ID}} (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/{{.ID}}", + "description": "{{.Description}}", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/templates/gh-release/install.sh b/templates/gh-release/install.sh new file mode 100755 index 000000000..ff1c69aeb --- /dev/null +++ b/templates/gh-release/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='{{.Repository}}' --option binaryNames='{{.BinaryNames}}' --option version="$VERSION" + +echo 'Done!' diff --git a/templates/gh-release/library_scripts.sh b/templates/gh-release/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/templates/gh-release/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/templates/test/boilerplate.yml b/templates/test/boilerplate.yml new file mode 100644 index 000000000..fe68ad94c --- /dev/null +++ b/templates/test/boilerplate.yml @@ -0,0 +1,13 @@ +variables: + - name: ID + description: The unique identifier for the feature (e.g., akamai-cli). + + - name: BinaryNames + description: The names of the binaries to install from the release (comma-separated if multiple). + + - name: BinaryVersionCommands + description: The commands to check the version of the installed binaries (comma-separated if multiple). + default: "--version" + + - name: BinaryNonLatestVersions + description: Specific non-latest versions for test scenarios (comma-separated if multiple). For example, if 1.2.3 is the latest version, you should use 1.2.2 here. diff --git a/templates/test/scenarios.json b/templates/test/scenarios.json index 48ca39c54..283d4b0ab 100644 --- a/templates/test/scenarios.json +++ b/templates/test/scenarios.json @@ -2,15 +2,15 @@ "test_debian": { "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { - "": {} + "{{.ID}}": {} } }, "test_specific_version": { "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { - "": { - "version": "x.y.z" + "{{.ID}}": { + "version": "{{index (splitList "," .BinaryNonLatestVersions) 0}}" } } } -} \ No newline at end of file +} diff --git a/templates/test/test.sh b/templates/test/test.sh index b2873a73c..f5101fa3f 100755 --- a/templates/test/test.sh +++ b/templates/test/test.sh @@ -4,6 +4,17 @@ set -e source dev-container-features-test-lib -check "something is installed" something --version +{{- $binaryNames := splitList "," .BinaryNames}} +{{- $versionCommands := splitList "," .BinaryVersionCommands}} +{{- range $index, $binary := $binaryNames}} +{{- $versionCmd := "--version"}} +{{- if gt (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands $index}} +{{- else if eq (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands 0}} +{{- end}} + +check "{{$binary}} is installed" {{$binary}} {{$versionCmd}} +{{- end}} reportResults diff --git a/templates/test/test_debian.sh b/templates/test/test_debian.sh index b2873a73c..f7780a21d 100755 --- a/templates/test/test_debian.sh +++ b/templates/test/test_debian.sh @@ -3,7 +3,17 @@ set -e source dev-container-features-test-lib +{{- $binaryNames := splitList "," .BinaryNames}} +{{- $versionCommands := splitList "," .BinaryVersionCommands}} +{{- range $index, $binary := $binaryNames}} +{{- $versionCmd := "--version"}} +{{- if gt (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands $index}} +{{- else if eq (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands 0}} +{{- end}} -check "something is installed" something --version +check "{{$binary}} is installed" {{$binary}} {{$versionCmd}} +{{- end}} reportResults diff --git a/templates/test/test_specific_version.sh b/templates/test/test_specific_version.sh index e4858fa21..0108dac96 100755 --- a/templates/test/test_specific_version.sh +++ b/templates/test/test_specific_version.sh @@ -3,7 +3,19 @@ set -e source dev-container-features-test-lib +{{- $binaryNames := splitList "," .BinaryNames}} +{{- $versionCommands := splitList "," .BinaryVersionCommands}} +{{- $nonLatestVersions := splitList "," .BinaryNonLatestVersions}} +{{- range $index, $binary := $binaryNames}} +{{- $versionCmd := "--version"}} +{{- if gt (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands $index}} +{{- else if eq (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands 0}} +{{- end}} +{{- $nonLatestVersion := index $nonLatestVersions $index}} -check "something version is equal to x.y.z" sh -c "something --version | grep 'x.y.z'" +check "{{$binary}} version is equal to {{$nonLatestVersion}}" sh -c "{{$binary}} {{$versionCmd}} | grep '{{$nonLatestVersion}}'" +{{- end}} reportResults From ca2ebda4eb84de4092cdbb79faacb8f4b838baf2 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sat, 20 Dec 2025 16:07:08 +0000 Subject: [PATCH 22/71] ci: generate gh-release based feature with template --- .github/workflows/generate-gh-feature.yml | 134 ++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 .github/workflows/generate-gh-feature.yml diff --git a/.github/workflows/generate-gh-feature.yml b/.github/workflows/generate-gh-feature.yml new file mode 100644 index 000000000..1719d1db5 --- /dev/null +++ b/.github/workflows/generate-gh-feature.yml @@ -0,0 +1,134 @@ +name: Generate Feature from gh-release Template + +on: + workflow_dispatch: + inputs: + ID: + description: 'Feature identifier (e.g., akamai-cli)' + required: true + type: string + Description: + description: 'Feature description' + required: true + type: string + Repository: + description: 'GitHub repository in format "owner/repo" (e.g., akamai/cli)' + required: true + type: string + BinaryNames: + description: 'Binary names (comma-separated if multiple)' + required: true + type: string + BinaryVersionCommands: + description: 'Version check commands (comma-separated if multiple)' + required: false + type: string + default: '--version' + BinaryNonLatestVersions: + description: 'Non-latest versions for testing (comma-separated if multiple)' + required: true + type: string + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ secrets.ARCHIVE_TOKEN }} + + - name: Check if feature already exists + run: | + if [ -d "src/${{ inputs.ID }}" ] || [ -d "test/${{ inputs.ID }}" ]; then + echo "❌ Feature '${{ inputs.ID }}' already exists!" + echo "Please use a different feature ID or remove the existing feature first." + exit 1 + fi + echo "✅ Feature ID is available" + + - name: Validate GitHub repository exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if gh api repos/${{ inputs.Repository }} > /dev/null 2>&1; then + echo "✅ Repository '${{ inputs.Repository }}' exists and is accessible" + else + echo "⚠️ Warning: Could not verify repository '${{ inputs.Repository }}'" + echo "The repository might be private, not exist, or you don't have access to it." + echo "Continuing with feature generation..." + fi + + - name: Download boilerplate + run: | + wget -q https://github.com/gruntwork-io/boilerplate/releases/download/v0.10.1/boilerplate_linux_amd64 + chmod +x boilerplate_linux_amd64 + + - name: Create vars.yml + run: | + cat > vars.yml << 'EOF' + ID: ${{ inputs.ID }} + Description: ${{ inputs.Description }} + Repository: ${{ inputs.Repository }} + BinaryNames: ${{ inputs.BinaryNames }} + BinaryVersionCommands: ${{ inputs.BinaryVersionCommands }} + BinaryNonLatestVersions: ${{ inputs.BinaryNonLatestVersions }} + EOF + echo "📝 Created vars.yml:" + cat vars.yml + + - name: Create branch for new feature + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b feature/${{ inputs.ID }}-init + + - name: Generate feature files + run: | + echo "🔧 Generating feature files from templates/gh-release..." + ./boilerplate_linux_amd64 \ + --template-url templates/gh-release/ \ + --output-folder src/${{ inputs.ID }} \ + --var-file vars.yml \ + --non-interactive + + - name: Generate test files + run: | + echo "🧪 Generating test files from templates/test..." + ./boilerplate_linux_amd64 \ + --template-url templates/test/ \ + --output-folder test/${{ inputs.ID }} \ + --var-file vars.yml \ + --non-interactive + + - name: Commit changes + run: | + git add -A + git commit -m "feat(${{ inputs.ID }}): init + + Generated using boilerplate templates." + + - name: Push changes + run: git push origin feature/${{ inputs.ID }}-init + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.ARCHIVE_TOKEN }} + run: | + gh pr create \ + --title "feat(${{ inputs.ID }}): init" \ + --body "## New Feature: \`${{ inputs.ID }}\` + + **Description:** ${{ inputs.Description }} + + **Repository:** [${{ inputs.Repository }}](https://github.com/${{ inputs.Repository }}) + + **Binaries:** \`${{ inputs.BinaryNames }}\` + + **Version Commands:** \`${{ inputs.BinaryVersionCommands }}\` + + **Test Versions:** \`${{ inputs.BinaryNonLatestVersions }}\` + + --- + + This PR was automatically generated using the boilerplate templates. From f571228986cd5cf7eaa25b8c0fa0a997fc259e33 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sat, 20 Dec 2025 16:08:51 +0000 Subject: [PATCH 23/71] chore: remove comments from gh-release template --- templates/gh-release/boilerplate.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/templates/gh-release/boilerplate.yml b/templates/gh-release/boilerplate.yml index 16ec14aa3..6f1ba7a9e 100644 --- a/templates/gh-release/boilerplate.yml +++ b/templates/gh-release/boilerplate.yml @@ -10,10 +10,3 @@ variables: - name: BinaryNames description: The names of the binaries to install from the release (comma-separated if multiple). - - # - name: BinaryVersionCommands - # description: The commands to check the version of the installed binaries (comma-separated if multiple). - # default: "--version" - - # - name: BinaryNonLatestVersions - # description: Specific non-latest versions for test scenarios (comma-separated if multiple). For example, if 1.2.3 is the latest version, you should use 1.2.2 here. From 0a1d9c050c5beba45b80b0040b02cec2456170f0 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:00:23 +0000 Subject: [PATCH 24/71] ci: create PR body from file for generated feature --- .github/workflows/generate-gh-feature.yml | 28 +++++++++++++++-------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/generate-gh-feature.yml b/.github/workflows/generate-gh-feature.yml index 1719d1db5..bd60d5ed9 100644 --- a/.github/workflows/generate-gh-feature.yml +++ b/.github/workflows/generate-gh-feature.yml @@ -111,24 +111,34 @@ jobs: - name: Push changes run: git push origin feature/${{ inputs.ID }}-init - - name: Create Pull Request - env: - GH_TOKEN: ${{ secrets.ARCHIVE_TOKEN }} + - name: Create PR body file run: | - gh pr create \ - --title "feat(${{ inputs.ID }}): init" \ - --body "## New Feature: \`${{ inputs.ID }}\` + cat > pr_body.md << 'EOF' + ## New Feature: `${{ inputs.ID }}` **Description:** ${{ inputs.Description }} **Repository:** [${{ inputs.Repository }}](https://github.com/${{ inputs.Repository }}) - **Binaries:** \`${{ inputs.BinaryNames }}\` + **Binaries:** `${{ inputs.BinaryNames }}` - **Version Commands:** \`${{ inputs.BinaryVersionCommands }}\` + **Version Commands:** `${{ inputs.BinaryVersionCommands }}` - **Test Versions:** \`${{ inputs.BinaryNonLatestVersions }}\` + **Test Versions:** `${{ inputs.BinaryNonLatestVersions }}` --- This PR was automatically generated using the boilerplate templates. + EOF + echo "📝 PR body content:" + cat pr_body.md + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.ARCHIVE_TOKEN }} + run: | + gh pr create \ + --title "feat(${{ inputs.ID }}): init" \ + --body-file pr_body.md \ + --base main \ + --head feature/${{ inputs.ID }}-init From ade2ee2c0a1d0d992b9a5a8b0da4a3e4eb71db99 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:30:47 +0000 Subject: [PATCH 25/71] ci(generate-gh-feature): only add src and test dir to staged --- .github/workflows/generate-gh-feature.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/generate-gh-feature.yml b/.github/workflows/generate-gh-feature.yml index bd60d5ed9..0b65374ad 100644 --- a/.github/workflows/generate-gh-feature.yml +++ b/.github/workflows/generate-gh-feature.yml @@ -103,7 +103,7 @@ jobs: - name: Commit changes run: | - git add -A + git add src/ test/ git commit -m "feat(${{ inputs.ID }}): init Generated using boilerplate templates." From e90e90f58e41f5382a95b564122243cf2a5138af Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 16:13:51 +0000 Subject: [PATCH 26/71] feat(opencode): init Generated using boilerplate templates. --- src/opencode/devcontainer-feature.json | 18 +++ src/opencode/install.sh | 21 +++ src/opencode/library_scripts.sh | 173 +++++++++++++++++++++++++ test/opencode/scenarios.json | 16 +++ test/opencode/test.sh | 9 ++ test/opencode/test_debian.sh | 9 ++ test/opencode/test_specific_version.sh | 9 ++ vars.yml | 6 + 8 files changed, 261 insertions(+) create mode 100644 src/opencode/devcontainer-feature.json create mode 100755 src/opencode/install.sh create mode 100644 src/opencode/library_scripts.sh create mode 100644 test/opencode/scenarios.json create mode 100755 test/opencode/test.sh create mode 100755 test/opencode/test_debian.sh create mode 100755 test/opencode/test_specific_version.sh create mode 100644 vars.yml diff --git a/src/opencode/devcontainer-feature.json b/src/opencode/devcontainer-feature.json new file mode 100644 index 000000000..bbcfa16b8 --- /dev/null +++ b/src/opencode/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "opencode", + "version": "1.0.0", + "name": "opencode (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/opencode", + "description": "The open source coding agent.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/opencode/install.sh b/src/opencode/install.sh new file mode 100755 index 000000000..6362dfb60 --- /dev/null +++ b/src/opencode/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='sst/opencode' --option binaryNames='opencode' --option version="$VERSION" + +echo 'Done!' diff --git a/src/opencode/library_scripts.sh b/src/opencode/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/opencode/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/opencode/scenarios.json b/test/opencode/scenarios.json new file mode 100644 index 000000000..cfad1a85e --- /dev/null +++ b/test/opencode/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "opencode": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "opencode": { + "version": "1.0.176" + } + } + } +} diff --git a/test/opencode/test.sh b/test/opencode/test.sh new file mode 100755 index 000000000..c10e98aad --- /dev/null +++ b/test/opencode/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opencode is installed" opencode --version + +reportResults diff --git a/test/opencode/test_debian.sh b/test/opencode/test_debian.sh new file mode 100755 index 000000000..c10e98aad --- /dev/null +++ b/test/opencode/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opencode is installed" opencode --version + +reportResults diff --git a/test/opencode/test_specific_version.sh b/test/opencode/test_specific_version.sh new file mode 100755 index 000000000..c374514b2 --- /dev/null +++ b/test/opencode/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opencode version is equal to 1.0.176" sh -c "opencode --version | grep '1.0.176'" + +reportResults diff --git a/vars.yml b/vars.yml new file mode 100644 index 000000000..4040ae38c --- /dev/null +++ b/vars.yml @@ -0,0 +1,6 @@ +ID: opencode +Description: The open source coding agent. +Repository: sst/opencode +BinaryNames: opencode +BinaryVersionCommands: --version +BinaryNonLatestVersions: 1.0.179 From c37e53f0786418b062dedac2ec04339ed97feeed Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:05:20 +0000 Subject: [PATCH 27/71] feat(opencode): filter out assets with baseline in their name --- src/opencode/install.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/opencode/install.sh b/src/opencode/install.sh index 6362dfb60..c47309c16 100755 --- a/src/opencode/install.sh +++ b/src/opencode/install.sh @@ -16,6 +16,7 @@ $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1" \ - --option repo='sst/opencode' --option binaryNames='opencode' --option version="$VERSION" + --option repo='sst/opencode' --option binaryNames='opencode' --option version="$VERSION" \ + --option assetRegex='^(?!.*baseline).*$' echo 'Done!' From dba2bf54e280e41b36c7558837124cf0e9ea8a54 Mon Sep 17 00:00:00 2001 From: Ben Tuttle Date: Mon, 29 Dec 2025 12:20:37 -0700 Subject: [PATCH 28/71] feat(infisical): init --- src/infisical/README.md | 27 ++++ src/infisical/devcontainer-feature.json | 17 +++ src/infisical/install.sh | 21 +++ src/infisical/library_scripts.sh | 173 ++++++++++++++++++++++++ test/infisical/scenarios.json | 16 +++ test/infisical/test.sh | 9 ++ test/infisical/test_debian.sh | 9 ++ test/infisical/test_specific_version.sh | 9 ++ 8 files changed, 281 insertions(+) create mode 100644 src/infisical/README.md create mode 100644 src/infisical/devcontainer-feature.json create mode 100755 src/infisical/install.sh create mode 100644 src/infisical/library_scripts.sh create mode 100644 test/infisical/scenarios.json create mode 100755 test/infisical/test.sh create mode 100755 test/infisical/test_debian.sh create mode 100755 test/infisical/test_specific_version.sh diff --git a/src/infisical/README.md b/src/infisical/README.md new file mode 100644 index 000000000..eebb50384 --- /dev/null +++ b/src/infisical/README.md @@ -0,0 +1,27 @@ +# infisical (via Github Releases) (infisical) + +Infisical is an OSS tool for secret management. + +## Example Usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/infisical:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +| ---------- | ------------------------------ | ------ | ------------- | +| version | Select the version to install. | string | latest | + +## Customizations + +### VS Code Extensions + +- `infisical.vscode-infisical` + +--- + +_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/infisical/devcontainer-feature.json b/src/infisical/devcontainer-feature.json new file mode 100644 index 000000000..2eab61e83 --- /dev/null +++ b/src/infisical/devcontainer-feature.json @@ -0,0 +1,17 @@ +{ + "id": "infisical", + "version": "1.0.0", + "name": "infisical (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/infisical", + "description": "The official Infisical CLI: Inject secrets into applications and manage your Infisical infrastructure.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": ["latest"], + "type": "string" + } + }, + "installsAfter": [] +} + diff --git a/src/infisical/install.sh b/src/infisical/install.sh new file mode 100755 index 000000000..768859afb --- /dev/null +++ b/src/infisical/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='infisical/cli' --option binaryNames='infisical' --option version="$VERSION" + +echo 'Done!' diff --git a/src/infisical/library_scripts.sh b/src/infisical/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/infisical/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/infisical/scenarios.json b/test/infisical/scenarios.json new file mode 100644 index 000000000..3c0188728 --- /dev/null +++ b/test/infisical/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "infisical": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "infisical": { + "version": "0.43.45" + } + } + } +} diff --git a/test/infisical/test.sh b/test/infisical/test.sh new file mode 100755 index 000000000..e37186ff3 --- /dev/null +++ b/test/infisical/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "infisical is installed" infisical --version + +reportResults diff --git a/test/infisical/test_debian.sh b/test/infisical/test_debian.sh new file mode 100755 index 000000000..e37186ff3 --- /dev/null +++ b/test/infisical/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "infisical is installed" infisical --version + +reportResults diff --git a/test/infisical/test_specific_version.sh b/test/infisical/test_specific_version.sh new file mode 100755 index 000000000..48f0ce71f --- /dev/null +++ b/test/infisical/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "infisical version is equal to 0.43.45" sh -c "infisical --version | grep '0.43.45'" + +reportResults From cb278f7d8869c169a7d4c1ec25964c9b890ea06e Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sun, 4 Jan 2026 08:40:21 +0000 Subject: [PATCH 29/71] fix(terragrunt): filter out additional assets --- src/terragrunt/devcontainer-feature.json | 2 +- src/terragrunt/install.sh | 5 ++--- test/terragrunt/scenarios.json | 16 ++++++++++++++++ test/terragrunt/test_specific_version.sh | 10 ++++++++++ ...test_specific_version_before_assets_change.sh | 10 ++++++++++ 5 files changed, 39 insertions(+), 4 deletions(-) create mode 100755 test/terragrunt/test_specific_version.sh create mode 100755 test/terragrunt/test_specific_version_before_assets_change.sh diff --git a/src/terragrunt/devcontainer-feature.json b/src/terragrunt/devcontainer-feature.json index 2d0440993..40598fac1 100644 --- a/src/terragrunt/devcontainer-feature.json +++ b/src/terragrunt/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "terragrunt", - "version": "1.0.15", + "version": "1.0.16", "name": "Terragrunt (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/terragrunt", "description": "Terragrunt is a thin wrapper that provides extra tools for keeping your configurations DRY, working with multiple Terraform modules, and managing remote state.", diff --git a/src/terragrunt/install.sh b/src/terragrunt/install.sh index 115b5969e..838ce1afb 100755 --- a/src/terragrunt/install.sh +++ b/src/terragrunt/install.sh @@ -15,9 +15,8 @@ $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1.0.25" \ - --option repo='gruntwork-io/terragrunt' --option binaryNames='terragrunt' --option version="$VERSION" - - + --option repo='gruntwork-io/terragrunt' --option binaryNames='terragrunt' --option version="$VERSION" \ + --option assetRegex='^(?!.*(\.zip|\.tar\.gz)$).*$' echo 'Done!' diff --git a/test/terragrunt/scenarios.json b/test/terragrunt/scenarios.json index 1d1e6cecd..0abc6a801 100644 --- a/test/terragrunt/scenarios.json +++ b/test/terragrunt/scenarios.json @@ -4,5 +4,21 @@ "features": { "terragrunt": {} } + }, + "test_specific_version_before_assets_change": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "terragrunt": { + "version": "0.93.5" + } + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "terragrunt": { + "version": "0.96.0" + } + } } } \ No newline at end of file diff --git a/test/terragrunt/test_specific_version.sh b/test/terragrunt/test_specific_version.sh new file mode 100755 index 000000000..5580e8094 --- /dev/null +++ b/test/terragrunt/test_specific_version.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "terragrunt --version" terragrunt --version +check "terragrunt version is equal to 0.96.0" sh -c "terragrunt --version | grep '0.96.0'" + +reportResults diff --git a/test/terragrunt/test_specific_version_before_assets_change.sh b/test/terragrunt/test_specific_version_before_assets_change.sh new file mode 100755 index 000000000..8f7a10be4 --- /dev/null +++ b/test/terragrunt/test_specific_version_before_assets_change.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "terragrunt --version" terragrunt --version +check "terragrunt version is equal to 0.93.5" sh -c "terragrunt --version | grep '0.93.5'" + +reportResults From 9fda7e6130b00887f5eb6212710cc265cb3c0c28 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 19:01:53 +0000 Subject: [PATCH 30/71] feat(ty): init Generated using boilerplate templates. --- src/ty/devcontainer-feature.json | 18 ++++ src/ty/install.sh | 21 ++++ src/ty/library_scripts.sh | 173 +++++++++++++++++++++++++++++++ test/ty/scenarios.json | 16 +++ test/ty/test.sh | 9 ++ test/ty/test_debian.sh | 9 ++ test/ty/test_specific_version.sh | 9 ++ 7 files changed, 255 insertions(+) create mode 100644 src/ty/devcontainer-feature.json create mode 100755 src/ty/install.sh create mode 100644 src/ty/library_scripts.sh create mode 100644 test/ty/scenarios.json create mode 100755 test/ty/test.sh create mode 100755 test/ty/test_debian.sh create mode 100755 test/ty/test_specific_version.sh diff --git a/src/ty/devcontainer-feature.json b/src/ty/devcontainer-feature.json new file mode 100644 index 000000000..77c38b5cc --- /dev/null +++ b/src/ty/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "ty", + "version": "1.0.0", + "name": "ty (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/ty", + "description": "An extremely fast Python type checker and language server, written in Rust.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/ty/install.sh b/src/ty/install.sh new file mode 100755 index 000000000..e417b490c --- /dev/null +++ b/src/ty/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='astral-sh/ty' --option binaryNames='ty' --option version="$VERSION" + +echo 'Done!' diff --git a/src/ty/library_scripts.sh b/src/ty/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/ty/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/ty/scenarios.json b/test/ty/scenarios.json new file mode 100644 index 000000000..ef48409c0 --- /dev/null +++ b/test/ty/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "ty": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "ty": { + "version": "0.0.10" + } + } + } +} diff --git a/test/ty/test.sh b/test/ty/test.sh new file mode 100755 index 000000000..c433ffc9f --- /dev/null +++ b/test/ty/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ty is installed" ty --version + +reportResults diff --git a/test/ty/test_debian.sh b/test/ty/test_debian.sh new file mode 100755 index 000000000..c433ffc9f --- /dev/null +++ b/test/ty/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ty is installed" ty --version + +reportResults diff --git a/test/ty/test_specific_version.sh b/test/ty/test_specific_version.sh new file mode 100755 index 000000000..5d5af0ad9 --- /dev/null +++ b/test/ty/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ty version is equal to 0.0.10" sh -c "ty --version | grep '0.0.10'" + +reportResults From 629d891408cf6bd6f4df6d2145e4fb685f3a5916 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:05:23 +0100 Subject: [PATCH 31/71] feat(ty): add astral-sh.ty extension --- src/ty/devcontainer-feature.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ty/devcontainer-feature.json b/src/ty/devcontainer-feature.json index 77c38b5cc..ba183c261 100644 --- a/src/ty/devcontainer-feature.json +++ b/src/ty/devcontainer-feature.json @@ -14,5 +14,12 @@ "type": "string" } }, + "customizations": { + "vscode": { + "extensions": [ + "astral-sh.ty" + ] + } + }, "installsAfter": [] } From aed911c7e9f9f62b6eeab6c1d197209e6ef809a8 Mon Sep 17 00:00:00 2001 From: Jens Nistler Date: Tue, 13 Jan 2026 18:07:48 +0100 Subject: [PATCH 32/71] install ruff from github releases instead of pipx --- src/ruff/README.md | 2 +- src/ruff/devcontainer-feature.json | 9 ++-- src/ruff/install.sh | 9 ++-- src/ruff/library_scripts.sh | 71 +++++++++++++++--------------- 4 files changed, 43 insertions(+), 48 deletions(-) diff --git a/src/ruff/README.md b/src/ruff/README.md index 6c052cea7..03c1e3fc8 100644 --- a/src/ruff/README.md +++ b/src/ruff/README.md @@ -1,5 +1,5 @@ -# Ruff (via pipx) (ruff) +# Ruff (via Github Releases) Ruff is an extremely fast Python linter, written in Rust. diff --git a/src/ruff/devcontainer-feature.json b/src/ruff/devcontainer-feature.json index 4df2188f7..dabdc6702 100644 --- a/src/ruff/devcontainer-feature.json +++ b/src/ruff/devcontainer-feature.json @@ -1,7 +1,7 @@ { "id": "ruff", - "version": "1.1.1", - "name": "Ruff (via pipx)", + "version": "2.0.0", + "name": "Ruff (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/ruff", "description": "Ruff is an extremely fast Python linter, written in Rust.", "options": { @@ -21,8 +21,5 @@ ] } }, - "installsAfter": [ - "ghcr.io/devcontainers-extra/features/pipx-package", - "ghcr.io/devcontainers/features/python" - ] + "installsAfter": [] } diff --git a/src/ruff/install.sh b/src/ruff/install.sh index 1c344caae..0f12357b8 100755 --- a/src/ruff/install.sh +++ b/src/ruff/install.sh @@ -8,16 +8,13 @@ set -e # `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.0" +ensure_nanolayer nanolayer_location "v0.5.6" $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/pipx-package:1.1.8" \ - --option package='ruff' --option version="$VERSION" - - + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='astral-sh/ruff' --option binaryNames='ruff' --option version="$VERSION" echo 'Done!' - diff --git a/src/ruff/library_scripts.sh b/src/ruff/library_scripts.sh index 9e1072eca..f6d0760d7 100644 --- a/src/ruff/library_scripts.sh +++ b/src/ruff/library_scripts.sh @@ -1,4 +1,4 @@ - +#!/usr/bin/env bash clean_download() { # The purpose of this function is to download a file with minimal impact on container layer size @@ -15,7 +15,7 @@ clean_download() { tempdir=$(mktemp -d) downloader_installed="" - _apt_get_install() { + function _apt_get_install() { tempdir=$1 # copy current state of apt list - in order to revert back later (minimize contianer layer size) @@ -24,7 +24,7 @@ clean_download() { apt-get -y install --no-install-recommends wget ca-certificates } - _apt_get_cleanup() { + function _apt_get_cleanup() { tempdir=$1 echo "removing wget" @@ -35,15 +35,15 @@ clean_download() { rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists } - _apk_install() { + 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 + apk add --no-cache wget } - _apk_cleanup() { + function _apk_cleanup() { tempdir=$1 echo "removing wget" @@ -59,10 +59,10 @@ clean_download() { fi # in case none of them is installed, install wget temporarly - if [ -z $downloader ] ; then - if [ -x "/usr/bin/apt-get" ] ; then + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then _apt_get_install $tempdir - elif [ -x "/sbin/apk" ] ; then + elif [ -x "/sbin/apk" ]; then _apk_install $tempdir else echo "distro not supported" @@ -72,7 +72,7 @@ clean_download() { downloader_installed="true" fi - if [ $downloader = "wget" ] ; then + if [ $downloader = "wget" ]; then wget -q $url -O $output_location else curl -sfL $url -o $output_location @@ -80,10 +80,10 @@ clean_download() { # 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 + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then _apt_get_cleanup $tempdir - elif [ -x "/sbin/apk" ] ; then + elif [ -x "/sbin/apk" ]; then _apk_cleanup $tempdir else echo "distro not supported" @@ -93,56 +93,60 @@ clean_download() { } - 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="" + 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 [[ -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 + 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" + 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 + if ! [[ -z "${nanolayer_location}" ]]; then local current_version - current_version=$($__nanolayer_location --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="" + 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 [[ -z "${nanolayer_location}" ]]; then - if [ "$(uname -sm)" = 'Linux x86_64' ] || [ "$(uname -sm)" = "Linux aarch64" ]; then + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) - clean_up () { + clean_up() { ARG=$? rm -rf $tmp_dir exit $ARG } trap clean_up EXIT - - if [ -x "/sbin/apk" ] ; then + if [ -x "/sbin/apk" ]; then clib_type=musl else clib_type=gnu @@ -155,8 +159,7 @@ ensure_nanolayer() { tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" chmod a+x $tmp_dir/nanolayer - __nanolayer_location=$tmp_dir/nanolayer - + nanolayer_location=$tmp_dir/nanolayer else echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" @@ -165,8 +168,6 @@ ensure_nanolayer() { fi # Expose outside the resolved location - export ${variable_name}=$__nanolayer_location + declare -g ${variable_name}=$nanolayer_location } - - From 40513914abc022dd1eb3cd41698e823637d811eb Mon Sep 17 00:00:00 2001 From: Jens Nistler Date: Wed, 14 Jan 2026 09:45:10 +0100 Subject: [PATCH 33/71] align with install script from ty --- src/ruff/install.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ruff/install.sh b/src/ruff/install.sh index 0f12357b8..87b3ec2af 100755 --- a/src/ruff/install.sh +++ b/src/ruff/install.sh @@ -1,7 +1,8 @@ +#!/usr/bin/env bash set -e -. ./library_scripts.sh +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 @@ -10,7 +11,7 @@ set -e # of the script ensure_nanolayer nanolayer_location "v0.5.6" - +# Example nanolayer installation via devcontainer-feature $nanolayer_location \ install \ devcontainer-feature \ From ab4d5fea91afc978278d7be1231a2414c5478c95 Mon Sep 17 00:00:00 2001 From: Jason Garber Date: Tue, 20 Jan 2026 00:19:52 -0500 Subject: [PATCH 34/71] Add assetRegex option At some point in the recent past, I started getting installation errors when using this Feature: ``` nanolayer.installers.gh_release.resolvers.asset_resolver.AssetResolver.AssetResolverError: Too many matches found ``` The shellcheck v0.11.0 release added some assets a few weeks ago, which _could_ be the trigger that caused the above error: https://github.com/koalaman/shellcheck/releases/tag/v0.11.0 I've added a pretty basic regex filter on the asset name that should limits the matches to gzipped tar files. If there's a more appropriate regex to use, please let me know. I successfully tested this pattern locally by using the gh-release feature with the following configuration: ```json "ghcr.io/devcontainers-extra/features/gh-release:1": { "repo": "koalaman/shellcheck", "binaryNames": "shellcheck", "assetRegex": "shellcheck-.*.tar.gz$" }, ``` Signed-off-by: Jason Garber --- src/shellcheck/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shellcheck/install.sh b/src/shellcheck/install.sh index 8dbf2ce1a..816835f3d 100755 --- a/src/shellcheck/install.sh +++ b/src/shellcheck/install.sh @@ -16,6 +16,6 @@ $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1" \ - --option repo='koalaman/shellcheck' --option binaryNames='shellcheck' --option version="$VERSION" + --option repo='koalaman/shellcheck' --option binaryNames='shellcheck' --option version="$VERSION" --option assetRegex='shellcheck-.*.tar.gz$' echo 'Done!' From c7ef7deecb5224867c65e9750f5fd5e8bd753107 Mon Sep 17 00:00:00 2001 From: Jason Garber Date: Tue, 20 Jan 2026 09:17:40 -0500 Subject: [PATCH 35/71] Use `tar.xz` instead of `tar.gz` shellcheck v0.11.0 introduced `tar.gz` files. Previous releases exclusively use `tar.xz`. Signed-off-by: Jason Garber --- src/shellcheck/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shellcheck/install.sh b/src/shellcheck/install.sh index 816835f3d..ecc24b3d5 100755 --- a/src/shellcheck/install.sh +++ b/src/shellcheck/install.sh @@ -16,6 +16,6 @@ $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1" \ - --option repo='koalaman/shellcheck' --option binaryNames='shellcheck' --option version="$VERSION" --option assetRegex='shellcheck-.*.tar.gz$' + --option repo='koalaman/shellcheck' --option binaryNames='shellcheck' --option version="$VERSION" --option assetRegex='shellcheck-.*.tar.xz$' echo 'Done!' From 83550b6063fef8a94026f66eefc7e8f57c4abaa3 Mon Sep 17 00:00:00 2001 From: Jason Garber Date: Tue, 20 Jan 2026 09:19:36 -0500 Subject: [PATCH 36/71] Bump shellcheck Feature version to v1.1.0 Signed-off-by: Jason Garber --- src/shellcheck/devcontainer-feature.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shellcheck/devcontainer-feature.json b/src/shellcheck/devcontainer-feature.json index 12b7cd586..821947f9b 100644 --- a/src/shellcheck/devcontainer-feature.json +++ b/src/shellcheck/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "shellcheck", - "version": "1.0.0", + "version": "1.1.0", "name": "shellcheck (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/shellcheck", "description": "", @@ -15,4 +15,4 @@ } }, "installsAfter": [] -} \ No newline at end of file +} From 4cf1ec6f17c0eec699ef861273f7bcf32b7ea60b Mon Sep 17 00:00:00 2001 From: Kolja Date: Wed, 21 Jan 2026 10:10:23 +0100 Subject: [PATCH 37/71] Deno: Linux aarch64 sources are supplied Don't error out on linux aarch64 Signed-off-by: Kolja --- src/deno/install.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/deno/install.sh b/src/deno/install.sh index e9f96854e..d06bee329 100755 --- a/src/deno/install.sh +++ b/src/deno/install.sh @@ -18,10 +18,7 @@ else case $(uname -sm) in "Darwin x86_64") target="x86_64-apple-darwin" ;; "Darwin arm64") target="aarch64-apple-darwin" ;; - "Linux aarch64") - echo "Error: Official Deno builds for Linux aarch64 are not available. (https://github.com/denoland/deno/issues/1846)" 1>&2 - exit 1 - ;; + "Linux aarch64") target='aarch64-unknown-linux-gnu' ;; *) target="x86_64-unknown-linux-gnu" ;; esac fi From 2dcb4f72598c9d286372943e602df9f9766dfc49 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:25:30 +0000 Subject: [PATCH 38/71] chore(devcontainer): add boilerplate and remove tmux --- .github/.devcontainer/devcontainer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/.devcontainer/devcontainer.json b/.github/.devcontainer/devcontainer.json index 12f920fb3..410a0f03e 100644 --- a/.github/.devcontainer/devcontainer.json +++ b/.github/.devcontainer/devcontainer.json @@ -21,7 +21,7 @@ "ghcr.io/lukewiwa/features/shellcheck:0.2.3": {}, "ghcr.io/guiyomh/features/just:0": {}, "ghcr.io/devcontainers-extra/features/pre-commit:2": {}, - "ghcr.io/devcontainers-extra/features/tmux-homebrew:1": {}, - "ghcr.io/devcontainers-extra/features/act:1": {} + "ghcr.io/devcontainers-extra/features/act:1": {}, + "ghcr.io/devcontainers-extra/features/boilerplate:1": {} } } \ No newline at end of file From a7e750e045ed66e2a0508ca93436172698330eff Mon Sep 17 00:00:00 2001 From: Razze Date: Sat, 24 Jan 2026 17:11:45 +0100 Subject: [PATCH 39/71] Bump deno feature version to 1.0.4 --- src/deno/devcontainer-feature.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deno/devcontainer-feature.json b/src/deno/devcontainer-feature.json index b40fdbfc1..c6b5bd4fe 100644 --- a/src/deno/devcontainer-feature.json +++ b/src/deno/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "Deno (via Github Releases)", "id": "deno", - "version": "1.0.3", + "version": "1.0.4", "description": "Deno is a simple, modern runtime for JavaScript and TypeScript that uses V8 and is built in Rust.", "documentationURL": "https://github.com/devcontainers-extra/features/tree/main/src/deno", "options": { From 88baff3e1e3e78418eac874b084f9b05f6f833c1 Mon Sep 17 00:00:00 2001 From: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> Date: Sun, 25 Jan 2026 20:29:40 +0000 Subject: [PATCH 40/71] chore(just): fix path to templates in add command --- justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 35e0c866e..50af130e2 100644 --- a/justfile +++ b/justfile @@ -18,7 +18,7 @@ archive feature-name: add feature-name: mkdir -p src/{{feature-name}} test/{{feature-name}} - cp templates/src/* src/{{feature-name}}/ - cp templates/test/* test/{{feature-name}}/ + cp templates/bak/src/* src/{{feature-name}}/ + cp templates/bak/test/* test/{{feature-name}}/ sed {{sed_inplace}} "s//{{feature-name}}/g" src/{{feature-name}}/* sed {{sed_inplace}} "s//{{feature-name}}/g" test/{{feature-name}}/* From da7016845383b02be07278e5d52bc8fcccb29c02 Mon Sep 17 00:00:00 2001 From: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:02:49 +0100 Subject: [PATCH 41/71] chore: set GitHub Sponsors in FUNDING.yml Signed-off-by: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..84ecab5c7 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: koralowiec From 15db1018d46404038e7d1f941f759635a6c656f9 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Sun, 21 Sep 2025 23:05:27 +0000 Subject: [PATCH 42/71] fix: Bun Feature Failure on Apple Silicon ARM64 + Comprehensive Feature Enhancements --- src/bun/NOTES.md | 36 ++- src/bun/devcontainer-feature.json | 2 +- src/bun/install.sh | 397 ++++++++++++++++++++++++++---- 3 files changed, 382 insertions(+), 53 deletions(-) diff --git a/src/bun/NOTES.md b/src/bun/NOTES.md index 966e89891..f11dbb560 100644 --- a/src/bun/NOTES.md +++ b/src/bun/NOTES.md @@ -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. diff --git a/src/bun/devcontainer-feature.json b/src/bun/devcontainer-feature.json index ba48f3b14..9d61213a0 100644 --- a/src/bun/devcontainer-feature.json +++ b/src/bun/devcontainer-feature.json @@ -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.", diff --git a/src/bun/install.sh b/src/bun/install.sh index 504fabe5b..64d94668c 100755 --- a/src/bun/install.sh +++ b/src/bun/install.sh @@ -4,67 +4,336 @@ set -e source ./library_scripts.sh +# Helper function for consistent logging (robust for container environments) +log() { + local level="$1" + shift + local message="$*" + local timestamp + + # Try to get timestamp, fallback if date command fails + if timestamp=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null); then + timestamp="[$timestamp]" + else + timestamp="[$(date 2>/dev/null || echo 'unknown')]" + fi + + case "$level" in + INFO) echo "$timestamp INFO: $message" ;; + SUCCESS) echo "$timestamp SUCCESS: $message" ;; + WARNING) echo "$timestamp WARNING: $message" ;; + ERROR) echo "$timestamp ERROR: $message" >&2 ;; + *) echo "$timestamp $level: $message" ;; + esac +} + +# Debug logging helper (enabled when BUN_FEATURE_DEBUG is set) +debug_log() { + if [ -n "$BUN_FEATURE_DEBUG" ]; then + log INFO "$@" + fi +} + +log INFO "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 +log INFO "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 + + # 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 -fi + + # Handle plain version numbers + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "bun-v$version" + return 0 + fi + + # Invalid version format + log WARNING "Invalid version format: '$version'. Using 'latest' instead." + echo "latest" +} + +version_for_release="$(canonicalize_version "$VERSION")" +log INFO "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 +} + +log INFO "Detecting system architecture" +if ! arch_segment="$(detect_arch)"; then + log ERROR "Failed to detect architecture" + 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 + log INFO "Detected musl libc (Alpine package manager found)" >&2 + 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 + log INFO "Detected musl libc (ldd output)" >&2 + 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 + log INFO "Detected musl libc (library files)" >&2 + echo "-musl" + return 0 + fi + + # Method 4: Check /proc/mounts for musl + if grep -qi musl /proc/mounts 2>/dev/null; then + log INFO "Detected musl libc (proc mounts)" >&2 + echo "-musl" + return 0 + fi + + # Default to glibc + log INFO "Using glibc (default)" >&2 + echo "" +} + +log INFO "Detecting libc implementation" +if ! libc_suffix="$(detect_libc)"; then + log ERROR "Failed to detect libc implementation" + exit 1 +fi + +# Set human-readable libc name for error messages +if [ "$libc_suffix" = "-musl" ]; then + libc_name="musl" +else + libc_name="glibc" 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 +log INFO "Building asset patterns for arch=$arch_segment, libc=${libc_suffix:-glibc}" +if ! asset_patterns_output=$(build_asset_patterns "$arch_segment" "$libc_suffix"); then + log ERROR "Failed to build asset patterns for arch=$arch_segment, libc_suffix=$libc_suffix" + exit 1 +fi + +# Convert to array (with debugging) + log INFO "Asset patterns built: $asset_patterns_output" + +# Convert to array - use a more reliable method +debug_log "Debug: asset_patterns_output='$asset_patterns_output'" +debug_log "Debug: asset_patterns_output contains newline: $([[ "$asset_patterns_output" == *$'\n'* ]] && echo 'YES' || echo 'NO')" + +# 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" + +debug_log "Debug: asset_patterns array has ${#asset_patterns[@]} elements" +for i in "${!asset_patterns[@]}"; do + debug_log "Debug: asset_patterns[$i] = '${asset_patterns[$i]}'" +done + +# Verify we have asset patterns +if [ ${#asset_patterns[@]} -eq 0 ]; then + log ERROR "No asset patterns generated for arch=$arch_segment, libc_suffix=$libc_suffix" + log ERROR "asset_patterns_output was: '$asset_patterns_output'" + exit 1 +fi + +# Try each asset pattern until one succeeds +install_bun() { + log INFO "install_bun function called with ${#asset_patterns[@]} asset patterns" + + if [ ${#asset_patterns[@]} -eq 0 ]; then + log ERROR "install_bun: No asset patterns provided!" + return 1 + fi + + for asset_regex in "${asset_patterns[@]}"; do + log INFO "Attempting to install Bun with asset regex: $asset_regex" + debug_log "Debug: asset_regex = '$asset_regex'" + + log INFO "Running nanolayer installation command..." + log INFO "Command: $nanolayer_location install devcontainer-feature ghcr.io/devcontainers-extra/features/gh-release:1" + log INFO "Options: repo=oven-sh/bun, binaryNames=bun, version=$version_for_release" + log INFO "Options: assetRegex=$asset_regex, releaseTagRegex=$release_tag_regex" + + # Verify nanolayer_location is set + if [ -z "$nanolayer_location" ]; then + log ERROR "nanolayer_location is not set!" + return 1 + fi + + debug_log "Debug: nanolayer_location = '$nanolayer_location'" + debug_log "Debug: nanolayer_location exists: $([ -x "$nanolayer_location" ] && echo 'YES' || echo 'NO')" + + debug_log "Debug: About to execute nanolayer command for asset: $asset_regex" + + if nanolayer_output=$($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 + + log INFO "Nanolayer command succeeded" + log INFO "Nanolayer output: $nanolayer_output" + + # Check if bun binary was actually installed + if [ -f "/usr/local/bin/bun" ]; then + log INFO "Bun binary found at /usr/local/bin/bun" + log INFO "Bun binary permissions: $(ls -la /usr/local/bin/bun)" + else + log ERROR "Bun binary NOT found at /usr/local/bin/bun after installation" + log ERROR "Checking /usr/local/bin contents: $(ls -la /usr/local/bin/ 2>/dev/null || echo 'Directory not accessible')" + + # Try to find where bun might have been installed + log INFO "Searching for bun binary in common locations..." + find /usr/local -name "*bun*" -type f 2>/dev/null || log INFO "No bun binaries found in /usr/local" + fi + log SUCCESS "Successfully installed Bun with asset regex: $asset_regex" + return 0 + else + log WARNING "Failed to install Bun with asset regex: $asset_regex, trying next pattern..." + log WARNING "Nanolayer output: $nanolayer_output" + fi + done + + log ERROR "Failed to install Bun. No asset patterns matched." + log ERROR "Troubleshooting information:" + log ERROR "- Architecture: $arch_segment" + log ERROR "- Libc: ${libc_name}" + log ERROR "- Version: $version_for_release" + log ERROR "- Asset patterns tried: ${asset_patterns[*]}" + log ERROR "Check https://github.com/oven-sh/bun/releases for available assets" + return 1 +} + +# Attempt installation +log INFO "Starting Bun installation process..." +log INFO "About to call install_bun function with ${#asset_patterns[@]} patterns" + +if ! install_bun; then + log ERROR "install_bun function failed" + exit 1 +fi + +log INFO "Bun installation process completed" # Provide a convenient bunx shim if [ -x "/usr/local/bin/bun" ] && ! [ -x "/usr/local/bin/bunx" ]; then @@ -75,4 +344,42 @@ EOF chmod +x /usr/local/bin/bunx fi -echo 'Bun installation completed.' +log SUCCESS "Bun installation completed successfully" + +# Verify the installation worked +log INFO "Verifying Bun installation..." +if [ -x "/usr/local/bin/bun" ]; then + log INFO "Bun binary is executable at /usr/local/bin/bun" + if bun_binary_version=$(/usr/local/bin/bun --version 2>&1); then + log INFO "Bun version check successful: $bun_binary_version" + else + log INFO "Bun binary exists but version check failed - this may be environment-specific" + log INFO "Binary info: $(file /usr/local/bin/bun 2>/dev/null || echo 'file command failed')" + log INFO "Library dependencies: $(ldd /usr/local/bin/bun 2>/dev/null || echo 'ldd command failed')" + + # Try to run with explicit library path for debugging + if [ -d "/lib" ] && [ -d "/usr/lib" ]; then + log INFO "Attempting to run bun with library path for debugging..." + if bun_binary_version=$(LD_LIBRARY_PATH=/lib:/usr/lib:/usr/local/lib /usr/local/bin/bun --version 2>&1); then + log INFO "Bun version check successful with LD_LIBRARY_PATH: $bun_binary_version" + else + log INFO "Bun version check still failed with LD_LIBRARY_PATH" + fi + fi + fi +else + log ERROR "Bun binary not found or not executable at /usr/local/bin/bun" + log ERROR "Checking if bun exists: $([ -f "/usr/local/bin/bun" ] && echo 'YES' || echo 'NO')" + if [ -f "/usr/local/bin/bun" ]; then + log ERROR "Bun file exists but is not executable. Permissions: $(ls -la /usr/local/bin/bun)" + log ERROR "Attempting to make executable..." + chmod +x /usr/local/bin/bun + if [ -x "/usr/local/bin/bun" ]; then + log INFO "Fixed: Bun binary is now executable" + else + log ERROR "Failed to make Bun binary executable" + fi + else + log ERROR "Bun binary file does not exist at /usr/local/bin/bun" + fi +fi From 058d31c77ea7343528048f1a8cb69ac953df7198 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 5 Feb 2026 02:35:48 +0000 Subject: [PATCH 43/71] fix: address reviewer feedback and fix Alpine musl detection bug Co-authored-by: Cursor --- src/bun/install.sh | 186 +++++------------------------- test/bun/scenarios.json | 2 +- test/bun/test_specific_version.sh | 4 +- 3 files changed, 30 insertions(+), 162 deletions(-) diff --git a/src/bun/install.sh b/src/bun/install.sh index 64d94668c..00921233b 100755 --- a/src/bun/install.sh +++ b/src/bun/install.sh @@ -4,44 +4,14 @@ set -e source ./library_scripts.sh -# Helper function for consistent logging (robust for container environments) -log() { - local level="$1" - shift - local message="$*" - local timestamp - - # Try to get timestamp, fallback if date command fails - if timestamp=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null); then - timestamp="[$timestamp]" - else - timestamp="[$(date 2>/dev/null || echo 'unknown')]" - fi - - case "$level" in - INFO) echo "$timestamp INFO: $message" ;; - SUCCESS) echo "$timestamp SUCCESS: $message" ;; - WARNING) echo "$timestamp WARNING: $message" ;; - ERROR) echo "$timestamp ERROR: $message" >&2 ;; - *) echo "$timestamp $level: $message" ;; - esac -} - -# Debug logging helper (enabled when BUN_FEATURE_DEBUG is set) -debug_log() { - if [ -n "$BUN_FEATURE_DEBUG" ]; then - log INFO "$@" - fi -} - -log INFO "Starting Bun installation" +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 -log INFO "Ensuring nanolayer is available" +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" @@ -82,12 +52,14 @@ canonicalize_version() { fi # Invalid version format - log WARNING "Invalid version format: '$version'. Using 'latest' instead." + 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")" -log INFO "Using Bun version: $version_for_release" +echo "Using Bun version: $version_for_release" # Figure out arch and libc to disambiguate Bun's multiple Linux assets detect_arch() { @@ -111,9 +83,9 @@ detect_arch() { esac } -log INFO "Detecting system architecture" +echo "Detecting system architecture" if ! arch_segment="$(detect_arch)"; then - log ERROR "Failed to detect architecture" + echo "ERROR: Failed to detect architecture" >&2 exit 1 fi @@ -121,7 +93,6 @@ fi detect_libc() { # Method 1: Check for apk (Alpine) if [ -x "/sbin/apk" ]; then - log INFO "Detected musl libc (Alpine package manager found)" >&2 echo "-musl" return 0 fi @@ -129,7 +100,6 @@ detect_libc() { # 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 - log INFO "Detected musl libc (ldd output)" >&2 echo "-musl" return 0 fi @@ -137,36 +107,26 @@ detect_libc() { # Method 3: Check for musl library files if [ -f "/lib/ld-musl-x86_64.so.1" ] || [ -f "/lib/ld-musl-aarch64.so.1" ]; then - log INFO "Detected musl libc (library files)" >&2 echo "-musl" return 0 fi # Method 4: Check /proc/mounts for musl if grep -qi musl /proc/mounts 2>/dev/null; then - log INFO "Detected musl libc (proc mounts)" >&2 echo "-musl" return 0 fi # Default to glibc - log INFO "Using glibc (default)" >&2 echo "" } -log INFO "Detecting libc implementation" +echo "Detecting libc implementation" if ! libc_suffix="$(detect_libc)"; then - log ERROR "Failed to detect libc implementation" + echo "ERROR: Failed to detect libc implementation" >&2 exit 1 fi -# Set human-readable libc name for error messages -if [ "$libc_suffix" = "-musl" ]; then - libc_name="musl" -else - libc_name="glibc" -fi - # 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 @@ -219,19 +179,12 @@ build_asset_patterns() { } # Build asset patterns for current architecture -log INFO "Building asset patterns for arch=$arch_segment, libc=${libc_suffix:-glibc}" if ! asset_patterns_output=$(build_asset_patterns "$arch_segment" "$libc_suffix"); then - log ERROR "Failed to build asset patterns for arch=$arch_segment, libc_suffix=$libc_suffix" + echo "ERROR: Failed to build asset patterns for arch=$arch_segment, libc_suffix=$libc_suffix" >&2 exit 1 fi -# Convert to array (with debugging) - log INFO "Asset patterns built: $asset_patterns_output" - # Convert to array - use a more reliable method -debug_log "Debug: asset_patterns_output='$asset_patterns_output'" -debug_log "Debug: asset_patterns_output contains newline: $([[ "$asset_patterns_output" == *$'\n'* ]] && echo 'YES' || echo 'NO')" - # Split by newlines and create array asset_patterns=() while IFS= read -r line; do @@ -240,48 +193,22 @@ while IFS= read -r line; do fi done <<< "$asset_patterns_output" -debug_log "Debug: asset_patterns array has ${#asset_patterns[@]} elements" -for i in "${!asset_patterns[@]}"; do - debug_log "Debug: asset_patterns[$i] = '${asset_patterns[$i]}'" -done - # Verify we have asset patterns if [ ${#asset_patterns[@]} -eq 0 ]; then - log ERROR "No asset patterns generated for arch=$arch_segment, libc_suffix=$libc_suffix" - log ERROR "asset_patterns_output was: '$asset_patterns_output'" + 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() { - log INFO "install_bun function called with ${#asset_patterns[@]} asset patterns" - if [ ${#asset_patterns[@]} -eq 0 ]; then - log ERROR "install_bun: No asset patterns provided!" + echo "ERROR: install_bun: No asset patterns provided!" >&2 return 1 fi for asset_regex in "${asset_patterns[@]}"; do - log INFO "Attempting to install Bun with asset regex: $asset_regex" - debug_log "Debug: asset_regex = '$asset_regex'" - - log INFO "Running nanolayer installation command..." - log INFO "Command: $nanolayer_location install devcontainer-feature ghcr.io/devcontainers-extra/features/gh-release:1" - log INFO "Options: repo=oven-sh/bun, binaryNames=bun, version=$version_for_release" - log INFO "Options: assetRegex=$asset_regex, releaseTagRegex=$release_tag_regex" - - # Verify nanolayer_location is set - if [ -z "$nanolayer_location" ]; then - log ERROR "nanolayer_location is not set!" - return 1 - fi - - debug_log "Debug: nanolayer_location = '$nanolayer_location'" - debug_log "Debug: nanolayer_location exists: $([ -x "$nanolayer_location" ] && echo 'YES' || echo 'NO')" - - debug_log "Debug: About to execute nanolayer command for asset: $asset_regex" - - if nanolayer_output=$($nanolayer_location \ + if $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1" \ @@ -289,51 +216,28 @@ install_bun() { --option binaryNames='bun' \ --option version="$version_for_release" \ --option assetRegex="$asset_regex" \ - --option releaseTagRegex="$release_tag_regex" 2>&1); then - - log INFO "Nanolayer command succeeded" - log INFO "Nanolayer output: $nanolayer_output" - - # Check if bun binary was actually installed - if [ -f "/usr/local/bin/bun" ]; then - log INFO "Bun binary found at /usr/local/bin/bun" - log INFO "Bun binary permissions: $(ls -la /usr/local/bin/bun)" - else - log ERROR "Bun binary NOT found at /usr/local/bin/bun after installation" - log ERROR "Checking /usr/local/bin contents: $(ls -la /usr/local/bin/ 2>/dev/null || echo 'Directory not accessible')" - - # Try to find where bun might have been installed - log INFO "Searching for bun binary in common locations..." - find /usr/local -name "*bun*" -type f 2>/dev/null || log INFO "No bun binaries found in /usr/local" - fi - log SUCCESS "Successfully installed Bun with asset regex: $asset_regex" + --option releaseTagRegex="$release_tag_regex" 2>&1; then return 0 - else - log WARNING "Failed to install Bun with asset regex: $asset_regex, trying next pattern..." - log WARNING "Nanolayer output: $nanolayer_output" fi done - log ERROR "Failed to install Bun. No asset patterns matched." - log ERROR "Troubleshooting information:" - log ERROR "- Architecture: $arch_segment" - log ERROR "- Libc: ${libc_name}" - log ERROR "- Version: $version_for_release" - log ERROR "- Asset patterns tried: ${asset_patterns[*]}" - log ERROR "Check https://github.com/oven-sh/bun/releases for available assets" + 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 -log INFO "Starting Bun installation process..." -log INFO "About to call install_bun function with ${#asset_patterns[@]} patterns" - if ! install_bun; then - log ERROR "install_bun function failed" + echo "ERROR: install_bun function failed" >&2 exit 1 fi -log INFO "Bun installation process completed" +echo "Bun installation process completed" # Provide a convenient bunx shim if [ -x "/usr/local/bin/bun" ] && ! [ -x "/usr/local/bin/bunx" ]; then @@ -344,42 +248,4 @@ EOF chmod +x /usr/local/bin/bunx fi -log SUCCESS "Bun installation completed successfully" - -# Verify the installation worked -log INFO "Verifying Bun installation..." -if [ -x "/usr/local/bin/bun" ]; then - log INFO "Bun binary is executable at /usr/local/bin/bun" - if bun_binary_version=$(/usr/local/bin/bun --version 2>&1); then - log INFO "Bun version check successful: $bun_binary_version" - else - log INFO "Bun binary exists but version check failed - this may be environment-specific" - log INFO "Binary info: $(file /usr/local/bin/bun 2>/dev/null || echo 'file command failed')" - log INFO "Library dependencies: $(ldd /usr/local/bin/bun 2>/dev/null || echo 'ldd command failed')" - - # Try to run with explicit library path for debugging - if [ -d "/lib" ] && [ -d "/usr/lib" ]; then - log INFO "Attempting to run bun with library path for debugging..." - if bun_binary_version=$(LD_LIBRARY_PATH=/lib:/usr/lib:/usr/local/lib /usr/local/bin/bun --version 2>&1); then - log INFO "Bun version check successful with LD_LIBRARY_PATH: $bun_binary_version" - else - log INFO "Bun version check still failed with LD_LIBRARY_PATH" - fi - fi - fi -else - log ERROR "Bun binary not found or not executable at /usr/local/bin/bun" - log ERROR "Checking if bun exists: $([ -f "/usr/local/bin/bun" ] && echo 'YES' || echo 'NO')" - if [ -f "/usr/local/bin/bun" ]; then - log ERROR "Bun file exists but is not executable. Permissions: $(ls -la /usr/local/bin/bun)" - log ERROR "Attempting to make executable..." - chmod +x /usr/local/bin/bun - if [ -x "/usr/local/bin/bun" ]; then - log INFO "Fixed: Bun binary is now executable" - else - log ERROR "Failed to make Bun binary executable" - fi - else - log ERROR "Bun binary file does not exist at /usr/local/bin/bun" - fi -fi +echo "Bun installation completed successfully" diff --git a/test/bun/scenarios.json b/test/bun/scenarios.json index 241794675..8e542b6dd 100644 --- a/test/bun/scenarios.json +++ b/test/bun/scenarios.json @@ -21,7 +21,7 @@ "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { "bun": { - "version": "1.2.20" + "version": "1.3.8" } } } diff --git a/test/bun/test_specific_version.sh b/test/bun/test_specific_version.sh index 7bfb97480..64ed43482 100755 --- a/test/bun/test_specific_version.sh +++ b/test/bun/test_specific_version.sh @@ -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 From d33e679bfb1e9b7f28bfce9f33921f2f4f79de96 Mon Sep 17 00:00:00 2001 From: Trevor Campbell <55037769+trevordcampbell@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:53:47 -0500 Subject: [PATCH 44/71] Apply suggestion from @koralowiec Co-authored-by: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> Signed-off-by: Trevor Campbell <55037769+trevordcampbell@users.noreply.github.com> --- test/bun/scenarios.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bun/scenarios.json b/test/bun/scenarios.json index 8e542b6dd..241794675 100644 --- a/test/bun/scenarios.json +++ b/test/bun/scenarios.json @@ -21,7 +21,7 @@ "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { "bun": { - "version": "1.3.8" + "version": "1.2.20" } } } From acea2d1c4b5ac3758e710e917a7b3ad2cdde3668 Mon Sep 17 00:00:00 2001 From: Trevor Campbell <55037769+trevordcampbell@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:54:00 -0500 Subject: [PATCH 45/71] Apply suggestion from @koralowiec Co-authored-by: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> Signed-off-by: Trevor Campbell <55037769+trevordcampbell@users.noreply.github.com> --- test/bun/test_specific_version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bun/test_specific_version.sh b/test/bun/test_specific_version.sh index 64ed43482..f32b754a8 100755 --- a/test/bun/test_specific_version.sh +++ b/test/bun/test_specific_version.sh @@ -6,6 +6,6 @@ set -e # 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.3.8" sh -c "bun --version | grep '^1.3.8'" +check "bun version is equal to 1.2.20" sh -c "bun --version | grep '^1.2.20'" reportResults From 49765551e5546726f6edd7317eac859b0327c382 Mon Sep 17 00:00:00 2001 From: Morgan Lindqvist Date: Tue, 24 Feb 2026 21:43:57 +0100 Subject: [PATCH 46/71] Added missing shebang to install.sh Change-Id: Id9fdb5ab49510e2fee363b333634fcfcf5541d9c --- src/ansible/devcontainer-feature.json | 4 ++-- src/ansible/install.sh | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ansible/devcontainer-feature.json b/src/ansible/devcontainer-feature.json index 6fc4a2cee..1456311cf 100644 --- a/src/ansible/devcontainer-feature.json +++ b/src/ansible/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "ansible", - "version": "2.1.1", + "version": "2.1.2", "name": "Ansible (via pipx)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/ansible", "description": "Ansible is a suite of software tools that enables infrastructure as code.", @@ -27,4 +27,4 @@ "ghcr.io/devcontainers-extra/features/pipx-package", "ghcr.io/devcontainers/features/python" ] -} +} \ No newline at end of file diff --git a/src/ansible/install.sh b/src/ansible/install.sh index 4f49988da..e14f62074 100755 --- a/src/ansible/install.sh +++ b/src/ansible/install.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + set -e . ./library_scripts.sh From a56d8787fd187734f7310553ef84805a6d60b90c Mon Sep 17 00:00:00 2001 From: Matt Kaar <214349+mattkaar@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:32:39 -0400 Subject: [PATCH 47/71] Fix assetresolver error with age-keygen install Similar to #50, this fixes assetresolver errors during the age-keygen install from proof files matching incorrectly. --- src/age-keygen/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/age-keygen/install.sh b/src/age-keygen/install.sh index 3e8c94a30..4cf15f66c 100755 --- a/src/age-keygen/install.sh +++ b/src/age-keygen/install.sh @@ -13,6 +13,6 @@ $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1.0.26" \ - --option repo='filosottile/age' --option binaryNames='age-keygen' --option version="$VERSION" + --option repo='filosottile/age' --option binaryNames='age-keygen' --option version="$VERSION" --option assetRegex='.*(.tar.gz)$' echo 'Done!' From 7c98b15ce3bde86a634d3ae855c25b945bec38dd Mon Sep 17 00:00:00 2001 From: Matt Kaar <214349+mattkaar@users.noreply.github.com> Date: Sun, 22 Mar 2026 12:46:35 -0400 Subject: [PATCH 48/71] Bump feature version --- src/age-keygen/devcontainer-feature.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/age-keygen/devcontainer-feature.json b/src/age-keygen/devcontainer-feature.json index 58cea6e48..c12cb7489 100644 --- a/src/age-keygen/devcontainer-feature.json +++ b/src/age-keygen/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "age-keygen", - "version": "1.0.16", + "version": "1.0.17", "name": "age-keygen (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/age-keygen", "description": "age-keygen generate a key pair for use with age.", From a8abcd5e087a36178d306db9bf639188b13930e6 Mon Sep 17 00:00:00 2001 From: lalten Date: Fri, 3 Apr 2026 16:06:09 +0200 Subject: [PATCH 49/71] Add `prek` --- src/prek/README.md | 39 +++++++ src/prek/devcontainer-feature.json | 20 ++++ src/prek/install.sh | 49 +++++++++ src/prek/library_scripts.sh | 170 +++++++++++++++++++++++++++++ test/prek/scenarios.json | 8 ++ test/prek/test_defaults_debian.sh | 9 ++ 6 files changed, 295 insertions(+) create mode 100644 src/prek/README.md create mode 100644 src/prek/devcontainer-feature.json create mode 100755 src/prek/install.sh create mode 100644 src/prek/library_scripts.sh create mode 100644 test/prek/scenarios.json create mode 100755 test/prek/test_defaults_debian.sh diff --git a/src/prek/README.md b/src/prek/README.md new file mode 100644 index 000000000..9cc9bc697 --- /dev/null +++ b/src/prek/README.md @@ -0,0 +1,39 @@ + +# prek (via Github Releases) + +prek is a faster, dependency-free alternative to pre-commit, re-engineered in Rust. + +## Example Usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/prek:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| version | Select the version to install. | string | latest | + +## About prek + +[prek](https://github.com/j178/prek) is a reimagined version of pre-commit, built in Rust. It is designed to be a faster, dependency-free and drop-in alternative, while also providing some additional long-requested features. + +### Key Features + +- A single binary with no dependencies, does not require Python or any other runtime +- Multiple times faster than `pre-commit` and takes up half the disk space +- Fully compatible with the original pre-commit configurations and hooks +- Built-in support for monorepos (workspace mode) +- Integration with uv for managing Python virtual environments and dependencies +- Built-in Rust-native implementation of some common hooks + +## Installation + +This feature installs prek from GitHub releases based on your system architecture (x86_64 or aarch64) and C library type (glibc or musl). + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](devcontainer-feature.json)._ diff --git a/src/prek/devcontainer-feature.json b/src/prek/devcontainer-feature.json new file mode 100644 index 000000000..876381d01 --- /dev/null +++ b/src/prek/devcontainer-feature.json @@ -0,0 +1,20 @@ +{ + "id": "prek", + "version": "1.0.0", + "name": "prek (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/prek", + "description": "prek is a faster, dependency-free alternative to pre-commit, re-engineered in Rust.", + "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/prek/install.sh b/src/prek/install.sh new file mode 100755 index 000000000..5d4c435b6 --- /dev/null +++ b/src/prek/install.sh @@ -0,0 +1,49 @@ +#!/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" + +# Detect C library type (musl for Alpine, gnu for Debian/Ubuntu) +if [ -f "/lib/ld-musl-x86_64.so.1" ] || [ -f "/lib/ld-musl-aarch64.so.1" ]; then + clib_type=musl +else + clib_type=gnu +fi + +# Detect architecture +architecture="$(uname -m)" +case $architecture in + x86_64) + arch="x86_64" + ;; + aarch64) + arch="aarch64" + ;; + *) + echo "(!) Architecture ${architecture} is not supported" + exit 1 + ;; +esac + +# Build asset regex to match prek release binaries +# Example: prek-x86_64-unknown-linux-gnu.tar.gz +asset_regex="^prek-${arch}-unknown-linux-${clib_type}\\.tar\\.gz$" + +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1.0.26" \ + --option repo='j178/prek' \ + --option binaryNames='prek' \ + --option version="$VERSION" \ + --option assetRegex="$asset_regex" + +echo 'Done!' diff --git a/src/prek/library_scripts.sh b/src/prek/library_scripts.sh new file mode 100644 index 000000000..d8354337c --- /dev/null +++ b/src/prek/library_scripts.sh @@ -0,0 +1,170 @@ + +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="" + + _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 + } + + _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 + } + + _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 + } + + _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 + + 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 == $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 + export ${variable_name}=$__nanolayer_location + +} + diff --git a/test/prek/scenarios.json b/test/prek/scenarios.json new file mode 100644 index 000000000..d7d1daef1 --- /dev/null +++ b/test/prek/scenarios.json @@ -0,0 +1,8 @@ +{ + "test_defaults_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "prek": {} + } + } +} diff --git a/test/prek/test_defaults_debian.sh b/test/prek/test_defaults_debian.sh new file mode 100755 index 000000000..2f7b21a34 --- /dev/null +++ b/test/prek/test_defaults_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "prek --version" prek --version + +reportResults From 2a2d3a55ea281059e4a2a2287ef3fbb9e6a91e58 Mon Sep 17 00:00:00 2001 From: lalten Date: Fri, 3 Apr 2026 16:22:14 +0200 Subject: [PATCH 50/71] Remove leading newlines from prek files --- src/prek/README.md | 1 - src/prek/library_scripts.sh | 1 - 2 files changed, 2 deletions(-) diff --git a/src/prek/README.md b/src/prek/README.md index 9cc9bc697..5d9d6127d 100644 --- a/src/prek/README.md +++ b/src/prek/README.md @@ -1,4 +1,3 @@ - # prek (via Github Releases) prek is a faster, dependency-free alternative to pre-commit, re-engineered in Rust. diff --git a/src/prek/library_scripts.sh b/src/prek/library_scripts.sh index d8354337c..08abda9c9 100644 --- a/src/prek/library_scripts.sh +++ b/src/prek/library_scripts.sh @@ -1,4 +1,3 @@ - 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 From 18e4df83fad0c3c483ce3772acf5709e740619e0 Mon Sep 17 00:00:00 2001 From: lalten Date: Sat, 4 Apr 2026 01:46:14 +0200 Subject: [PATCH 51/71] add specific version test --- test/prek/scenarios.json | 8 ++++++++ test/prek/test_specific_version.sh | 9 +++++++++ 2 files changed, 17 insertions(+) create mode 100755 test/prek/test_specific_version.sh diff --git a/test/prek/scenarios.json b/test/prek/scenarios.json index d7d1daef1..ce24f0077 100644 --- a/test/prek/scenarios.json +++ b/test/prek/scenarios.json @@ -4,5 +4,13 @@ "features": { "prek": {} } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "prek": { + "version": "0.3.7" + } + } } } diff --git a/test/prek/test_specific_version.sh b/test/prek/test_specific_version.sh new file mode 100755 index 000000000..69bf613df --- /dev/null +++ b/test/prek/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "prek version is equal to 0.3.7" sh -c "prek --version | grep '0.3.7'" + +reportResults From 061fa63f4e5ff58df1e4388a98109aff46c03df9 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 4 Apr 2026 10:26:33 +0200 Subject: [PATCH 52/71] test with `0.3.6` --- test/prek/scenarios.json | 2 +- test/prek/test_specific_version.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/prek/scenarios.json b/test/prek/scenarios.json index ce24f0077..995efe310 100644 --- a/test/prek/scenarios.json +++ b/test/prek/scenarios.json @@ -9,7 +9,7 @@ "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { "prek": { - "version": "0.3.7" + "version": "0.3.6" } } } diff --git a/test/prek/test_specific_version.sh b/test/prek/test_specific_version.sh index 69bf613df..6fefebb50 100755 --- a/test/prek/test_specific_version.sh +++ b/test/prek/test_specific_version.sh @@ -4,6 +4,6 @@ set -e source dev-container-features-test-lib -check "prek version is equal to 0.3.7" sh -c "prek --version | grep '0.3.7'" +check "prek version is equal to 0.3.6" sh -c "prek --version | grep '0.3.6'" reportResults From 2372dd97329c0c4bc6579bfbf125f90b52e462c4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 5 Apr 2026 10:54:17 +0000 Subject: [PATCH 53/71] feat(gofumpt): init Generated using boilerplate templates. --- src/gofumpt/devcontainer-feature.json | 18 +++ src/gofumpt/install.sh | 21 ++++ src/gofumpt/library_scripts.sh | 173 ++++++++++++++++++++++++++ test/gofumpt/scenarios.json | 16 +++ test/gofumpt/test.sh | 9 ++ test/gofumpt/test_debian.sh | 9 ++ test/gofumpt/test_specific_version.sh | 9 ++ 7 files changed, 255 insertions(+) create mode 100644 src/gofumpt/devcontainer-feature.json create mode 100755 src/gofumpt/install.sh create mode 100644 src/gofumpt/library_scripts.sh create mode 100644 test/gofumpt/scenarios.json create mode 100755 test/gofumpt/test.sh create mode 100755 test/gofumpt/test_debian.sh create mode 100755 test/gofumpt/test_specific_version.sh diff --git a/src/gofumpt/devcontainer-feature.json b/src/gofumpt/devcontainer-feature.json new file mode 100644 index 000000000..b7ab1991a --- /dev/null +++ b/src/gofumpt/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "gofumpt", + "version": "1.0.0", + "name": "gofumpt (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/gofumpt", + "description": "A stricter gofmt", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/gofumpt/install.sh b/src/gofumpt/install.sh new file mode 100755 index 000000000..757067be9 --- /dev/null +++ b/src/gofumpt/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='mvdan/gofumpt' --option binaryNames='gofumpt' --option version="$VERSION" + +echo 'Done!' diff --git a/src/gofumpt/library_scripts.sh b/src/gofumpt/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/gofumpt/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/gofumpt/scenarios.json b/test/gofumpt/scenarios.json new file mode 100644 index 000000000..ddb717af8 --- /dev/null +++ b/test/gofumpt/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "gofumpt": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "gofumpt": { + "version": "0.9.1" + } + } + } +} diff --git a/test/gofumpt/test.sh b/test/gofumpt/test.sh new file mode 100755 index 000000000..5b32aea25 --- /dev/null +++ b/test/gofumpt/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "gofumpt is installed" gofumpt --version + +reportResults diff --git a/test/gofumpt/test_debian.sh b/test/gofumpt/test_debian.sh new file mode 100755 index 000000000..5b32aea25 --- /dev/null +++ b/test/gofumpt/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "gofumpt is installed" gofumpt --version + +reportResults diff --git a/test/gofumpt/test_specific_version.sh b/test/gofumpt/test_specific_version.sh new file mode 100755 index 000000000..cc1d6d3bb --- /dev/null +++ b/test/gofumpt/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "gofumpt version is equal to 0.9.1" sh -c "gofumpt --version | grep '0.9.1'" + +reportResults From 434b9b09c2949d8f905a805b6b412aa333e6eac9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 5 Apr 2026 10:58:06 +0000 Subject: [PATCH 54/71] feat(golangci-lint): init Generated using boilerplate templates. --- src/golangci-lint/devcontainer-feature.json | 18 ++ src/golangci-lint/install.sh | 21 +++ src/golangci-lint/library_scripts.sh | 173 ++++++++++++++++++++ test/golangci-lint/scenarios.json | 16 ++ test/golangci-lint/test.sh | 9 + test/golangci-lint/test_debian.sh | 9 + test/golangci-lint/test_specific_version.sh | 9 + 7 files changed, 255 insertions(+) create mode 100644 src/golangci-lint/devcontainer-feature.json create mode 100755 src/golangci-lint/install.sh create mode 100644 src/golangci-lint/library_scripts.sh create mode 100644 test/golangci-lint/scenarios.json create mode 100755 test/golangci-lint/test.sh create mode 100755 test/golangci-lint/test_debian.sh create mode 100755 test/golangci-lint/test_specific_version.sh diff --git a/src/golangci-lint/devcontainer-feature.json b/src/golangci-lint/devcontainer-feature.json new file mode 100644 index 000000000..da686f2d9 --- /dev/null +++ b/src/golangci-lint/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "golangci-lint", + "version": "1.0.0", + "name": "golangci-lint (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/golangci-lint", + "description": "Fast linters runner for Go", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/golangci-lint/install.sh b/src/golangci-lint/install.sh new file mode 100755 index 000000000..e88801f01 --- /dev/null +++ b/src/golangci-lint/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='golangci/golangci-lint' --option binaryNames='golangci-lint' --option version="$VERSION" + +echo 'Done!' diff --git a/src/golangci-lint/library_scripts.sh b/src/golangci-lint/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/golangci-lint/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/golangci-lint/scenarios.json b/test/golangci-lint/scenarios.json new file mode 100644 index 000000000..e02c2fb21 --- /dev/null +++ b/test/golangci-lint/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "golangci-lint": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "golangci-lint": { + "version": "2.11.3" + } + } + } +} diff --git a/test/golangci-lint/test.sh b/test/golangci-lint/test.sh new file mode 100755 index 000000000..0308f30a2 --- /dev/null +++ b/test/golangci-lint/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "golangci-lint is installed" golangci-lint --version + +reportResults diff --git a/test/golangci-lint/test_debian.sh b/test/golangci-lint/test_debian.sh new file mode 100755 index 000000000..0308f30a2 --- /dev/null +++ b/test/golangci-lint/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "golangci-lint is installed" golangci-lint --version + +reportResults diff --git a/test/golangci-lint/test_specific_version.sh b/test/golangci-lint/test_specific_version.sh new file mode 100755 index 000000000..e673274ff --- /dev/null +++ b/test/golangci-lint/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "golangci-lint version is equal to 2.11.3" sh -c "golangci-lint --version | grep '2.11.3'" + +reportResults From 5363113cb487fc7a4f0c3c3f31fcfa853d9abd31 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:29:47 +0200 Subject: [PATCH 55/71] feat(claude-code): install from script as npm is deprecated by Anthropic --- src/claude-code/bootstrap.sh | 158 ++++++++++++++++++++ src/claude-code/devcontainer-feature.json | 4 +- src/claude-code/install.sh | 23 +-- src/claude-code/library_scripts.sh | 173 ---------------------- 4 files changed, 168 insertions(+), 190 deletions(-) create mode 100644 src/claude-code/bootstrap.sh delete mode 100644 src/claude-code/library_scripts.sh diff --git a/src/claude-code/bootstrap.sh b/src/claude-code/bootstrap.sh new file mode 100644 index 000000000..5cc7babab --- /dev/null +++ b/src/claude-code/bootstrap.sh @@ -0,0 +1,158 @@ +#!/bin/bash + +set -e + +# Parse command line arguments +TARGET="$1" # Optional target parameter + +# Validate target if provided +if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^(stable|latest|[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?)$ ]]; then + echo "Usage: $0 [stable|latest|VERSION]" >&2 + exit 1 +fi + +GCS_BUCKET="https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases" +DOWNLOAD_DIR="$HOME/.claude/downloads" + +# Check for required dependencies +DOWNLOADER="" +if command -v curl >/dev/null 2>&1; then + DOWNLOADER="curl" +elif command -v wget >/dev/null 2>&1; then + DOWNLOADER="wget" +else + echo "Either curl or wget is required but neither is installed" >&2 + exit 1 +fi + +# Check if jq is available (optional) +HAS_JQ=false +if command -v jq >/dev/null 2>&1; then + HAS_JQ=true +fi + +# Download function that works with both curl and wget +download_file() { + local url="$1" + local output="$2" + + if [ "$DOWNLOADER" = "curl" ]; then + if [ -n "$output" ]; then + curl -fsSL -o "$output" "$url" + else + curl -fsSL "$url" + fi + elif [ "$DOWNLOADER" = "wget" ]; then + if [ -n "$output" ]; then + wget -q -O "$output" "$url" + else + wget -q -O - "$url" + fi + else + return 1 + fi +} + +# Simple JSON parser for extracting checksum when jq is not available +get_checksum_from_manifest() { + local json="$1" + local platform="$2" + + # Normalize JSON to single line and extract checksum + json=$(echo "$json" | tr -d '\n\r\t' | sed 's/ \+/ /g') + + # Extract checksum for platform using bash regex + if [[ $json =~ \"$platform\"[^}]*\"checksum\"[[:space:]]*:[[:space:]]*\"([a-f0-9]{64})\" ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + + return 1 +} + +# Detect platform +case "$(uname -s)" in + Darwin) os="darwin" ;; + Linux) os="linux" ;; + MINGW*|MSYS*|CYGWIN*) echo "Windows is not supported by this script. See https://code.claude.com/docs for installation options." >&2; exit 1 ;; + *) echo "Unsupported operating system: $(uname -s). See https://code.claude.com/docs for supported platforms." >&2; exit 1 ;; +esac + +case "$(uname -m)" in + x86_64|amd64) arch="x64" ;; + arm64|aarch64) arch="arm64" ;; + *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; +esac + +# Detect Rosetta 2 on macOS: if the shell is running as x64 under Rosetta on an ARM Mac, +# download the native arm64 binary instead of the x64 one +if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then + if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then + arch="arm64" + fi +fi + +# Check for musl on Linux and adjust platform accordingly +if [ "$os" = "linux" ]; then + if [ -f /lib/libc.musl-x86_64.so.1 ] || [ -f /lib/libc.musl-aarch64.so.1 ] || ldd /bin/ls 2>&1 | grep -q musl; then + platform="linux-${arch}-musl" + else + platform="linux-${arch}" + fi +else + platform="${os}-${arch}" +fi +mkdir -p "$DOWNLOAD_DIR" + +# Always download latest version (which has the most up-to-date installer) +version=$(download_file "$GCS_BUCKET/latest") + +# Download manifest and extract checksum +manifest_json=$(download_file "$GCS_BUCKET/$version/manifest.json") + +# Use jq if available, otherwise fall back to pure bash parsing +if [ "$HAS_JQ" = true ]; then + checksum=$(echo "$manifest_json" | jq -r ".platforms[\"$platform\"].checksum // empty") +else + checksum=$(get_checksum_from_manifest "$manifest_json" "$platform") +fi + +# Validate checksum format (SHA256 = 64 hex characters) +if [ -z "$checksum" ] || [[ ! "$checksum" =~ ^[a-f0-9]{64}$ ]]; then + echo "Platform $platform not found in manifest" >&2 + exit 1 +fi + +# Download and verify +binary_path="$DOWNLOAD_DIR/claude-$version-$platform" +if ! download_file "$GCS_BUCKET/$version/$platform/claude" "$binary_path"; then + echo "Download failed" >&2 + rm -f "$binary_path" + exit 1 +fi + +# Pick the right checksum tool +if [ "$os" = "darwin" ]; then + actual=$(shasum -a 256 "$binary_path" | cut -d' ' -f1) +else + actual=$(sha256sum "$binary_path" | cut -d' ' -f1) +fi + +if [ "$actual" != "$checksum" ]; then + echo "Checksum verification failed" >&2 + rm -f "$binary_path" + exit 1 +fi + +chmod +x "$binary_path" + +# Run claude install to set up launcher and shell integration +echo "Setting up Claude Code..." +"$binary_path" install ${TARGET:+"$TARGET"} + +# Clean up downloaded file +rm -f "$binary_path" + +echo "" +echo "✅ Installation complete!" +echo "" diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index 281bec53c..1e970d379 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -1,7 +1,7 @@ { "id": "claude-code", - "version": "1.0.0", - "name": "claude-code (via npm)", + "version": "2.0.0", + "name": "claude-code", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/claude-code", "description": "Claude Code is an agentic coding tool that lives in your terminal", "options": { diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh index adc2d3820..ecc6b50df 100644 --- a/src/claude-code/install.sh +++ b/src/claude-code/install.sh @@ -2,20 +2,13 @@ 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" - -# Example nanolayer installation via devcontainer-feature -$nanolayer_location \ - install \ - devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/npm-package:1.0.4" \ - --option package='@anthropic-ai/claude-code' --option version="$VERSION" +# Install via Anthropic's recommended method +# Script downloaded from them: +# https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/bootstrap.sh +bash "$(dirname "$0")/bootstrap.sh" "$VERSION" + +# Copy the binary to /usr/local/bin for global access +# It's installed in /root/.local/bin/claude and there's no option to override the install location +cp "$HOME/.local/bin/claude" /usr/local/bin/claude echo 'Done!' diff --git a/src/claude-code/library_scripts.sh b/src/claude-code/library_scripts.sh deleted file mode 100644 index f6d0760d7..000000000 --- a/src/claude-code/library_scripts.sh +++ /dev/null @@ -1,173 +0,0 @@ -#!/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 - -} From 36b88e42802ab6f1310dfaf693fd928fb5908da1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 11 Apr 2026 07:16:21 +0000 Subject: [PATCH 56/71] feat(archon): init Generated using boilerplate templates. --- src/archon/devcontainer-feature.json | 18 +++ src/archon/install.sh | 21 ++++ src/archon/library_scripts.sh | 173 +++++++++++++++++++++++++++ test/archon/scenarios.json | 16 +++ test/archon/test.sh | 9 ++ test/archon/test_debian.sh | 9 ++ test/archon/test_specific_version.sh | 9 ++ 7 files changed, 255 insertions(+) create mode 100644 src/archon/devcontainer-feature.json create mode 100755 src/archon/install.sh create mode 100644 src/archon/library_scripts.sh create mode 100644 test/archon/scenarios.json create mode 100755 test/archon/test.sh create mode 100755 test/archon/test_debian.sh create mode 100755 test/archon/test_specific_version.sh diff --git a/src/archon/devcontainer-feature.json b/src/archon/devcontainer-feature.json new file mode 100644 index 000000000..7fdedbb3f --- /dev/null +++ b/src/archon/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "archon", + "version": "1.0.0", + "name": "archon (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/archon", + "description": "The first open-source harness builder for AI coding. Make AI coding deterministic and repeatable.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/archon/install.sh b/src/archon/install.sh new file mode 100755 index 000000000..94ce02311 --- /dev/null +++ b/src/archon/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='coleam00/Archon' --option binaryNames='archon' --option version="$VERSION" + +echo 'Done!' diff --git a/src/archon/library_scripts.sh b/src/archon/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/archon/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/archon/scenarios.json b/test/archon/scenarios.json new file mode 100644 index 000000000..ab5f5292f --- /dev/null +++ b/test/archon/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "archon": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "archon": { + "version": "0.3.4" + } + } + } +} diff --git a/test/archon/test.sh b/test/archon/test.sh new file mode 100755 index 000000000..2739ca851 --- /dev/null +++ b/test/archon/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "archon is installed" archon version + +reportResults diff --git a/test/archon/test_debian.sh b/test/archon/test_debian.sh new file mode 100755 index 000000000..2739ca851 --- /dev/null +++ b/test/archon/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "archon is installed" archon version + +reportResults diff --git a/test/archon/test_specific_version.sh b/test/archon/test_specific_version.sh new file mode 100755 index 000000000..68dd898f3 --- /dev/null +++ b/test/archon/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "archon version is equal to 0.3.4" sh -c "archon version | grep '0.3.4'" + +reportResults From 576fcf3dc9dfe9eec84e38695fb65f27bac79bdd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 10 Apr 2026 08:49:15 +0000 Subject: [PATCH 57/71] feat(worktrunk): init Generated using boilerplate templates. --- src/worktrunk/devcontainer-feature.json | 18 +++ src/worktrunk/install.sh | 22 +++ src/worktrunk/library_scripts.sh | 173 ++++++++++++++++++++++++ test/worktrunk/scenarios.json | 16 +++ test/worktrunk/test.sh | 9 ++ test/worktrunk/test_debian.sh | 9 ++ test/worktrunk/test_specific_version.sh | 9 ++ 7 files changed, 256 insertions(+) create mode 100644 src/worktrunk/devcontainer-feature.json create mode 100755 src/worktrunk/install.sh create mode 100644 src/worktrunk/library_scripts.sh create mode 100644 test/worktrunk/scenarios.json create mode 100755 test/worktrunk/test.sh create mode 100755 test/worktrunk/test_debian.sh create mode 100755 test/worktrunk/test_specific_version.sh diff --git a/src/worktrunk/devcontainer-feature.json b/src/worktrunk/devcontainer-feature.json new file mode 100644 index 000000000..47811b0f1 --- /dev/null +++ b/src/worktrunk/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "worktrunk", + "version": "1.0.0", + "name": "worktrunk (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/worktrunk", + "description": "Worktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/worktrunk/install.sh b/src/worktrunk/install.sh new file mode 100755 index 000000000..efdd12f8b --- /dev/null +++ b/src/worktrunk/install.sh @@ -0,0 +1,22 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='max-sixty/worktrunk' --option binaryNames='wt' --option version="$VERSION" \ + --option assetRegex='worktrunk.*(tar).*' + +echo 'Done!' diff --git a/src/worktrunk/library_scripts.sh b/src/worktrunk/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/worktrunk/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/worktrunk/scenarios.json b/test/worktrunk/scenarios.json new file mode 100644 index 000000000..a40ab0053 --- /dev/null +++ b/test/worktrunk/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "worktrunk": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "worktrunk": { + "version": "0.35.2" + } + } + } +} diff --git a/test/worktrunk/test.sh b/test/worktrunk/test.sh new file mode 100755 index 000000000..b04829f67 --- /dev/null +++ b/test/worktrunk/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "wt is installed" wt --version + +reportResults diff --git a/test/worktrunk/test_debian.sh b/test/worktrunk/test_debian.sh new file mode 100755 index 000000000..b04829f67 --- /dev/null +++ b/test/worktrunk/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "wt is installed" wt --version + +reportResults diff --git a/test/worktrunk/test_specific_version.sh b/test/worktrunk/test_specific_version.sh new file mode 100755 index 000000000..8a47888e5 --- /dev/null +++ b/test/worktrunk/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "wt version is equal to 0.35.2" sh -c "wt --version 2>&1 | grep '0.35.2'" + +reportResults From e19435b45a8f026beb476bee073f73ae391d3b52 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 10 May 2026 10:25:02 +0000 Subject: [PATCH 58/71] feat(opengrep): init Generated using boilerplate templates. --- src/opengrep/devcontainer-feature.json | 18 +++ src/opengrep/install.sh | 21 +++ src/opengrep/library_scripts.sh | 173 +++++++++++++++++++++++++ test/opengrep/scenarios.json | 16 +++ test/opengrep/test.sh | 9 ++ test/opengrep/test_debian.sh | 9 ++ test/opengrep/test_specific_version.sh | 9 ++ 7 files changed, 255 insertions(+) create mode 100644 src/opengrep/devcontainer-feature.json create mode 100755 src/opengrep/install.sh create mode 100644 src/opengrep/library_scripts.sh create mode 100644 test/opengrep/scenarios.json create mode 100755 test/opengrep/test.sh create mode 100755 test/opengrep/test_debian.sh create mode 100755 test/opengrep/test_specific_version.sh diff --git a/src/opengrep/devcontainer-feature.json b/src/opengrep/devcontainer-feature.json new file mode 100644 index 000000000..594ddc49c --- /dev/null +++ b/src/opengrep/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "opengrep", + "version": "1.0.0", + "name": "opengrep (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/opengrep", + "description": "Static code analysis engine to find security issues in code.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/opengrep/install.sh b/src/opengrep/install.sh new file mode 100755 index 000000000..3a6013285 --- /dev/null +++ b/src/opengrep/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='opengrep/opengrep' --option binaryNames='opengrep' --option version="$VERSION" + +echo 'Done!' diff --git a/src/opengrep/library_scripts.sh b/src/opengrep/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/opengrep/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/opengrep/scenarios.json b/test/opengrep/scenarios.json new file mode 100644 index 000000000..666eabe99 --- /dev/null +++ b/test/opengrep/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "opengrep": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "opengrep": { + "version": "1.18.0" + } + } + } +} diff --git a/test/opengrep/test.sh b/test/opengrep/test.sh new file mode 100755 index 000000000..54223790c --- /dev/null +++ b/test/opengrep/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opengrep is installed" opengrep --version + +reportResults diff --git a/test/opengrep/test_debian.sh b/test/opengrep/test_debian.sh new file mode 100755 index 000000000..54223790c --- /dev/null +++ b/test/opengrep/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opengrep is installed" opengrep --version + +reportResults diff --git a/test/opengrep/test_specific_version.sh b/test/opengrep/test_specific_version.sh new file mode 100755 index 000000000..d7b816f74 --- /dev/null +++ b/test/opengrep/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opengrep version is equal to 1.18.0" sh -c "opengrep --version | grep '1.18.0'" + +reportResults From 2de1553da54e1e15fe826e024e31e9d835c498f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 10:38:59 +0000 Subject: [PATCH 59/71] fix(opengrep): use assetRegex to avoid ambiguous asset matches Excludes .tar.gz and .cert assets, keeping only the plain manylinux binary. https://claude.ai/code/session_01S5yubvvERsngN7BHF2Am7b --- src/opengrep/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/opengrep/install.sh b/src/opengrep/install.sh index 3a6013285..9b7efc1e1 100755 --- a/src/opengrep/install.sh +++ b/src/opengrep/install.sh @@ -16,6 +16,6 @@ $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1" \ - --option repo='opengrep/opengrep' --option binaryNames='opengrep' --option version="$VERSION" + --option repo='opengrep/opengrep' --option binaryNames='opengrep' --option version="$VERSION" --option assetRegex='opengrep_manylinux_[^.]*$' echo 'Done!' From f95a96742ea4c975070a8fa0af1d6ff922a0dbd4 Mon Sep 17 00:00:00 2001 From: Stephen Rudolph Date: Tue, 12 May 2026 09:33:06 -0500 Subject: [PATCH 60/71] chore: Caddy install.sh dependency updates Only the Go version was strictly necessary due to https://github.com/golangci/golangci-lint/issues/6555, but bumping the others while here. Signed-off-by: Stephen Rudolph --- src/caddy/install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/caddy/install.sh b/src/caddy/install.sh index d5d7f9357..fd47c6175 100755 --- a/src/caddy/install.sh +++ b/src/caddy/install.sh @@ -8,13 +8,13 @@ set -e # `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.4" +ensure_nanolayer nanolayer_location "v0.5.6" $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers/features/go:1.1.3" \ + "ghcr.io/devcontainers/features/go:1.3.4" \ --option version="$GOLANGVERSION" @@ -22,7 +22,7 @@ $nanolayer_location \ $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/gh-release:1.0.25" \ + "ghcr.io/devcontainers-extra/features/gh-release:1.0.26" \ --option repo='caddyserver/caddy' --option binaryNames='caddy' --option version="$VERSION" From a075d8fc7dad82e20d6fddb1e79d14a99949e417 Mon Sep 17 00:00:00 2001 From: Stephen Rudolph Date: Tue, 12 May 2026 14:02:52 -0500 Subject: [PATCH 61/71] fix: updating the Caddy feature version because the dependencies were updated Signed-off-by: Stephen Rudolph --- src/caddy/devcontainer-feature.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/caddy/devcontainer-feature.json b/src/caddy/devcontainer-feature.json index e61553eb1..be9dd4712 100644 --- a/src/caddy/devcontainer-feature.json +++ b/src/caddy/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "caddy", - "version": "1.0.10", + "version": "1.0.11", "name": "Caddy (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/caddy", "description": "Caddy is a powerful, enterprise-ready, open source web server with automatic HTTPS.", From 3acf26818f3dfab084d69e3dad045949568e9b1f Mon Sep 17 00:00:00 2001 From: Sudhanva Sreesha Date: Sat, 16 May 2026 19:14:09 -0400 Subject: [PATCH 62/71] Add `bazel-compile-commands` --- src/bazel-compile-commands/README.md | 42 ++++++++ .../devcontainer-feature.json | 21 ++++ src/bazel-compile-commands/install.sh | 52 +++++++++ src/bazel-compile-commands/library_scripts.sh | 100 ++++++++++++++++++ test/bazel-compile-commands/scenarios.json | 25 +++++ test/bazel-compile-commands/test.sh | 10 ++ test/bazel-compile-commands/test_debian.sh | 10 ++ .../test_specific_version.sh | 11 ++ 8 files changed, 271 insertions(+) create mode 100644 src/bazel-compile-commands/README.md create mode 100644 src/bazel-compile-commands/devcontainer-feature.json create mode 100755 src/bazel-compile-commands/install.sh create mode 100644 src/bazel-compile-commands/library_scripts.sh create mode 100644 test/bazel-compile-commands/scenarios.json create mode 100755 test/bazel-compile-commands/test.sh create mode 100755 test/bazel-compile-commands/test_debian.sh create mode 100644 test/bazel-compile-commands/test_specific_version.sh diff --git a/src/bazel-compile-commands/README.md b/src/bazel-compile-commands/README.md new file mode 100644 index 000000000..dcb9da00c --- /dev/null +++ b/src/bazel-compile-commands/README.md @@ -0,0 +1,42 @@ +# bazel-compile-commands (via Github Releases) (bazel-compile-commands) + +Generates `compile_commands.json` from Bazel builds, enabling accurate code intelligence (go-to-definition, cross-references, diagnostics) in editors that support the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). + +## Example Usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/bazel-compile-commands:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------- | ------ | ------------- | +| version | Version to install. Accepts `latest`, a numeric version like `0.22.4`, or the full release tag `bazel-compile-commands-v0.22.4`. | string | latest | + +## Install behaviour + +The feature selects the best available package for the current environment: + +| Environment | Package used | +| -------------------- | ------------------------------- | +| Ubuntu Noble (24.04) | Native `.deb` via `apt-get` | +| Other Linux | Generic `.zip` (amd64 or arm64) | +| macOS | Universal `.zip` | + +## Pairing with Bazel + +This feature pairs naturally with [`ghcr.io/devcontainers-community/features/bazel:1`](https://github.com/devcontainers-community/features-bazel), which installs Bazelisk and Buildifier. When both are present in `devcontainer.json`, the Bazel feature is installed first. + +```json +"features": { + "ghcr.io/devcontainers-community/features/bazel:1": {}, + "ghcr.io/devcontainers-extra/features/bazel-compile-commands:1": {} +} +``` + +--- + +_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/bazel-compile-commands/devcontainer-feature.json b/src/bazel-compile-commands/devcontainer-feature.json new file mode 100644 index 000000000..d8149697b --- /dev/null +++ b/src/bazel-compile-commands/devcontainer-feature.json @@ -0,0 +1,21 @@ +{ + "id": "bazel-compile-commands", + "version": "1.0.0", + "name": "bazel-compile-commands (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/bazel-compile-commands", + "description": "Generates compile_commands.json from Bazel builds. Installs the appropriate binary for the current platform and CPU architecture: a native .deb on Ubuntu Noble (24.04), a generic zip on other Linux distros, and the macOS universal binary on Darwin.", + "options": { + "version": { + "default": "latest", + "description": "Version to install. Accepts 'latest', a numeric version like '0.22.4', or the full release tag 'bazel-compile-commands-v0.22.4'.", + "proposals": [ + "latest", + "0.22.4" + ], + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-community/features/bazel" + ] +} diff --git a/src/bazel-compile-commands/install.sh b/src/bazel-compile-commands/install.sh new file mode 100755 index 000000000..73525fe22 --- /dev/null +++ b/src/bazel-compile-commands/install.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +BINARY_NAME="bazel-compile-commands" +GITHUB_REPO="kiron1/bazel-compile-commands" +VERSION="${VERSION:-"latest"}" + +PLATFORM="$(detect_platform)" +ARCH="$(detect_arch)" +ensure_packages curl ca-certificates +TAG="$(resolve_tag "${VERSION}" "${GITHUB_REPO}" "${BINARY_NAME}")" + +# Extract the numeric version from the tag (e.g. "bazel-compile-commands-v0.22.4" → "0.22.4") +FILE_VERSION="${TAG#${BINARY_NAME}-v}" + +BASE_URL="https://github.com/${GITHUB_REPO}/releases/download/${TAG}" + +echo "Installing ${BINARY_NAME} ${TAG} for ${PLATFORM}/${ARCH}..." + +USE_DEB=false +ZIP_ASSET="" + +if [ "${PLATFORM}" = "linux" ]; then + # On Ubuntu Noble (24.04), prefer the native .deb package; fall back to the generic .zip. + if command -v apt-get >/dev/null 2>&1 && \ + [ -f /etc/os-release ] && \ + grep -qi "noble" /etc/os-release; then + USE_DEB=true + else + ZIP_ASSET="linux_${ARCH}" + fi +elif [ "${PLATFORM}" = "macos" ]; then + ZIP_ASSET="macos_universal" +fi + +if [ "${USE_DEB}" = true ]; then + echo "Ubuntu Noble detected – installing via .deb package..." + TMP_DEB=$(mktemp --suffix=.deb) + curl -fsSL "${BASE_URL}/${BINARY_NAME}_${FILE_VERSION}-noble_${ARCH}.deb" -o "${TMP_DEB}" + apt_get_update + apt-get install -y "${TMP_DEB}" + rm -f "${TMP_DEB}" + apt_cleanup +else + echo "Installing via .zip (${ZIP_ASSET})..." + install_via_zip "${BASE_URL}/${BINARY_NAME}_${FILE_VERSION}-${ZIP_ASSET}.zip" "${BINARY_NAME}" +fi + +echo "Done! ${BINARY_NAME} installed to /usr/local/bin/${BINARY_NAME}" diff --git a/src/bazel-compile-commands/library_scripts.sh b/src/bazel-compile-commands/library_scripts.sh new file mode 100644 index 000000000..9a09c2232 --- /dev/null +++ b/src/bazel-compile-commands/library_scripts.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# Adapted from https://github.com/devcontainers-community/features-bazel/blob/main/lib.sh +# which itself traces back to the official devcontainers/features node install script. + +# Runs apt-get update only when the apt lists cache is empty, avoiding redundant network hits. +apt_get_update() { + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi +} + +# Installs the given packages only if they are not already installed (checked via dpkg). +# Debian/Ubuntu only; calls apt_get_update lazily rather than unconditionally. +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update + apt-get -y install --no-install-recommends "$@" + fi +} + +# Removes apt list files to keep the container layer small. +apt_cleanup() { + rm -rf /var/lib/apt/lists/* +} + +# Ensures the given packages are installed, using apt-get on Debian/Ubuntu or apk on Alpine. +ensure_packages() { + if command -v apt-get >/dev/null 2>&1; then + check_packages "$@" + elif command -v apk >/dev/null 2>&1; then + apk add --no-cache "$@" + else + echo "No supported package manager found to install: $*" >&2 + exit 1 + fi +} + +detect_platform() { + local os + os="$(uname -s)" + case "${os}" in + Linux) echo "linux" ;; + Darwin) echo "macos" ;; + *) echo "Unsupported platform: ${os}" >&2; exit 1 ;; + esac +} + +detect_arch() { + local arch + arch="$(uname -m)" + case "${arch}" in + x86_64) echo "amd64" ;; + aarch64 | arm64) echo "arm64" ;; + *) echo "Unsupported architecture: ${arch}" >&2; exit 1 ;; + esac +} + +# Resolves a user-provided version string to the exact GitHub release tag. +# Accepted inputs: "latest", "0.22.4", "v0.22.4", or "bazel-compile-commands-v0.22.4" +resolve_tag() { + local version="$1" + local repo="$2" + local tag_prefix="$3" # e.g. "bazel-compile-commands" + + if [ "${version}" = "latest" ]; then + curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" \ + | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' + elif [[ "${version}" == ${tag_prefix}-* ]]; then + echo "${version}" + elif [[ "${version}" == v* ]]; then + echo "${tag_prefix}-${version}" + else + echo "${tag_prefix}-v${version}" + fi +} + +# Downloads a GitHub release zip asset, extracts a named binary, and places it in /usr/local/bin. +# +# Usage: install_via_zip +# +# zip_url - Direct URL to the .zip asset on GitHub Releases. +# binary_name - Name of the binary inside the archive (e.g. "bazel-compile-commands"). +# +# unzip is auto-installed if not already present, and the temporary download +# directory is cleaned up on exit. +install_via_zip() { + local zip_url="$1" + local binary_name="$2" + + ensure_packages unzip + + local tmp_dir + tmp_dir=$(mktemp -d) + curl -fsSL "${zip_url}" -o "${tmp_dir}/${binary_name}.zip" + unzip -j "${tmp_dir}/${binary_name}.zip" -d "${tmp_dir}" + install -m 0755 "${tmp_dir}/${binary_name}" /usr/local/bin/ + rm -rf "${tmp_dir}" +} diff --git a/test/bazel-compile-commands/scenarios.json b/test/bazel-compile-commands/scenarios.json new file mode 100644 index 000000000..a893dbd73 --- /dev/null +++ b/test/bazel-compile-commands/scenarios.json @@ -0,0 +1,25 @@ +{ + "test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "ghcr.io/devcontainers-community/features/bazel:1": {}, + "bazel-compile-commands": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "ghcr.io/devcontainers-community/features/bazel:1": {}, + "bazel-compile-commands": { + "version": "0.22.4" + } + } + }, + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "ghcr.io/devcontainers-community/features/bazel:1": {}, + "bazel-compile-commands": {} + } + } +} diff --git a/test/bazel-compile-commands/test.sh b/test/bazel-compile-commands/test.sh new file mode 100755 index 000000000..2b2cfd07a --- /dev/null +++ b/test/bazel-compile-commands/test.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "bazel-compile-commands is installed" bazel-compile-commands --version +check "bazelisk is installed" bazelisk --version + +reportResults diff --git a/test/bazel-compile-commands/test_debian.sh b/test/bazel-compile-commands/test_debian.sh new file mode 100755 index 000000000..2b2cfd07a --- /dev/null +++ b/test/bazel-compile-commands/test_debian.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "bazel-compile-commands is installed" bazel-compile-commands --version +check "bazelisk is installed" bazelisk --version + +reportResults diff --git a/test/bazel-compile-commands/test_specific_version.sh b/test/bazel-compile-commands/test_specific_version.sh new file mode 100644 index 000000000..a42376b83 --- /dev/null +++ b/test/bazel-compile-commands/test_specific_version.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +which bazel-compile-commands +check "bazel-compile-commands version is 0.22.4" sh -c "bazel-compile-commands --version 2>&1 | grep '0.22.4'" +check "bazelisk is installed" bazelisk --version + +reportResults From d0b4881e87f98c0c84856d9f18115fcc20547d07 Mon Sep 17 00:00:00 2001 From: Sudhanva Sreesha Date: Sun, 17 May 2026 15:10:24 +0000 Subject: [PATCH 63/71] Refactor: Smplify `install.sh` to use `gh-release` pattern and standarize `library_scripts.sh`. --- .../devcontainer-feature.json | 4 +- src/bazel-compile-commands/install.sh | 70 ++--- src/bazel-compile-commands/library_scripts.sh | 239 ++++++++++++------ 3 files changed, 182 insertions(+), 131 deletions(-) diff --git a/src/bazel-compile-commands/devcontainer-feature.json b/src/bazel-compile-commands/devcontainer-feature.json index d8149697b..7d1963345 100644 --- a/src/bazel-compile-commands/devcontainer-feature.json +++ b/src/bazel-compile-commands/devcontainer-feature.json @@ -3,11 +3,11 @@ "version": "1.0.0", "name": "bazel-compile-commands (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/bazel-compile-commands", - "description": "Generates compile_commands.json from Bazel builds. Installs the appropriate binary for the current platform and CPU architecture: a native .deb on Ubuntu Noble (24.04), a generic zip on other Linux distros, and the macOS universal binary on Darwin.", + "description": "Generates compile_commands.json from Bazel builds.", "options": { "version": { "default": "latest", - "description": "Version to install. Accepts 'latest', a numeric version like '0.22.4', or the full release tag 'bazel-compile-commands-v0.22.4'.", + "description": "Version to install. Accepts 'latest' or a numeric version like '0.22.4'.", "proposals": [ "latest", "0.22.4" diff --git a/src/bazel-compile-commands/install.sh b/src/bazel-compile-commands/install.sh index 73525fe22..de53af507 100755 --- a/src/bazel-compile-commands/install.sh +++ b/src/bazel-compile-commands/install.sh @@ -2,51 +2,29 @@ set -e -source ./library_scripts.sh - -BINARY_NAME="bazel-compile-commands" -GITHUB_REPO="kiron1/bazel-compile-commands" -VERSION="${VERSION:-"latest"}" - -PLATFORM="$(detect_platform)" -ARCH="$(detect_arch)" -ensure_packages curl ca-certificates -TAG="$(resolve_tag "${VERSION}" "${GITHUB_REPO}" "${BINARY_NAME}")" - -# Extract the numeric version from the tag (e.g. "bazel-compile-commands-v0.22.4" → "0.22.4") -FILE_VERSION="${TAG#${BINARY_NAME}-v}" - -BASE_URL="https://github.com/${GITHUB_REPO}/releases/download/${TAG}" - -echo "Installing ${BINARY_NAME} ${TAG} for ${PLATFORM}/${ARCH}..." - -USE_DEB=false -ZIP_ASSET="" - -if [ "${PLATFORM}" = "linux" ]; then - # On Ubuntu Noble (24.04), prefer the native .deb package; fall back to the generic .zip. - if command -v apt-get >/dev/null 2>&1 && \ - [ -f /etc/os-release ] && \ - grep -qi "noble" /etc/os-release; then - USE_DEB=true - else - ZIP_ASSET="linux_${ARCH}" - fi -elif [ "${PLATFORM}" = "macos" ]; then - ZIP_ASSET="macos_universal" -fi - -if [ "${USE_DEB}" = true ]; then - echo "Ubuntu Noble detected – installing via .deb package..." - TMP_DEB=$(mktemp --suffix=.deb) - curl -fsSL "${BASE_URL}/${BINARY_NAME}_${FILE_VERSION}-noble_${ARCH}.deb" -o "${TMP_DEB}" - apt_get_update - apt-get install -y "${TMP_DEB}" - rm -f "${TMP_DEB}" - apt_cleanup -else - echo "Installing via .zip (${ZIP_ASSET})..." - install_via_zip "${BASE_URL}/${BINARY_NAME}_${FILE_VERSION}-${ZIP_ASSET}.zip" "${BINARY_NAME}" +. ./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" + +# Upstream tags use a non-standard "bazel-compile-commands-v" format. Normalise +# any plain version (e.g. "0.22.4" or "v0.22.4") so nanolayer can match it exactly. +if [[ "${VERSION}" != "latest" && "${VERSION}" != bazel-compile-commands-* ]]; then + VERSION="bazel-compile-commands-v${VERSION#v}" fi -echo "Done! ${BINARY_NAME} installed to /usr/local/bin/${BINARY_NAME}" +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='kiron1/bazel-compile-commands' \ + --option binaryNames='bazel-compile-commands' \ + --option version="$VERSION" \ + --option assetRegex='linux_' \ + --option releaseTagRegex='bazel-compile-commands-v' + +echo 'Done!' diff --git a/src/bazel-compile-commands/library_scripts.sh b/src/bazel-compile-commands/library_scripts.sh index 9a09c2232..f6d0760d7 100644 --- a/src/bazel-compile-commands/library_scripts.sh +++ b/src/bazel-compile-commands/library_scripts.sh @@ -1,100 +1,173 @@ #!/usr/bin/env bash -# Adapted from https://github.com/devcontainers-community/features-bazel/blob/main/lib.sh -# which itself traces back to the official devcontainers/features node install script. +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 -# Runs apt-get update only when the apt lists cache is empty, avoiding redundant network hits. -apt_get_update() { - if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then - echo "Running apt-get update..." + 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 - fi -} + 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 + } -# Installs the given packages only if they are not already installed (checked via dpkg). -# Debian/Ubuntu only; calls apt_get_update lazily rather than unconditionally. -check_packages() { - if ! dpkg -s "$@" > /dev/null 2>&1; then - apt_get_update - apt-get -y install --no-install-recommends "$@" + 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 -} -# Removes apt list files to keep the container layer small. -apt_cleanup() { - rm -rf /var/lib/apt/lists/* -} + # 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 -# Ensures the given packages are installed, using apt-get on Debian/Ubuntu or apk on Alpine. -ensure_packages() { - if command -v apt-get >/dev/null 2>&1; then - check_packages "$@" - elif command -v apk >/dev/null 2>&1; then - apk add --no-cache "$@" + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location else - echo "No supported package manager found to install: $*" >&2 - exit 1 + curl -sfL $url -o $output_location fi -} -detect_platform() { - local os - os="$(uname -s)" - case "${os}" in - Linux) echo "linux" ;; - Darwin) echo "macos" ;; - *) echo "Unsupported platform: ${os}" >&2; exit 1 ;; - esac -} + # 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 -detect_arch() { - local arch - arch="$(uname -m)" - case "${arch}" in - x86_64) echo "amd64" ;; - aarch64 | arm64) echo "arm64" ;; - *) echo "Unsupported architecture: ${arch}" >&2; exit 1 ;; - esac } -# Resolves a user-provided version string to the exact GitHub release tag. -# Accepted inputs: "latest", "0.22.4", "v0.22.4", or "bazel-compile-commands-v0.22.4" -resolve_tag() { - local version="$1" - local repo="$2" - local tag_prefix="$3" # e.g. "bazel-compile-commands" - - if [ "${version}" = "latest" ]; then - curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" \ - | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' - elif [[ "${version}" == ${tag_prefix}-* ]]; then - echo "${version}" - elif [[ "${version}" == v* ]]; then - echo "${tag_prefix}-${version}" - else - echo "${tag_prefix}-v${version}" +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 -} -# Downloads a GitHub release zip asset, extracts a named binary, and places it in /usr/local/bin. -# -# Usage: install_via_zip -# -# zip_url - Direct URL to the .zip asset on GitHub Releases. -# binary_name - Name of the binary inside the archive (e.g. "bazel-compile-commands"). -# -# unzip is auto-installed if not already present, and the temporary download -# directory is cleaned up on exit. -install_via_zip() { - local zip_url="$1" - local binary_name="$2" - - ensure_packages unzip - - local tmp_dir - tmp_dir=$(mktemp -d) - curl -fsSL "${zip_url}" -o "${tmp_dir}/${binary_name}.zip" - unzip -j "${tmp_dir}/${binary_name}.zip" -d "${tmp_dir}" - install -m 0755 "${tmp_dir}/${binary_name}" /usr/local/bin/ - rm -rf "${tmp_dir}" + 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 + } From c00fcc4b480d80ae09f9bf5ae99b19cef57f2a52 Mon Sep 17 00:00:00 2001 From: Oscar Brouwer Date: Wed, 20 May 2026 14:40:18 +0000 Subject: [PATCH 64/71] Create new feature for TFLint --- src/tflint/README.md | 24 ++++ src/tflint/devcontainer-feature.json | 20 ++++ src/tflint/install.sh | 21 ++++ src/tflint/library_scripts.sh | 173 +++++++++++++++++++++++++++ test/tflint/scenarios.json | 16 +++ test/tflint/test.sh | 9 ++ test/tflint/test_default.sh | 9 ++ test/tflint/test_specific_version.sh | 9 ++ 8 files changed, 281 insertions(+) create mode 100644 src/tflint/README.md create mode 100644 src/tflint/devcontainer-feature.json create mode 100755 src/tflint/install.sh create mode 100644 src/tflint/library_scripts.sh create mode 100644 test/tflint/scenarios.json create mode 100755 test/tflint/test.sh create mode 100755 test/tflint/test_default.sh create mode 100755 test/tflint/test_specific_version.sh diff --git a/src/tflint/README.md b/src/tflint/README.md new file mode 100644 index 000000000..507b023bc --- /dev/null +++ b/src/tflint/README.md @@ -0,0 +1,24 @@ + +# TFLint (via Github Releases) (tflint) + +TFLint is a framework and each feature is provided by plugins, the key features are as follows: + +- Find possible errors (like invalid instance types) for Major Cloud providers (AWS/Azure/GCP). +- Warn about deprecated syntax, unused declarations. +- Enforce best practices, naming conventions. + +## Example Usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/tflint:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|------------|--------------------------------|--------|---------------| +| version | Select the version to install. | string | latest | + +--- diff --git a/src/tflint/devcontainer-feature.json b/src/tflint/devcontainer-feature.json new file mode 100644 index 000000000..88865b937 --- /dev/null +++ b/src/tflint/devcontainer-feature.json @@ -0,0 +1,20 @@ +{ + "id": "tflint", + "version": "1.0.0", + "name": "tflint (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/tflint", + "description": "A Pluggable Terraform Linter", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-extra/features/gh-release" + ] +} \ No newline at end of file diff --git a/src/tflint/install.sh b/src/tflint/install.sh new file mode 100755 index 000000000..bae05e0ed --- /dev/null +++ b/src/tflint/install.sh @@ -0,0 +1,21 @@ +#!/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" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='terraform-linters/tflint' --option binaryNames='tflint' --option version="$VERSION" + +echo 'Done!' diff --git a/src/tflint/library_scripts.sh b/src/tflint/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/tflint/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/tflint/scenarios.json b/test/tflint/scenarios.json new file mode 100644 index 000000000..75c49af43 --- /dev/null +++ b/test/tflint/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_default": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "tflint": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "tflint": { + "version": "0.62.1" + } + } + } +} \ No newline at end of file diff --git a/test/tflint/test.sh b/test/tflint/test.sh new file mode 100755 index 000000000..70c491bac --- /dev/null +++ b/test/tflint/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something is installed" tflint --version + +reportResults diff --git a/test/tflint/test_default.sh b/test/tflint/test_default.sh new file mode 100755 index 000000000..96f1aaddd --- /dev/null +++ b/test/tflint/test_default.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "tflint is installed" tflint --version + +reportResults diff --git a/test/tflint/test_specific_version.sh b/test/tflint/test_specific_version.sh new file mode 100755 index 000000000..dd7e01ce9 --- /dev/null +++ b/test/tflint/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "tflint version is equal to 0.62.1" sh -c "tflint --version | grep '0.62.1'" + +reportResults From 44042adaf239dc74edf9c446c08e2cd91b08e9f6 Mon Sep 17 00:00:00 2001 From: Oscar Brouwer Date: Thu, 21 May 2026 07:49:50 +0200 Subject: [PATCH 65/71] Use another version than the latest Co-authored-by: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> Signed-off-by: Oscar Brouwer --- test/tflint/scenarios.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tflint/scenarios.json b/test/tflint/scenarios.json index 75c49af43..7cb7266ef 100644 --- a/test/tflint/scenarios.json +++ b/test/tflint/scenarios.json @@ -9,7 +9,7 @@ "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { "tflint": { - "version": "0.62.1" + "version": "0.62.0" } } } From 1f4fe5162f245cc23124810c94fb982b16438694 Mon Sep 17 00:00:00 2001 From: Oscar Brouwer Date: Thu, 21 May 2026 07:50:00 +0200 Subject: [PATCH 66/71] Use another version than the latest Co-authored-by: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> Signed-off-by: Oscar Brouwer --- test/tflint/test_specific_version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tflint/test_specific_version.sh b/test/tflint/test_specific_version.sh index dd7e01ce9..742971673 100755 --- a/test/tflint/test_specific_version.sh +++ b/test/tflint/test_specific_version.sh @@ -4,6 +4,6 @@ set -e source dev-container-features-test-lib -check "tflint version is equal to 0.62.1" sh -c "tflint --version | grep '0.62.1'" +check "tflint version is equal to 0.62.0" sh -c "tflint --version | grep '0.62.0'" reportResults From 58b09f119c00287c854e4afc315d7d6324e7563e Mon Sep 17 00:00:00 2001 From: twsl <45483159+twsl@users.noreply.github.com> Date: Tue, 26 May 2026 22:45:14 +0200 Subject: [PATCH 67/71] Update download URL for claude-code releases Signed-off-by: twsl <45483159+twsl@users.noreply.github.com> --- src/claude-code/bootstrap.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/claude-code/bootstrap.sh b/src/claude-code/bootstrap.sh index 5cc7babab..375daea51 100644 --- a/src/claude-code/bootstrap.sh +++ b/src/claude-code/bootstrap.sh @@ -11,7 +11,7 @@ if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^(stable|latest|[0-9]+\.[0-9]+\.[0-9] exit 1 fi -GCS_BUCKET="https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases" +DOWNLOAD_BASE_URL="https://downloads.claude.ai/claude-code-releases" DOWNLOAD_DIR="$HOME/.claude/downloads" # Check for required dependencies @@ -105,10 +105,10 @@ fi mkdir -p "$DOWNLOAD_DIR" # Always download latest version (which has the most up-to-date installer) -version=$(download_file "$GCS_BUCKET/latest") +version=$(download_file "$DOWNLOAD_BASE_URL/latest") # Download manifest and extract checksum -manifest_json=$(download_file "$GCS_BUCKET/$version/manifest.json") +manifest_json=$(download_file "$DOWNLOAD_BASE_URL/$version/manifest.json") # Use jq if available, otherwise fall back to pure bash parsing if [ "$HAS_JQ" = true ]; then @@ -125,7 +125,7 @@ fi # Download and verify binary_path="$DOWNLOAD_DIR/claude-$version-$platform" -if ! download_file "$GCS_BUCKET/$version/$platform/claude" "$binary_path"; then +if ! download_file "$DOWNLOAD_BASE_URL/$version/$platform/claude" "$binary_path"; then echo "Download failed" >&2 rm -f "$binary_path" exit 1 From a87a829d008318159efad1c43a4c9469ecb03307 Mon Sep 17 00:00:00 2001 From: twsl <45483159+twsl@users.noreply.github.com> Date: Wed, 27 May 2026 10:01:22 +0200 Subject: [PATCH 68/71] Bump version from 2.0.0 to 2.0.1 Signed-off-by: twsl <45483159+twsl@users.noreply.github.com> --- src/claude-code/devcontainer-feature.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index 1e970d379..be0f0ab5d 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "claude-code", - "version": "2.0.0", + "version": "2.0.1", "name": "claude-code", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/claude-code", "description": "Claude Code is an agentic coding tool that lives in your terminal", @@ -22,4 +22,4 @@ } }, "installsAfter": [] -} \ No newline at end of file +} From 5962aef5dd561bc0d5fe8d634b43aab48d51ba53 Mon Sep 17 00:00:00 2001 From: Brian Helba Date: Wed, 17 Jun 2026 11:10:53 -0400 Subject: [PATCH 69/71] Fix claude-code installing files owned by root `bootstrap.sh` was running as root, so `~/.claude/`, `~/.local/bin/claude`, and `~/.local/share/claude/` all ended up under `/root/` owned by root. The devcontainer user had no access to config or shell integration. Use the devcontainer spec variables `_REMOTE_USER` and `_REMOTE_USER_HOME` to run the install as the devcontainer user, following the pattern used by `asdf-package`, `homebrew-package`, and other features in this repo. --- src/claude-code/devcontainer-feature.json | 2 +- src/claude-code/install.sh | 14 +++++++++++--- test/claude-code/test_debian.sh | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index be0f0ab5d..ffc36ad8d 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "claude-code", - "version": "2.0.1", + "version": "2.0.2", "name": "claude-code", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/claude-code", "description": "Claude Code is an agentic coding tool that lives in your terminal", diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh index ecc6b50df..1ab410ca0 100644 --- a/src/claude-code/install.sh +++ b/src/claude-code/install.sh @@ -5,10 +5,18 @@ set -e # Install via Anthropic's recommended method # Script downloaded from them: # https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/bootstrap.sh -bash "$(dirname "$0")/bootstrap.sh" "$VERSION" + +# Resolve the absolute path before su changes the working directory +BOOTSTRAP_SCRIPT="$(pwd)/bootstrap.sh" + +# Run as the devcontainer user so that ~/.claude/, ~/.local/bin/claude, and +# ~/.local/share/claude/ are owned by the right user, not root. +su - "$_REMOTE_USER" < Date: Mon, 29 Jun 2026 11:10:50 +0200 Subject: [PATCH 70/71] fix: install fails with unset _REMOTE_USER_HOME --- src/claude-code/devcontainer-feature.json | 2 +- src/claude-code/install.sh | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index ffc36ad8d..d0288726e 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "claude-code", - "version": "2.0.2", + "version": "2.0.3", "name": "claude-code", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/claude-code", "description": "Claude Code is an agentic coding tool that lives in your terminal", diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh index 1ab410ca0..e31ac039d 100644 --- a/src/claude-code/install.sh +++ b/src/claude-code/install.sh @@ -2,6 +2,13 @@ set -e +# Make sure that variable _REMOTE_USER_HOME is properly set +if [ -z "$_REMOTE_USER_HOME" ]; then + # shellcheck disable=2016 # this is intentional as the expansion should happen in the context of the new session + _REMOTE_USER_HOME="$(su - "$_REMOTE_USER" -c 'echo "$HOME"')" + export _REMOTE_USER_HOME +fi + # Install via Anthropic's recommended method # Script downloaded from them: # https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/bootstrap.sh From 7a2bdb8531643d2e356a214ad012b1ff9ea580e1 Mon Sep 17 00:00:00 2001 From: Kerolos Atallah Date: Wed, 5 Aug 2026 22:49:16 +0300 Subject: [PATCH 71/71] feat(outagedeck): add CLI feature --- src/outagedeck/README.md | 34 ++++++ src/outagedeck/devcontainer-feature.json | 21 ++++ src/outagedeck/install.sh | 19 ++++ src/outagedeck/library_scripts.sh | 131 +++++++++++++++++++++++ test/outagedeck/scenarios.json | 16 +++ test/outagedeck/test.sh | 9 ++ test/outagedeck/test_default.sh | 10 ++ test/outagedeck/test_specific_version.sh | 9 ++ 8 files changed, 249 insertions(+) create mode 100644 src/outagedeck/README.md create mode 100644 src/outagedeck/devcontainer-feature.json create mode 100755 src/outagedeck/install.sh create mode 100755 src/outagedeck/library_scripts.sh create mode 100644 test/outagedeck/scenarios.json create mode 100755 test/outagedeck/test.sh create mode 100755 test/outagedeck/test_default.sh create mode 100755 test/outagedeck/test_specific_version.sh diff --git a/src/outagedeck/README.md b/src/outagedeck/README.md new file mode 100644 index 000000000..5c0bbadc3 --- /dev/null +++ b/src/outagedeck/README.md @@ -0,0 +1,34 @@ +# OutageDeck CLI (via GitHub Releases) (outagedeck) + +Check the live status of 170+ cloud and SaaS providers from a terminal or CI +script. OutageDeck normalizes each vendor's official status feed; it does not +replace synthetic monitoring. + +## Example usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/outagedeck:1": {} +} +``` + +Then query one or more provider slugs: + +```bash +outagedeck status aws cloudflare github openai +``` + +For structured CI output and a nonzero exit code when a provider is down: + +```bash +outagedeck status --json --fail-on=outage aws github openai +``` + +Find more examples in the [OutageDeck CLI repository](https://github.com/outagedeck/cli) +or review the [API documentation](https://outagedeck.com/developers/api?utm_source=devcontainers-extra&utm_medium=feature&utm_campaign=devcontainer_feature). + +## Options + +| Options Id | Description | Type | Default Value | +|------------|----------------------------------------------|--------|---------------| +| version | Select the OutageDeck CLI version to install. | string | latest | diff --git a/src/outagedeck/devcontainer-feature.json b/src/outagedeck/devcontainer-feature.json new file mode 100644 index 000000000..c8d1c231a --- /dev/null +++ b/src/outagedeck/devcontainer-feature.json @@ -0,0 +1,21 @@ +{ + "id": "outagedeck", + "version": "1.0.0", + "name": "OutageDeck CLI (via GitHub Releases)", + "documentationURL": "https://github.com/devcontainers-extra/features/tree/main/src/outagedeck", + "description": "Check the live status of cloud and SaaS providers from a terminal or CI script.", + "options": { + "version": { + "default": "latest", + "description": "Select the OutageDeck CLI version to install.", + "proposals": [ + "latest", + "0.1.0" + ], + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-extra/features/gh-release" + ] +} diff --git a/src/outagedeck/install.sh b/src/outagedeck/install.sh new file mode 100755 index 000000000..fd472f850 --- /dev/null +++ b/src/outagedeck/install.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a CLI utility which keeps container layers as small as possible. +# Source: https://github.com/devcontainers-extra/nanolayer +ensure_nanolayer nanolayer_location "v0.5.6" + +"$nanolayer_location" \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='outagedeck/cli' \ + --option binaryNames='outagedeck' \ + --option version="$VERSION" + +echo 'Done!' diff --git a/src/outagedeck/library_scripts.sh b/src/outagedeck/library_scripts.sh new file mode 100755 index 000000000..98e0da825 --- /dev/null +++ b/src/outagedeck/library_scripts.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash + +clean_download() { + local url=$1 + local output_location=$2 + local tempdir + tempdir=$(mktemp -d) + local downloader_installed="" + + _apt_get_install() { + local apt_tempdir=$1 + cp -p -R /var/lib/apt/lists "$apt_tempdir" + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + _apt_get_cleanup() { + local apt_tempdir=$1 + apt-get -y purge wget --auto-remove + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists + mv "$apt_tempdir/lists" /var/lib/apt/lists + } + + _apk_install() { + local apk_tempdir=$1 + cp -p -R /var/cache/apk "$apk_tempdir" + apk add --no-cache wget + } + + _apk_cleanup() { + apk del wget + } + + local downloader + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + 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 + + if [ -n "$downloader_installed" ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup "$tempdir" + elif [ -x "/sbin/apk" ]; then + _apk_cleanup + fi + fi +} + +ensure_nanolayer() { + local variable_name=$1 + local required_version=$2 + + if [[ $required_version != v* ]]; then + required_version="v$required_version" + fi + + local resolved_nanolayer="" + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION:-}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION:-}" ]]; then + if type nanolayer >/dev/null 2>&1; then + resolved_nanolayer=nanolayer + fi + elif [[ -f "$NANOLAYER_CLI_LOCATION" && -x "$NANOLAYER_CLI_LOCATION" ]]; then + resolved_nanolayer=$NANOLAYER_CLI_LOCATION + fi + + if [[ -n "$resolved_nanolayer" ]]; then + local current_version + current_version=$($resolved_nanolayer --version) + if [[ $current_version != v* ]]; then + current_version="v$current_version" + fi + if [[ $current_version != "$required_version" ]]; then + resolved_nanolayer="" + fi + fi + fi + + if [[ -z "$resolved_nanolayer" ]]; then + if [[ "$(uname -sm)" != "Linux x86_64" && "$(uname -sm)" != "Linux aarch64" ]]; then + echo "No nanolayer binary for $(uname -sm)" + exit 1 + fi + + local nanolayer_tempdir + nanolayer_tempdir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + clean_up() { + local exit_code=$? + rm -rf "$nanolayer_tempdir" + exit "$exit_code" + } + trap clean_up EXIT + + local clib_type=gnu + if [ -x "/sbin/apk" ]; then + clib_type=musl + fi + local tar_filename="nanolayer-$(uname -m)-unknown-linux-$clib_type.tgz" + clean_download \ + "https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename" \ + "$nanolayer_tempdir/$tar_filename" + tar xfz "$nanolayer_tempdir/$tar_filename" -C "$nanolayer_tempdir" + chmod a+x "$nanolayer_tempdir/nanolayer" + resolved_nanolayer="$nanolayer_tempdir/nanolayer" + fi + + declare -g "$variable_name=$resolved_nanolayer" +} diff --git a/test/outagedeck/scenarios.json b/test/outagedeck/scenarios.json new file mode 100644 index 000000000..bb01553fa --- /dev/null +++ b/test/outagedeck/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_default": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "outagedeck": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "outagedeck": { + "version": "0.1.0" + } + } + } +} diff --git a/test/outagedeck/test.sh b/test/outagedeck/test.sh new file mode 100755 index 000000000..b0120b3d2 --- /dev/null +++ b/test/outagedeck/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "outagedeck is installed" outagedeck --version + +reportResults diff --git a/test/outagedeck/test_default.sh b/test/outagedeck/test_default.sh new file mode 100755 index 000000000..2602aad9b --- /dev/null +++ b/test/outagedeck/test_default.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "outagedeck is installed" outagedeck --version +check "outagedeck status command is available" sh -c "outagedeck help | grep 'outagedeck status'" + +reportResults diff --git a/test/outagedeck/test_specific_version.sh b/test/outagedeck/test_specific_version.sh new file mode 100755 index 000000000..d8f1a0491 --- /dev/null +++ b/test/outagedeck/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "outagedeck version is 0.1.0" sh -c "outagedeck --version | grep '0.1.0'" + +reportResults