From 4cf1ec6f17c0eec699ef861273f7bcf32b7ea60b Mon Sep 17 00:00:00 2001 From: Kolja Date: Wed, 21 Jan 2026 10:10:23 +0100 Subject: [PATCH 01/35] 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 02/35] 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 03/35] 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 04/35] 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 05/35] 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 06/35] 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 07/35] 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 08/35] 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 09/35] 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 10/35] 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 11/35] 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 12/35] 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 13/35] 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 14/35] 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 15/35] 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 16/35] 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 17/35] 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 18/35] 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 19/35] 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 20/35] 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 21/35] 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 22/35] 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 23/35] 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 24/35] 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 25/35] 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 26/35] 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 27/35] 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 28/35] 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 29/35] 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 30/35] 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 31/35] 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 32/35] 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 33/35] 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 34/35] 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 35/35] 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