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 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 diff --git a/.github/workflows/archive-feature.yaml b/.github/workflows/archive-feature.yaml new file mode 100644 index 000000000..56cc8c65d --- /dev/null +++ b/.github/workflows/archive-feature.yaml @@ -0,0 +1,48 @@ +name: Archive Feature + +on: + workflow_dispatch: + inputs: + feature_name: + description: 'Name of the feature to archive' + required: true + type: string + +jobs: + archive: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ secrets.ARCHIVE_TOKEN }} + + - name: Install just + uses: extractions/setup-just@v3 + + - name: Create branch for archiving + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b archive/${{ inputs.feature_name }} + + - name: Archive feature + run: just archive ${{ inputs.feature_name }} + + - name: Commit changes + run: | + git add -A + git commit -m "chore: archive feature ${{ inputs.feature_name }}" + + - name: Push changes + run: git push origin archive/${{ inputs.feature_name }} + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.ARCHIVE_TOKEN }} + run: | + gh pr create \ + --title "Archive feature: ${{ inputs.feature_name }}" \ + --body "This PR archives the \`${{ inputs.feature_name }}\` feature by moving it to the archive directory." \ + --base main \ + --head archive/${{ inputs.feature_name }} diff --git a/.github/workflows/generate-gh-feature.yml b/.github/workflows/generate-gh-feature.yml new file mode 100644 index 000000000..0b65374ad --- /dev/null +++ b/.github/workflows/generate-gh-feature.yml @@ -0,0 +1,144 @@ +name: Generate Feature from gh-release Template + +on: + workflow_dispatch: + inputs: + ID: + description: 'Feature identifier (e.g., akamai-cli)' + required: true + type: string + Description: + description: 'Feature description' + required: true + type: string + Repository: + description: 'GitHub repository in format "owner/repo" (e.g., akamai/cli)' + required: true + type: string + BinaryNames: + description: 'Binary names (comma-separated if multiple)' + required: true + type: string + BinaryVersionCommands: + description: 'Version check commands (comma-separated if multiple)' + required: false + type: string + default: '--version' + BinaryNonLatestVersions: + description: 'Non-latest versions for testing (comma-separated if multiple)' + required: true + type: string + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ secrets.ARCHIVE_TOKEN }} + + - name: Check if feature already exists + run: | + if [ -d "src/${{ inputs.ID }}" ] || [ -d "test/${{ inputs.ID }}" ]; then + echo "❌ Feature '${{ inputs.ID }}' already exists!" + echo "Please use a different feature ID or remove the existing feature first." + exit 1 + fi + echo "✅ Feature ID is available" + + - name: Validate GitHub repository exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if gh api repos/${{ inputs.Repository }} > /dev/null 2>&1; then + echo "✅ Repository '${{ inputs.Repository }}' exists and is accessible" + else + echo "⚠️ Warning: Could not verify repository '${{ inputs.Repository }}'" + echo "The repository might be private, not exist, or you don't have access to it." + echo "Continuing with feature generation..." + fi + + - name: Download boilerplate + run: | + wget -q https://github.com/gruntwork-io/boilerplate/releases/download/v0.10.1/boilerplate_linux_amd64 + chmod +x boilerplate_linux_amd64 + + - name: Create vars.yml + run: | + cat > vars.yml << 'EOF' + ID: ${{ inputs.ID }} + Description: ${{ inputs.Description }} + Repository: ${{ inputs.Repository }} + BinaryNames: ${{ inputs.BinaryNames }} + BinaryVersionCommands: ${{ inputs.BinaryVersionCommands }} + BinaryNonLatestVersions: ${{ inputs.BinaryNonLatestVersions }} + EOF + echo "📝 Created vars.yml:" + cat vars.yml + + - name: Create branch for new feature + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b feature/${{ inputs.ID }}-init + + - name: Generate feature files + run: | + echo "🔧 Generating feature files from templates/gh-release..." + ./boilerplate_linux_amd64 \ + --template-url templates/gh-release/ \ + --output-folder src/${{ inputs.ID }} \ + --var-file vars.yml \ + --non-interactive + + - name: Generate test files + run: | + echo "🧪 Generating test files from templates/test..." + ./boilerplate_linux_amd64 \ + --template-url templates/test/ \ + --output-folder test/${{ inputs.ID }} \ + --var-file vars.yml \ + --non-interactive + + - name: Commit changes + run: | + git add src/ test/ + git commit -m "feat(${{ inputs.ID }}): init + + Generated using boilerplate templates." + + - name: Push changes + run: git push origin feature/${{ inputs.ID }}-init + + - name: Create PR body file + run: | + cat > pr_body.md << 'EOF' + ## New Feature: `${{ inputs.ID }}` + + **Description:** ${{ inputs.Description }} + + **Repository:** [${{ inputs.Repository }}](https://github.com/${{ inputs.Repository }}) + + **Binaries:** `${{ inputs.BinaryNames }}` + + **Version Commands:** `${{ inputs.BinaryVersionCommands }}` + + **Test Versions:** `${{ inputs.BinaryNonLatestVersions }}` + + --- + + This PR was automatically generated using the boilerplate templates. + EOF + echo "📝 PR body content:" + cat pr_body.md + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.ARCHIVE_TOKEN }} + run: | + gh pr create \ + --title "feat(${{ inputs.ID }}): init" \ + --body-file pr_body.md \ + --base main \ + --head feature/${{ inputs.ID }}-init diff --git a/src/elasticsearch-asdf/README.md b/archive/src/elasticsearch-asdf/README.md similarity index 100% rename from src/elasticsearch-asdf/README.md rename to archive/src/elasticsearch-asdf/README.md diff --git a/src/elasticsearch-asdf/devcontainer-feature.json b/archive/src/elasticsearch-asdf/devcontainer-feature.json similarity index 100% rename from src/elasticsearch-asdf/devcontainer-feature.json rename to archive/src/elasticsearch-asdf/devcontainer-feature.json diff --git a/src/elasticsearch-asdf/install.sh b/archive/src/elasticsearch-asdf/install.sh similarity index 100% rename from src/elasticsearch-asdf/install.sh rename to archive/src/elasticsearch-asdf/install.sh diff --git a/src/elasticsearch-asdf/library_scripts.sh b/archive/src/elasticsearch-asdf/library_scripts.sh similarity index 100% rename from src/elasticsearch-asdf/library_scripts.sh rename to archive/src/elasticsearch-asdf/library_scripts.sh diff --git a/src/istioctl/README.md b/archive/src/istioctl/README.md similarity index 100% rename from src/istioctl/README.md rename to archive/src/istioctl/README.md diff --git a/src/istioctl/devcontainer-feature.json b/archive/src/istioctl/devcontainer-feature.json similarity index 100% rename from src/istioctl/devcontainer-feature.json rename to archive/src/istioctl/devcontainer-feature.json diff --git a/src/istioctl/install.sh b/archive/src/istioctl/install.sh similarity index 100% rename from src/istioctl/install.sh rename to archive/src/istioctl/install.sh diff --git a/archive/src/age-keygen/library_scripts.sh b/archive/src/istioctl/library_scripts.sh similarity index 100% rename from archive/src/age-keygen/library_scripts.sh rename to archive/src/istioctl/library_scripts.sh diff --git a/archive/test/btm/scenarios.json b/archive/test/btm/scenarios.json deleted file mode 100644 index 56389dc03..000000000 --- a/archive/test/btm/scenarios.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "test_defaults_debian": { - "image": "mcr.microsoft.com/devcontainers/base:debian", - "features": { - "btm": {} - } - } -} \ No newline at end of file diff --git a/test/elasticsearch-asdf/scenarios.json b/archive/test/elasticsearch-asdf/scenarios.json similarity index 100% rename from test/elasticsearch-asdf/scenarios.json rename to archive/test/elasticsearch-asdf/scenarios.json diff --git a/test/elasticsearch-asdf/test.sh b/archive/test/elasticsearch-asdf/test.sh similarity index 100% rename from test/elasticsearch-asdf/test.sh rename to archive/test/elasticsearch-asdf/test.sh diff --git a/test/istioctl/scenarios.json b/archive/test/istioctl/scenarios.json similarity index 100% rename from test/istioctl/scenarios.json rename to archive/test/istioctl/scenarios.json diff --git a/test/istioctl/test_defaults_debian.sh b/archive/test/istioctl/test_defaults_debian.sh similarity index 100% rename from test/istioctl/test_defaults_debian.sh rename to archive/test/istioctl/test_defaults_debian.sh 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}}/* diff --git a/archive/src/age-keygen/README.md b/src/age-keygen/README.md similarity index 100% rename from archive/src/age-keygen/README.md rename to src/age-keygen/README.md diff --git a/archive/src/age-keygen/devcontainer-feature.json b/src/age-keygen/devcontainer-feature.json similarity index 95% rename from archive/src/age-keygen/devcontainer-feature.json rename to src/age-keygen/devcontainer-feature.json index b40ee2be1..c12cb7489 100644 --- a/archive/src/age-keygen/devcontainer-feature.json +++ b/src/age-keygen/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "age-keygen", - "version": "1.0.15", + "version": "1.0.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.", diff --git a/archive/src/age-keygen/install.sh b/src/age-keygen/install.sh similarity index 74% rename from archive/src/age-keygen/install.sh rename to src/age-keygen/install.sh index 2a0e98201..4cf15f66c 100755 --- a/archive/src/age-keygen/install.sh +++ b/src/age-keygen/install.sh @@ -1,4 +1,3 @@ - set -e . ./library_scripts.sh @@ -8,16 +7,12 @@ set -e # `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, # and if missing - will download a temporary copy that automatically get deleted at the end # of the script -ensure_nanolayer nanolayer_location "v0.5.4" - +ensure_nanolayer nanolayer_location "v0.5.6" $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/gh-release:1.0.25" \ - --option repo='filosottile/age' --option binaryNames='age-keygen' --option version="$VERSION" - - + "ghcr.io/devcontainers-extra/features/gh-release:1.0.26" \ + --option repo='filosottile/age' --option binaryNames='age-keygen' --option version="$VERSION" --option assetRegex='.*(.tar.gz)$' echo 'Done!' - diff --git a/archive/src/btm/library_scripts.sh b/src/age-keygen/library_scripts.sh similarity index 100% rename from archive/src/btm/library_scripts.sh rename to src/age-keygen/library_scripts.sh 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 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/templates/src/library_scripts.sh b/src/archon/library_scripts.sh similarity index 100% rename from templates/src/library_scripts.sh rename to src/archon/library_scripts.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..7d1963345 --- /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.", + "options": { + "version": { + "default": "latest", + "description": "Version to install. Accepts 'latest' or a numeric version like '0.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..de53af507 --- /dev/null +++ b/src/bazel-compile-commands/install.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -e + +. ./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 + +$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 new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/bazel-compile-commands/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/src/boilerplate/devcontainer-feature.json b/src/boilerplate/devcontainer-feature.json new file mode 100644 index 000000000..78b2f99a2 --- /dev/null +++ b/src/boilerplate/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "boilerplate", + "version": "1.0.0", + "name": "boilerplate (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/boilerplate", + "description": "A tool for generating files and folders (boilerplate) from a set of templates", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/boilerplate/install.sh b/src/boilerplate/install.sh new file mode 100755 index 000000000..bedab30bb --- /dev/null +++ b/src/boilerplate/install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='gruntwork-io/boilerplate' --option binaryNames='boilerplate' --option version="$VERSION" + +echo 'Done!' diff --git a/src/boilerplate/library_scripts.sh b/src/boilerplate/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/boilerplate/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/archive/src/btm/README.md b/src/btm/README.md similarity index 100% rename from archive/src/btm/README.md rename to src/btm/README.md diff --git a/archive/src/btm/devcontainer-feature.json b/src/btm/devcontainer-feature.json similarity index 96% rename from archive/src/btm/devcontainer-feature.json rename to src/btm/devcontainer-feature.json index 8e9048345..e384107bd 100644 --- a/archive/src/btm/devcontainer-feature.json +++ b/src/btm/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "btm", - "version": "1.0.16", + "version": "1.0.17", "name": "bottom (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/btm", "description": "bottom is a customizable cross-platform graphical process/system monitor for the terminal.", diff --git a/src/btm/install.sh b/src/btm/install.sh new file mode 100755 index 000000000..b6cc158ad --- /dev/null +++ b/src/btm/install.sh @@ -0,0 +1,48 @@ +#!/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 +if [ -f "/lib/ld-musl-x86_64.so.1" ] || [ -f "/lib/ld-musl-aarch64.so.1" ]; then + clib_type=musl +else + clib_type=gnu +fi + +# Detect architecture +architecture="$(uname -m)" +case $architecture in + arm64) + arch="aarch64" + ;; + armv7) + clib_type="${clib_type}eabihf" + ;; + *) + arch="$architecture" + ;; +esac +asset_regex="^bottom_${arch}-unknown-linux-${clib_type}\\.tar\\.gz$" + +# Exclude nightly prereleases, only match semantic version tags +release_tag_regex="^(?!.*nightly)[0-9]+\\.[0-9]+\\.[0-9]+$" + +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1.0.26" \ + --option repo='ClementTsang/bottom' --option binaryNames='btm' --option version="$VERSION" --option assetRegex="$asset_regex" --option releaseTagRegex="$release_tag_regex" + + + +echo 'Done!' + diff --git a/src/btm/library_scripts.sh b/src/btm/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/btm/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/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..00921233b 100755 --- a/src/bun/install.sh +++ b/src/bun/install.sh @@ -4,67 +4,240 @@ set -e source ./library_scripts.sh +echo "Starting Bun installation" + # nanolayer is a cli utility which keeps container layers as small as possible # source code: https://github.com/devcontainers-extra/nanolayer # `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, # and if missing - will download a temporary copy that automatically get deleted at the end # of the script +echo "Ensuring nanolayer is available" +# Initialize variable to suppress shellcheck warning (assigned by ensure_nanolayer function) +nanolayer_location="" ensure_nanolayer nanolayer_location "v0.5.6" # Canonicalize VERSION for Bun's tag scheme when pinning -version_for_release="$VERSION" -if [ "${VERSION:-latest}" != "latest" ] && [ -n "$VERSION" ]; then - if [[ "$VERSION" =~ ^bun-v ]]; then - version_for_release="$VERSION" - elif [[ "$VERSION" =~ ^v ]]; then - version_for_release="bun-$VERSION" - else - version_for_release="bun-v$VERSION" +canonicalize_version() { + local version="$1" + + if [ "$version" = "latest" ] || [ -z "$version" ]; then + echo "latest" + return 0 + fi + + # Remove any leading/trailing whitespace + version="$(echo "$version" | xargs)" + + # Already in correct format + if [[ "$version" =~ ^bun-v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "$version" + return 0 fi -fi + + # Remove 'bun-' prefix if present + if [[ "$version" =~ ^bun- ]]; then + version="${version#bun-}" + fi + + # Handle versions starting with 'v' + if [[ "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "bun-$version" + return 0 + fi + + # Handle plain version numbers + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "bun-v$version" + return 0 + fi + + # Invalid version format + echo "WARNING: Invalid version format: '$version'. Using 'latest' instead." >&2 + echo "latest" +} + +# shellcheck disable=SC2153 +# VERSION is provided by the devcontainer features framework +version_for_release="$(canonicalize_version "$VERSION")" +echo "Using Bun version: $version_for_release" # Figure out arch and libc to disambiguate Bun's multiple Linux assets -arch_segment="" -case "$(uname -m)" in - x86_64) - arch_segment="x64" - ;; - aarch64|arm64) - arch_segment="aarch64" - ;; - *) - # Fallback to uname -m (unlikely used by bun release naming) - arch_segment="$(uname -m)" - ;; -esac - -# Detect musl (Alpine) vs glibc -libc_suffix="" -if [ -x "/sbin/apk" ]; then - libc_suffix="-musl" +detect_arch() { + local arch + arch="$(uname -m)" + + case "$arch" in + x86_64|amd64) + echo "x64" + ;; + aarch64|arm64) + echo "aarch64" + ;; + *) + # Log warning for unsupported architectures + echo "WARNING: Architecture '$arch' is not supported by Bun" >&2 + echo "WARNING: Bun only supports x64 and aarch64 architectures" >&2 + echo "WARNING: Available assets can be found at: https://github.com/oven-sh/bun/releases" >&2 + echo "$arch" + ;; + esac +} + +echo "Detecting system architecture" +if ! arch_segment="$(detect_arch)"; then + echo "ERROR: Failed to detect architecture" >&2 + exit 1 +fi + +# Detect musl vs glibc (robust detection across multiple distros) +detect_libc() { + # Method 1: Check for apk (Alpine) + if [ -x "/sbin/apk" ]; then + echo "-musl" + return 0 + fi + + # Method 2: Check ldd output for musl + if command -v ldd >/dev/null 2>&1; then + if ldd --version 2>&1 | grep -qi musl; then + echo "-musl" + return 0 + fi + fi + + # Method 3: Check for musl library files + if [ -f "/lib/ld-musl-x86_64.so.1" ] || [ -f "/lib/ld-musl-aarch64.so.1" ]; then + echo "-musl" + return 0 + fi + + # Method 4: Check /proc/mounts for musl + if grep -qi musl /proc/mounts 2>/dev/null; then + echo "-musl" + return 0 + fi + + # Default to glibc + echo "" +} + +echo "Detecting libc implementation" +if ! libc_suffix="$(detect_libc)"; then + echo "ERROR: Failed to detect libc implementation" >&2 + exit 1 fi -# Prefer baseline builds for widest CPU compatibility; exclude profile variants -# Examples matched: -# - bun-linux-x64-baseline.zip -# - bun-linux-x64-musl-baseline.zip -# - bun-linux-aarch64-baseline.zip -# - bun-linux-aarch64-musl-baseline.zip -asset_regex="^bun-linux-${arch_segment}${libc_suffix}-baseline\\.zip$" +# Build asset regex based on architecture and libc +# - x64: Use baseline builds for widest CPU compatibility +# - aarch64: Use non-baseline builds since baseline doesn't exist for ARM64 +if [ "$arch_segment" = "x64" ]; then + # For x64, try baseline first, then fallback to non-baseline + asset_regex="^bun-linux-x64${libc_suffix}-baseline\\.zip$" +else + # For aarch64, use non-baseline since baseline doesn't exist + asset_regex="^bun-linux-aarch64${libc_suffix}\\.zip$" +fi # Bun tags are of the form 'bun-vX.Y.Z'; constrain tag discovery accordingly release_tag_regex="^bun-v" -# Install Bun via gh-release helper with explicit asset/tag filters -$nanolayer_location \ - install \ - devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/gh-release:1" \ - --option repo='oven-sh/bun' \ - --option binaryNames='bun' \ - --option version="$version_for_release" \ - --option assetRegex="$asset_regex" \ - --option releaseTagRegex="$release_tag_regex" +# Try multiple asset regex patterns for robust fallback support +# Build the ordered list of asset regex patterns. +# For x64, we prefer "baseline" first for widest CPU compatibility across +# older/varied machines, then fall back to non-baseline. For aarch64, Bun does +# not publish baseline variants, so we use standard builds. +build_asset_patterns() { + local arch="$1" + local libc="$2" + + case "$arch" in + x64) + if [ "$libc" = "-musl" ]; then + echo "^bun-linux-x64-musl-baseline\\.zip$" + echo "^bun-linux-x64-musl\\.zip$" + else + echo "^bun-linux-x64-baseline\\.zip$" + echo "^bun-linux-x64\\.zip$" + fi + return 0 + ;; + aarch64) + if [ "$libc" = "-musl" ]; then + echo "^bun-linux-aarch64-musl\\.zip$" + else + echo "^bun-linux-aarch64\\.zip$" + fi + return 0 + ;; + *) + echo "ERROR: Unsupported architecture '$arch'" >&2 + echo "ERROR: Bun only supports x64 and aarch64 architectures" >&2 + echo "ERROR: Available assets can be found at: https://github.com/oven-sh/bun/releases" >&2 + return 1 + ;; + esac +} + +# Build asset patterns for current architecture +if ! asset_patterns_output=$(build_asset_patterns "$arch_segment" "$libc_suffix"); then + echo "ERROR: Failed to build asset patterns for arch=$arch_segment, libc_suffix=$libc_suffix" >&2 + exit 1 +fi + +# Convert to array - use a more reliable method +# Split by newlines and create array +asset_patterns=() +while IFS= read -r line; do + if [ -n "$line" ]; then + asset_patterns+=("$line") + fi +done <<< "$asset_patterns_output" + +# Verify we have asset patterns +if [ ${#asset_patterns[@]} -eq 0 ]; then + echo "ERROR: No asset patterns generated for arch=$arch_segment, libc_suffix=$libc_suffix" >&2 + echo "ERROR: asset_patterns_output was: '$asset_patterns_output'" >&2 + exit 1 +fi + +# Try each asset pattern until one succeeds +install_bun() { + if [ ${#asset_patterns[@]} -eq 0 ]; then + echo "ERROR: install_bun: No asset patterns provided!" >&2 + return 1 + fi + + for asset_regex in "${asset_patterns[@]}"; do + if $nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='oven-sh/bun' \ + --option binaryNames='bun' \ + --option version="$version_for_release" \ + --option assetRegex="$asset_regex" \ + --option releaseTagRegex="$release_tag_regex" 2>&1; then + return 0 + fi + done + + echo "ERROR: Failed to install Bun. No asset patterns matched." >&2 + echo "ERROR: Troubleshooting information:" >&2 + echo "ERROR: - Architecture: $arch_segment" >&2 + echo "ERROR: - Libc suffix: ${libc_suffix:-(none/glibc)}" >&2 + echo "ERROR: - Version: $version_for_release" >&2 + echo "ERROR: - Asset patterns tried: ${asset_patterns[*]}" >&2 + echo "ERROR: Check https://github.com/oven-sh/bun/releases for available assets" >&2 + return 1 +} + +# Attempt installation +if ! install_bun; then + echo "ERROR: install_bun function failed" >&2 + exit 1 +fi + +echo "Bun installation process completed" # Provide a convenient bunx shim if [ -x "/usr/local/bin/bun" ] && ! [ -x "/usr/local/bin/bunx" ]; then @@ -75,4 +248,4 @@ EOF chmod +x /usr/local/bin/bunx fi -echo 'Bun installation completed.' +echo "Bun installation completed successfully" 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.", 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" diff --git a/src/claude-code/README.md b/src/claude-code/README.md new file mode 100644 index 000000000..321958cd2 --- /dev/null +++ b/src/claude-code/README.md @@ -0,0 +1,30 @@ + +# claude-code (via npm) (claude-code) + +Claude Code is an agentic coding tool that lives in your terminal + +## Example Usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/claude-code:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| version | Select the version to install. | string | latest | + +## Customizations + +### VS Code Extensions + +- `anthropic.claude-code` + + + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/claude-code/bootstrap.sh b/src/claude-code/bootstrap.sh new file mode 100644 index 000000000..375daea51 --- /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 + +DOWNLOAD_BASE_URL="https://downloads.claude.ai/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 "$DOWNLOAD_BASE_URL/latest") + +# Download manifest and extract checksum +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 + 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 "$DOWNLOAD_BASE_URL/$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 new file mode 100644 index 000000000..d0288726e --- /dev/null +++ b/src/claude-code/devcontainer-feature.json @@ -0,0 +1,25 @@ +{ + "id": "claude-code", + "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", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "anthropic.claude-code" + ] + } + }, + "installsAfter": [] +} diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh new file mode 100644 index 000000000..e31ac039d --- /dev/null +++ b/src/claude-code/install.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +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 + +# 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" </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/src/dart/devcontainer-feature.json b/src/dart/devcontainer-feature.json new file mode 100644 index 000000000..878009f44 --- /dev/null +++ b/src/dart/devcontainer-feature.json @@ -0,0 +1,20 @@ +{ + "id": "dart", + "version": "1.0.0", + "name": "dart (via asdf)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/dart", + "description": "Dart is an approachable, portable, and productive programming language for high-quality apps on any platform", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-extra/features/asdf-package" + ] +} \ No newline at end of file diff --git a/archive/src/btm/install.sh b/src/dart/install.sh similarity index 61% rename from archive/src/btm/install.sh rename to src/dart/install.sh index 56871f3ba..b19da22a7 100755 --- a/archive/src/btm/install.sh +++ b/src/dart/install.sh @@ -1,23 +1,21 @@ +#!/usr/bin/env bash set -e -. ./library_scripts.sh +source ./library_scripts.sh # nanolayer is a cli utility which keeps container layers as small as possible # source code: https://github.com/devcontainers-extra/nanolayer # `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, # and if missing - will download a temporary copy that automatically get deleted at the end # of the script -ensure_nanolayer nanolayer_location "v0.5.4" - +ensure_nanolayer nanolayer_location "v0.5.6" +# Example nanolayer installation via devcontainer-feature $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/gh-release:1.0.25" \ - --option repo='ClementTsang/bottom' --option binaryNames='btm' --option version="$VERSION" --option assetRegex='^(?!.*(2-17))' - - + "ghcr.io/devcontainers-extra/features/asdf-package:1.0.10" \ + --option plugin='dart' --option version="$VERSION" echo 'Done!' - diff --git a/src/dart/library_scripts.sh b/src/dart/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/dart/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/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": { 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 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/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/src/heroku-cli/devcontainer-feature.json b/src/heroku-cli/devcontainer-feature.json index ea55b2f28..4c7b3abab 100644 --- a/src/heroku-cli/devcontainer-feature.json +++ b/src/heroku-cli/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "Heroku CLI (via cli-assets.heroku.com)", "id": "heroku-cli", - "version": "1.0.4", + "version": "1.0.5", "description": "Heroku CLI allows you to interact with the Heroku API via the command line (version 8 and above)", "documentationURL": "https://github.com/devcontainers-extra/features/tree/main/src/heroku-cli", "options": { diff --git a/src/heroku-cli/install.sh b/src/heroku-cli/install.sh index 7e715f56f..9700864f1 100755 --- a/src/heroku-cli/install.sh +++ b/src/heroku-cli/install.sh @@ -20,10 +20,10 @@ rm -rf /var/lib/apt/lists/* ARCH="$(uname -m)" if [ "$ARCH" == "x86_64" ]; then ARCH=x64 +elif [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then + ARCH=arm64 elif [[ "$ARCH" == aarch* ]]; then ARCH=arm -elif [[ "$ARCH" == "arm64" ]]; then - ARCH=arm64 else echo -e "unsupported arch: $ARCH" exit 1 diff --git a/src/infisical/README.md b/src/infisical/README.md new file mode 100644 index 000000000..eebb50384 --- /dev/null +++ b/src/infisical/README.md @@ -0,0 +1,27 @@ +# infisical (via Github Releases) (infisical) + +Infisical is an OSS tool for secret management. + +## Example Usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/infisical:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +| ---------- | ------------------------------ | ------ | ------------- | +| version | Select the version to install. | string | latest | + +## Customizations + +### VS Code Extensions + +- `infisical.vscode-infisical` + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/infisical/devcontainer-feature.json b/src/infisical/devcontainer-feature.json new file mode 100644 index 000000000..2eab61e83 --- /dev/null +++ b/src/infisical/devcontainer-feature.json @@ -0,0 +1,17 @@ +{ + "id": "infisical", + "version": "1.0.0", + "name": "infisical (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/infisical", + "description": "The official Infisical CLI: Inject secrets into applications and manage your Infisical infrastructure.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": ["latest"], + "type": "string" + } + }, + "installsAfter": [] +} + diff --git a/src/infisical/install.sh b/src/infisical/install.sh new file mode 100755 index 000000000..768859afb --- /dev/null +++ b/src/infisical/install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='infisical/cli' --option binaryNames='infisical' --option version="$VERSION" + +echo 'Done!' diff --git a/src/infisical/library_scripts.sh b/src/infisical/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/infisical/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/src/opencode/devcontainer-feature.json b/src/opencode/devcontainer-feature.json new file mode 100644 index 000000000..bbcfa16b8 --- /dev/null +++ b/src/opencode/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "opencode", + "version": "1.0.0", + "name": "opencode (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/opencode", + "description": "The open source coding agent.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/opencode/install.sh b/src/opencode/install.sh new file mode 100755 index 000000000..c47309c16 --- /dev/null +++ b/src/opencode/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='sst/opencode' --option binaryNames='opencode' --option version="$VERSION" \ + --option assetRegex='^(?!.*baseline).*$' + +echo 'Done!' diff --git a/src/opencode/library_scripts.sh b/src/opencode/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/opencode/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/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..9b7efc1e1 --- /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" --option assetRegex='opengrep_manylinux_[^.]*$' + +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/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/src/prek/README.md b/src/prek/README.md new file mode 100644 index 000000000..5d9d6127d --- /dev/null +++ b/src/prek/README.md @@ -0,0 +1,38 @@ +# 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/istioctl/library_scripts.sh b/src/prek/library_scripts.sh similarity index 99% rename from src/istioctl/library_scripts.sh rename to src/prek/library_scripts.sh index 9e1072eca..08abda9c9 100644 --- a/src/istioctl/library_scripts.sh +++ b/src/prek/library_scripts.sh @@ -1,5 +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 @@ -169,4 +167,3 @@ ensure_nanolayer() { } - diff --git a/src/ruff/README.md b/src/ruff/README.md index 6c052cea7..03c1e3fc8 100644 --- a/src/ruff/README.md +++ b/src/ruff/README.md @@ -1,5 +1,5 @@ -# Ruff (via pipx) (ruff) +# Ruff (via Github Releases) Ruff is an extremely fast Python linter, written in Rust. diff --git a/src/ruff/devcontainer-feature.json b/src/ruff/devcontainer-feature.json index 4df2188f7..dabdc6702 100644 --- a/src/ruff/devcontainer-feature.json +++ b/src/ruff/devcontainer-feature.json @@ -1,7 +1,7 @@ { "id": "ruff", - "version": "1.1.1", - "name": "Ruff (via pipx)", + "version": "2.0.0", + "name": "Ruff (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/ruff", "description": "Ruff is an extremely fast Python linter, written in Rust.", "options": { @@ -21,8 +21,5 @@ ] } }, - "installsAfter": [ - "ghcr.io/devcontainers-extra/features/pipx-package", - "ghcr.io/devcontainers/features/python" - ] + "installsAfter": [] } diff --git a/src/ruff/install.sh b/src/ruff/install.sh index 1c344caae..87b3ec2af 100755 --- a/src/ruff/install.sh +++ b/src/ruff/install.sh @@ -1,23 +1,21 @@ +#!/usr/bin/env bash set -e -. ./library_scripts.sh +source ./library_scripts.sh # nanolayer is a cli utility which keeps container layers as small as possible # source code: https://github.com/devcontainers-extra/nanolayer # `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, # and if missing - will download a temporary copy that automatically get deleted at the end # of the script -ensure_nanolayer nanolayer_location "v0.5.0" - +ensure_nanolayer nanolayer_location "v0.5.6" +# Example nanolayer installation via devcontainer-feature $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/pipx-package:1.1.8" \ - --option package='ruff' --option version="$VERSION" - - + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='astral-sh/ruff' --option binaryNames='ruff' --option version="$VERSION" echo 'Done!' - diff --git a/src/ruff/library_scripts.sh b/src/ruff/library_scripts.sh index 9e1072eca..f6d0760d7 100644 --- a/src/ruff/library_scripts.sh +++ b/src/ruff/library_scripts.sh @@ -1,4 +1,4 @@ - +#!/usr/bin/env bash clean_download() { # The purpose of this function is to download a file with minimal impact on container layer size @@ -15,7 +15,7 @@ clean_download() { tempdir=$(mktemp -d) downloader_installed="" - _apt_get_install() { + function _apt_get_install() { tempdir=$1 # copy current state of apt list - in order to revert back later (minimize contianer layer size) @@ -24,7 +24,7 @@ clean_download() { apt-get -y install --no-install-recommends wget ca-certificates } - _apt_get_cleanup() { + function _apt_get_cleanup() { tempdir=$1 echo "removing wget" @@ -35,15 +35,15 @@ clean_download() { rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists } - _apk_install() { + function _apk_install() { tempdir=$1 # copy current state of apk cache - in order to revert back later (minimize contianer layer size) cp -p -R /var/cache/apk $tempdir - apk add --no-cache wget + apk add --no-cache wget } - _apk_cleanup() { + function _apk_cleanup() { tempdir=$1 echo "removing wget" @@ -59,10 +59,10 @@ clean_download() { fi # in case none of them is installed, install wget temporarly - if [ -z $downloader ] ; then - if [ -x "/usr/bin/apt-get" ] ; then + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then _apt_get_install $tempdir - elif [ -x "/sbin/apk" ] ; then + elif [ -x "/sbin/apk" ]; then _apk_install $tempdir else echo "distro not supported" @@ -72,7 +72,7 @@ clean_download() { downloader_installed="true" fi - if [ $downloader = "wget" ] ; then + if [ $downloader = "wget" ]; then wget -q $url -O $output_location else curl -sfL $url -o $output_location @@ -80,10 +80,10 @@ clean_download() { # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because # alpine lack bash, and RETURN is not a valid signal under sh shell - if ! [ -z $downloader_installed ] ; then - if [ -x "/usr/bin/apt-get" ] ; then + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then _apt_get_cleanup $tempdir - elif [ -x "/sbin/apk" ] ; then + elif [ -x "/sbin/apk" ]; then _apk_cleanup $tempdir else echo "distro not supported" @@ -93,56 +93,60 @@ clean_download() { } - ensure_nanolayer() { # Ensure existance of the nanolayer cli program local variable_name=$1 local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi - local __nanolayer_location="" + local nanolayer_location="" # If possible - try to use an already installed nanolayer - if [ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]; then - if [ -z "${NANOLAYER_CLI_LOCATION}" ]; then + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then if type nanolayer >/dev/null 2>&1; then echo "Found a pre-existing nanolayer in PATH" - __nanolayer_location=nanolayer + nanolayer_location=nanolayer fi - elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ] ; then - __nanolayer_location=${NANOLAYER_CLI_LOCATION} - echo "Found a pre-existing nanolayer which were given in env variable: $__nanolayer_location" + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" fi # make sure its of the required version - if ! [ -z "${__nanolayer_location}" ]; then + if ! [[ -z "${nanolayer_location}" ]]; then local current_version - current_version=$($__nanolayer_location --version) - + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi if ! [ $current_version == $required_version ]; then echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" - __nanolayer_location="" + nanolayer_location="" fi fi fi # If not previuse installation found, download it temporarly and delete at the end of the script - if [ -z "${__nanolayer_location}" ]; then + if [[ -z "${nanolayer_location}" ]]; then - if [ "$(uname -sm)" = 'Linux x86_64' ] || [ "$(uname -sm)" = "Linux aarch64" ]; then + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) - clean_up () { + clean_up() { ARG=$? rm -rf $tmp_dir exit $ARG } trap clean_up EXIT - - if [ -x "/sbin/apk" ] ; then + if [ -x "/sbin/apk" ]; then clib_type=musl else clib_type=gnu @@ -155,8 +159,7 @@ ensure_nanolayer() { tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" chmod a+x $tmp_dir/nanolayer - __nanolayer_location=$tmp_dir/nanolayer - + nanolayer_location=$tmp_dir/nanolayer else echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" @@ -165,8 +168,6 @@ ensure_nanolayer() { fi # Expose outside the resolved location - export ${variable_name}=$__nanolayer_location + declare -g ${variable_name}=$nanolayer_location } - - diff --git a/src/shellcheck/devcontainer-feature.json b/src/shellcheck/devcontainer-feature.json index 12b7cd586..821947f9b 100644 --- a/src/shellcheck/devcontainer-feature.json +++ b/src/shellcheck/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "shellcheck", - "version": "1.0.0", + "version": "1.1.0", "name": "shellcheck (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/shellcheck", "description": "", @@ -15,4 +15,4 @@ } }, "installsAfter": [] -} \ No newline at end of file +} diff --git a/src/shellcheck/install.sh b/src/shellcheck/install.sh index 8dbf2ce1a..ecc24b3d5 100755 --- a/src/shellcheck/install.sh +++ b/src/shellcheck/install.sh @@ -16,6 +16,6 @@ $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1" \ - --option repo='koalaman/shellcheck' --option binaryNames='shellcheck' --option version="$VERSION" + --option repo='koalaman/shellcheck' --option binaryNames='shellcheck' --option version="$VERSION" --option assetRegex='shellcheck-.*.tar.xz$' echo 'Done!' diff --git a/src/talosctl/devcontainer-feature.json b/src/talosctl/devcontainer-feature.json index 25d9c6134..7d7e070c9 100644 --- a/src/talosctl/devcontainer-feature.json +++ b/src/talosctl/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "talosctl", - "version": "1.0.0", + "version": "1.0.1", "name": "talosctl (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/talosctl", "description": "A CLI tool that interfaces with the Talos Linux API.", diff --git a/src/talosctl/install.sh b/src/talosctl/install.sh index 70de85293..fd7dfe95a 100644 --- a/src/talosctl/install.sh +++ b/src/talosctl/install.sh @@ -16,6 +16,10 @@ $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1" \ - --option repo='siderolabs/talos' --option binaryNames='talosctl' --option version="$VERSION" --option releaseTagRegex='^(?!.*(alpha|beta)).*$' + --option repo='siderolabs/talos' \ + --option binaryNames='talosctl' \ + --option assetRegex='^.*(?/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/src/ty/devcontainer-feature.json b/src/ty/devcontainer-feature.json new file mode 100644 index 000000000..ba183c261 --- /dev/null +++ b/src/ty/devcontainer-feature.json @@ -0,0 +1,25 @@ +{ + "id": "ty", + "version": "1.0.0", + "name": "ty (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/ty", + "description": "An extremely fast Python type checker and language server, written in Rust.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "astral-sh.ty" + ] + } + }, + "installsAfter": [] +} diff --git a/src/ty/install.sh b/src/ty/install.sh new file mode 100755 index 000000000..e417b490c --- /dev/null +++ b/src/ty/install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='astral-sh/ty' --option binaryNames='ty' --option version="$VERSION" + +echo 'Done!' diff --git a/src/ty/library_scripts.sh b/src/ty/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/ty/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/src/uv-tool/devcontainer-feature.json b/src/uv-tool/devcontainer-feature.json new file mode 100644 index 000000000..2af62076b --- /dev/null +++ b/src/uv-tool/devcontainer-feature.json @@ -0,0 +1,42 @@ +{ + "id": "uv-tool", + "version": "1.0.0", + "name": "Python tool (via uv)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/uv-tool", + "description": "Installs a Python tool using uv.", + "options": { + "uv_version": { + "default": "latest", + "description": "The version of uv to install. Set to 'latest' to always get the latest version.", + "type": "string" + }, + "package": { + "default": "", + "description": "The package to install commands from.", + "type": "string" + }, + "from": { + "default": "", + "description": "Use the given package to provide the command. By default, the package name is assumed to match the command name.", + "type": "string" + }, + "with": { + "default": "", + "description": "Space-separated list of additional packages to include as dependencies (does not install their executables). Supports version constraints, e.g., 'mkdocs-material mkdocs-minify-plugin>=0.7,<0.8'", + "type": "string" + }, + "with-executables-from": { + "default": "", + "description": "Comma-separated list of packages to include as dependencies and install their executables, e.g., 'ansible-core,ansible-lint'", + "type": "string" + }, + "python": { + "default": "", + "description": "Specify the Python version to use.", + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-extra/features/uv" + ] +} diff --git a/src/uv-tool/install.sh b/src/uv-tool/install.sh new file mode 100755 index 000000000..0f1782401 --- /dev/null +++ b/src/uv-tool/install.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +set -e + +UV_VERSION="${UV_VERSION:-"latest"}" +PACKAGE="${PACKAGE:-""}" +FROM="${FROM:-""}" +WITH="${WITH:-""}" +WITH_EXECUTABLES_FROM="${WITH_EXECUTABLES_FROM:-""}" +PYTHON="${PYTHON:-""}" + +USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}" + +# Validate required parameters +if [ -z "${PACKAGE}" ]; then + echo "Error: PACKAGE option is required" + exit 1 +fi + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/uv:1" \ + --option version="$UV_VERSION" + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + possible_users=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for current_user in "${possible_users[@]}"; do + if id -u "${current_user}" > /dev/null 2>&1; then + USERNAME=${current_user} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u "${USERNAME}" > /dev/null 2>&1; then + USERNAME=root +fi + +uv_args=("${PACKAGE}") + +[ -n "${FROM}" ] && uv_args+=(--from "${FROM}") +[ -n "${WITH_EXECUTABLES_FROM}" ] && uv_args+=(--with-executables-from "${WITH_EXECUTABLES_FROM}") +[ -n "${PYTHON}" ] && uv_args+=(--python "${PYTHON}") + +# Convert space-delimited WITH packages to multiple --with flags +if [ -n "${WITH}" ]; then + read -ra with_array <<<"${WITH}" + for pkg in "${with_array[@]}"; do + [ -n "$pkg" ] && uv_args+=(--with "$pkg") + done +fi + +if [ "${USERNAME}" != "root" ] && [ "${USERNAME}" != "" ]; then + echo "Installing ${PACKAGE} for user ${USERNAME}..." + # Export the uv_args array as a properly quoted string + uv_cmd="uv tool install $(printf '%q ' "${uv_args[@]}")" + sudo -u "${USERNAME}" bash -c " + export PATH=\"/home/${USERNAME}/.local/bin:\$PATH\" + ${uv_cmd} + " +else + echo "Installing ${PACKAGE} for root..." + uv tool install "${uv_args[@]}" +fi + +echo 'Done!' diff --git a/src/uv-tool/library_scripts.sh b/src/uv-tool/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/uv-tool/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/src/uv/devcontainer-feature.json b/src/uv/devcontainer-feature.json index 7f266c238..ce9995d06 100644 --- a/src/uv/devcontainer-feature.json +++ b/src/uv/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "uv", - "version": "1.0.1", + "version": "1.0.2", "name": "uv (via GitHub Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/uv", "description": "An extremely fast Python package and project manager, written in Rust.", diff --git a/src/uv/install.sh b/src/uv/install.sh index c544d7b65..a2d76746a 100755 --- a/src/uv/install.sh +++ b/src/uv/install.sh @@ -16,7 +16,7 @@ ensure_nanolayer nanolayer_location "v0.5.6" arch_segment="" case "$(uname -m)" in x86_64) - arch_segment="x64" + arch_segment="x86_64" ;; aarch64|arm64) arch_segment="aarch64" @@ -25,7 +25,7 @@ case "$(uname -m)" in arch_segment="$(uname -m)" ;; esac -assetRegex="^uv-${arch_segment}-unknown-linux-gnu\\.tar\\.gz$" +asset_regex="^uv-${arch_segment}-unknown-linux-gnu\\.tar\\.gz$" # Example nanolayer installation via devcontainer-feature $nanolayer_location \ 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/templates/src/devcontainer-feature.json b/templates/bak/src/devcontainer-feature.json similarity index 100% rename from templates/src/devcontainer-feature.json rename to templates/bak/src/devcontainer-feature.json diff --git a/templates/src/install.sh b/templates/bak/src/install.sh similarity index 100% rename from templates/src/install.sh rename to templates/bak/src/install.sh diff --git a/templates/bak/src/library_scripts.sh b/templates/bak/src/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/templates/bak/src/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/templates/bak/test/scenarios.json b/templates/bak/test/scenarios.json new file mode 100644 index 000000000..48ca39c54 --- /dev/null +++ b/templates/bak/test/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "": { + "version": "x.y.z" + } + } + } +} \ No newline at end of file diff --git a/templates/bak/test/test.sh b/templates/bak/test/test.sh new file mode 100755 index 000000000..b2873a73c --- /dev/null +++ b/templates/bak/test/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something is installed" something --version + +reportResults diff --git a/templates/bak/test/test_debian.sh b/templates/bak/test/test_debian.sh new file mode 100755 index 000000000..b2873a73c --- /dev/null +++ b/templates/bak/test/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something is installed" something --version + +reportResults diff --git a/templates/bak/test/test_specific_version.sh b/templates/bak/test/test_specific_version.sh new file mode 100755 index 000000000..e4858fa21 --- /dev/null +++ b/templates/bak/test/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something version is equal to x.y.z" sh -c "something --version | grep 'x.y.z'" + +reportResults diff --git a/templates/gh-release/boilerplate.yml b/templates/gh-release/boilerplate.yml new file mode 100644 index 000000000..6f1ba7a9e --- /dev/null +++ b/templates/gh-release/boilerplate.yml @@ -0,0 +1,12 @@ +variables: + - name: ID + description: The unique identifier for the feature (e.g., akamai-cli). + + - name: Description + description: A brief description of the feature. + + - name: Repository + description: The GitHub repository in the format "owner/repo" (e.g., akamai/cli). + + - name: BinaryNames + description: The names of the binaries to install from the release (comma-separated if multiple). diff --git a/templates/gh-release/devcontainer-feature.json b/templates/gh-release/devcontainer-feature.json new file mode 100644 index 000000000..466db7e1e --- /dev/null +++ b/templates/gh-release/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "{{.ID}}", + "version": "1.0.0", + "name": "{{.ID}} (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/{{.ID}}", + "description": "{{.Description}}", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/templates/gh-release/install.sh b/templates/gh-release/install.sh new file mode 100755 index 000000000..ff1c69aeb --- /dev/null +++ b/templates/gh-release/install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='{{.Repository}}' --option binaryNames='{{.BinaryNames}}' --option version="$VERSION" + +echo 'Done!' diff --git a/templates/gh-release/library_scripts.sh b/templates/gh-release/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/templates/gh-release/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/templates/test/boilerplate.yml b/templates/test/boilerplate.yml new file mode 100644 index 000000000..fe68ad94c --- /dev/null +++ b/templates/test/boilerplate.yml @@ -0,0 +1,13 @@ +variables: + - name: ID + description: The unique identifier for the feature (e.g., akamai-cli). + + - name: BinaryNames + description: The names of the binaries to install from the release (comma-separated if multiple). + + - name: BinaryVersionCommands + description: The commands to check the version of the installed binaries (comma-separated if multiple). + default: "--version" + + - name: BinaryNonLatestVersions + description: Specific non-latest versions for test scenarios (comma-separated if multiple). For example, if 1.2.3 is the latest version, you should use 1.2.2 here. diff --git a/templates/test/scenarios.json b/templates/test/scenarios.json index 48ca39c54..283d4b0ab 100644 --- a/templates/test/scenarios.json +++ b/templates/test/scenarios.json @@ -2,15 +2,15 @@ "test_debian": { "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { - "": {} + "{{.ID}}": {} } }, "test_specific_version": { "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { - "": { - "version": "x.y.z" + "{{.ID}}": { + "version": "{{index (splitList "," .BinaryNonLatestVersions) 0}}" } } } -} \ No newline at end of file +} diff --git a/templates/test/test.sh b/templates/test/test.sh index b2873a73c..f5101fa3f 100755 --- a/templates/test/test.sh +++ b/templates/test/test.sh @@ -4,6 +4,17 @@ set -e source dev-container-features-test-lib -check "something is installed" something --version +{{- $binaryNames := splitList "," .BinaryNames}} +{{- $versionCommands := splitList "," .BinaryVersionCommands}} +{{- range $index, $binary := $binaryNames}} +{{- $versionCmd := "--version"}} +{{- if gt (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands $index}} +{{- else if eq (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands 0}} +{{- end}} + +check "{{$binary}} is installed" {{$binary}} {{$versionCmd}} +{{- end}} reportResults diff --git a/templates/test/test_debian.sh b/templates/test/test_debian.sh index b2873a73c..f7780a21d 100755 --- a/templates/test/test_debian.sh +++ b/templates/test/test_debian.sh @@ -3,7 +3,17 @@ set -e source dev-container-features-test-lib +{{- $binaryNames := splitList "," .BinaryNames}} +{{- $versionCommands := splitList "," .BinaryVersionCommands}} +{{- range $index, $binary := $binaryNames}} +{{- $versionCmd := "--version"}} +{{- if gt (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands $index}} +{{- else if eq (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands 0}} +{{- end}} -check "something is installed" something --version +check "{{$binary}} is installed" {{$binary}} {{$versionCmd}} +{{- end}} reportResults diff --git a/templates/test/test_specific_version.sh b/templates/test/test_specific_version.sh index e4858fa21..0108dac96 100755 --- a/templates/test/test_specific_version.sh +++ b/templates/test/test_specific_version.sh @@ -3,7 +3,19 @@ set -e source dev-container-features-test-lib +{{- $binaryNames := splitList "," .BinaryNames}} +{{- $versionCommands := splitList "," .BinaryVersionCommands}} +{{- $nonLatestVersions := splitList "," .BinaryNonLatestVersions}} +{{- range $index, $binary := $binaryNames}} +{{- $versionCmd := "--version"}} +{{- if gt (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands $index}} +{{- else if eq (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands 0}} +{{- end}} +{{- $nonLatestVersion := index $nonLatestVersions $index}} -check "something version is equal to x.y.z" sh -c "something --version | grep 'x.y.z'" +check "{{$binary}} version is equal to {{$nonLatestVersion}}" sh -c "{{$binary}} {{$versionCmd}} | grep '{{$nonLatestVersion}}'" +{{- end}} reportResults diff --git a/archive/test/age-keygen/scenarios.json b/test/age-keygen/scenarios.json similarity index 100% rename from archive/test/age-keygen/scenarios.json rename to test/age-keygen/scenarios.json diff --git a/archive/test/age-keygen/test_defaults_debian.sh b/test/age-keygen/test_defaults_debian.sh similarity index 100% rename from archive/test/age-keygen/test_defaults_debian.sh rename to test/age-keygen/test_defaults_debian.sh 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 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 diff --git a/test/boilerplate/scenarios.json b/test/boilerplate/scenarios.json new file mode 100644 index 000000000..facce5d10 --- /dev/null +++ b/test/boilerplate/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "boilerplate": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "boilerplate": { + "version": "0.10.0" + } + } + } +} diff --git a/test/boilerplate/test.sh b/test/boilerplate/test.sh new file mode 100755 index 000000000..c6f6a6404 --- /dev/null +++ b/test/boilerplate/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "boilerplate is installed" boilerplate --version + +reportResults diff --git a/test/boilerplate/test_debian.sh b/test/boilerplate/test_debian.sh new file mode 100755 index 000000000..c6f6a6404 --- /dev/null +++ b/test/boilerplate/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "boilerplate is installed" boilerplate --version + +reportResults diff --git a/test/boilerplate/test_specific_version.sh b/test/boilerplate/test_specific_version.sh new file mode 100755 index 000000000..fc712fba7 --- /dev/null +++ b/test/boilerplate/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "boilerplate version is equal to 0.10.0" sh -c "boilerplate --version | grep '0.10.0'" + +reportResults diff --git a/test/btm/scenarios.json b/test/btm/scenarios.json new file mode 100644 index 000000000..88b24d836 --- /dev/null +++ b/test/btm/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_defaults_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "btm": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "btm": { + "version": "0.11.0" + } + } + } +} \ No newline at end of file diff --git a/archive/test/btm/test_defaults_debian.sh b/test/btm/test_defaults_debian.sh similarity index 100% rename from archive/test/btm/test_defaults_debian.sh rename to test/btm/test_defaults_debian.sh diff --git a/test/btm/test_specific_version.sh b/test/btm/test_specific_version.sh new file mode 100644 index 000000000..2476ea3a8 --- /dev/null +++ b/test/btm/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "btm version is equal to 0.11.0" sh -c "btm --version | grep '0.11.0'" + +reportResults diff --git a/test/bun/test_specific_version.sh b/test/bun/test_specific_version.sh index 7bfb97480..f32b754a8 100755 --- a/test/bun/test_specific_version.sh +++ b/test/bun/test_specific_version.sh @@ -2,6 +2,8 @@ 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'" diff --git a/test/claude-code/scenarios.json b/test/claude-code/scenarios.json new file mode 100644 index 000000000..6f06352cc --- /dev/null +++ b/test/claude-code/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "claude-code": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "claude-code": { + "version": "2.0.30" + } + } + } +} \ No newline at end of file diff --git a/test/claude-code/test.sh b/test/claude-code/test.sh new file mode 100644 index 000000000..d8c00b08c --- /dev/null +++ b/test/claude-code/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "claude is installed" claude --version + +reportResults diff --git a/test/claude-code/test_debian.sh b/test/claude-code/test_debian.sh new file mode 100644 index 000000000..cdaa1733d --- /dev/null +++ b/test/claude-code/test_debian.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "claude is installed" claude --version +check "~/.claude is owned by the current user" test "$(stat -c %u "$HOME/.claude")" = "$(id -u)" +check "~/.local/share/claude is owned by the current user" test "$(stat -c %u "$HOME/.local/share/claude")" = "$(id -u)" + +reportResults diff --git a/test/claude-code/test_specific_version.sh b/test/claude-code/test_specific_version.sh new file mode 100644 index 000000000..5a0b564f2 --- /dev/null +++ b/test/claude-code/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "claude version is equal to 2.0.30" sh -c "claude --version | grep '2.0.30'" + +reportResults diff --git a/test/codebuddy-code/scenarios.json b/test/codebuddy-code/scenarios.json new file mode 100644 index 000000000..2e9e50421 --- /dev/null +++ b/test/codebuddy-code/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "codebuddy-code": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "codebuddy-code": { + "version": "2.16.0" + } + } + } +} \ No newline at end of file diff --git a/test/codebuddy-code/test.sh b/test/codebuddy-code/test.sh new file mode 100755 index 000000000..89f009cc6 --- /dev/null +++ b/test/codebuddy-code/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "CodeBuddy is installed" codebuddy --version + +reportResults diff --git a/test/codebuddy-code/test_debian.sh b/test/codebuddy-code/test_debian.sh new file mode 100755 index 000000000..89f009cc6 --- /dev/null +++ b/test/codebuddy-code/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "CodeBuddy is installed" codebuddy --version + +reportResults diff --git a/test/codebuddy-code/test_specific_version.sh b/test/codebuddy-code/test_specific_version.sh new file mode 100755 index 000000000..3f66754bf --- /dev/null +++ b/test/codebuddy-code/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "CodeBuddy version is equal to 2.16.0" sh -c "codebuddy --version | grep '2.16.0'" + +reportResults diff --git a/test/dart/scenarios.json b/test/dart/scenarios.json new file mode 100644 index 000000000..f561c70df --- /dev/null +++ b/test/dart/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "dart": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "dart": { + "version": "3.9.3" + } + } + } +} \ No newline at end of file diff --git a/test/dart/test.sh b/test/dart/test.sh new file mode 100755 index 000000000..1fe0590eb --- /dev/null +++ b/test/dart/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "dart is installed" dart --version + +reportResults diff --git a/test/dart/test_debian.sh b/test/dart/test_debian.sh new file mode 100755 index 000000000..8a04fbb07 --- /dev/null +++ b/test/dart/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "dart --version" dart --version + +reportResults diff --git a/test/dart/test_specific_version.sh b/test/dart/test_specific_version.sh new file mode 100755 index 000000000..765cf9503 --- /dev/null +++ b/test/dart/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "dart version is equal to 3.9.3" sh -c "dart --version | grep '3.9.3'" + +reportResults 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 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 diff --git a/test/infisical/scenarios.json b/test/infisical/scenarios.json new file mode 100644 index 000000000..3c0188728 --- /dev/null +++ b/test/infisical/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "infisical": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "infisical": { + "version": "0.43.45" + } + } + } +} diff --git a/test/infisical/test.sh b/test/infisical/test.sh new file mode 100755 index 000000000..e37186ff3 --- /dev/null +++ b/test/infisical/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "infisical is installed" infisical --version + +reportResults diff --git a/test/infisical/test_debian.sh b/test/infisical/test_debian.sh new file mode 100755 index 000000000..e37186ff3 --- /dev/null +++ b/test/infisical/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "infisical is installed" infisical --version + +reportResults diff --git a/test/infisical/test_specific_version.sh b/test/infisical/test_specific_version.sh new file mode 100755 index 000000000..48f0ce71f --- /dev/null +++ b/test/infisical/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "infisical version is equal to 0.43.45" sh -c "infisical --version | grep '0.43.45'" + +reportResults diff --git a/test/opencode/scenarios.json b/test/opencode/scenarios.json new file mode 100644 index 000000000..cfad1a85e --- /dev/null +++ b/test/opencode/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "opencode": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "opencode": { + "version": "1.0.176" + } + } + } +} diff --git a/test/opencode/test.sh b/test/opencode/test.sh new file mode 100755 index 000000000..c10e98aad --- /dev/null +++ b/test/opencode/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opencode is installed" opencode --version + +reportResults diff --git a/test/opencode/test_debian.sh b/test/opencode/test_debian.sh new file mode 100755 index 000000000..c10e98aad --- /dev/null +++ b/test/opencode/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opencode is installed" opencode --version + +reportResults diff --git a/test/opencode/test_specific_version.sh b/test/opencode/test_specific_version.sh new file mode 100755 index 000000000..c374514b2 --- /dev/null +++ b/test/opencode/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opencode version is equal to 1.0.176" sh -c "opencode --version | grep '1.0.176'" + +reportResults diff --git a/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 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 diff --git a/test/prek/scenarios.json b/test/prek/scenarios.json new file mode 100644 index 000000000..995efe310 --- /dev/null +++ b/test/prek/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_defaults_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "prek": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "prek": { + "version": "0.3.6" + } + } + } +} 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 diff --git a/test/prek/test_specific_version.sh b/test/prek/test_specific_version.sh new file mode 100755 index 000000000..6fefebb50 --- /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.6" sh -c "prek --version | grep '0.3.6'" + +reportResults diff --git a/test/terragrunt/scenarios.json b/test/terragrunt/scenarios.json index 1d1e6cecd..0abc6a801 100644 --- a/test/terragrunt/scenarios.json +++ b/test/terragrunt/scenarios.json @@ -4,5 +4,21 @@ "features": { "terragrunt": {} } + }, + "test_specific_version_before_assets_change": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "terragrunt": { + "version": "0.93.5" + } + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "terragrunt": { + "version": "0.96.0" + } + } } } \ No newline at end of file diff --git a/test/terragrunt/test_specific_version.sh b/test/terragrunt/test_specific_version.sh new file mode 100755 index 000000000..5580e8094 --- /dev/null +++ b/test/terragrunt/test_specific_version.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "terragrunt --version" terragrunt --version +check "terragrunt version is equal to 0.96.0" sh -c "terragrunt --version | grep '0.96.0'" + +reportResults diff --git a/test/terragrunt/test_specific_version_before_assets_change.sh b/test/terragrunt/test_specific_version_before_assets_change.sh new file mode 100755 index 000000000..8f7a10be4 --- /dev/null +++ b/test/terragrunt/test_specific_version_before_assets_change.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "terragrunt --version" terragrunt --version +check "terragrunt version is equal to 0.93.5" sh -c "terragrunt --version | grep '0.93.5'" + +reportResults diff --git a/test/tflint/scenarios.json b/test/tflint/scenarios.json new file mode 100644 index 000000000..7cb7266ef --- /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.0" + } + } + } +} \ 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..742971673 --- /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.0" sh -c "tflint --version | grep '0.62.0'" + +reportResults diff --git a/test/ty/scenarios.json b/test/ty/scenarios.json new file mode 100644 index 000000000..ef48409c0 --- /dev/null +++ b/test/ty/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "ty": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "ty": { + "version": "0.0.10" + } + } + } +} diff --git a/test/ty/test.sh b/test/ty/test.sh new file mode 100755 index 000000000..c433ffc9f --- /dev/null +++ b/test/ty/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ty is installed" ty --version + +reportResults diff --git a/test/ty/test_debian.sh b/test/ty/test_debian.sh new file mode 100755 index 000000000..c433ffc9f --- /dev/null +++ b/test/ty/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ty is installed" ty --version + +reportResults diff --git a/test/ty/test_specific_version.sh b/test/ty/test_specific_version.sh new file mode 100755 index 000000000..5d5af0ad9 --- /dev/null +++ b/test/ty/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ty version is equal to 0.0.10" sh -c "ty --version | grep '0.0.10'" + +reportResults diff --git a/test/uv-tool/install_tool_using_from.sh b/test/uv-tool/install_tool_using_from.sh new file mode 100644 index 000000000..1ccff31af --- /dev/null +++ b/test/uv-tool/install_tool_using_from.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "httpie is installed" http --version + +reportResults diff --git a/test/uv-tool/install_tool_using_multiple_with.sh b/test/uv-tool/install_tool_using_multiple_with.sh new file mode 100644 index 000000000..5bc9199a5 --- /dev/null +++ b/test/uv-tool/install_tool_using_multiple_with.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "mkdocs is installed" mkdocs --version +check "mkdocs-material theme is available" sh -c "cd $(uv tool dir)/mkdocs && uv pip list | grep mkdocs-material" +check "mkdocs-minify-plugin is available" sh -c "cd $(uv tool dir)/mkdocs && uv pip list | grep mkdocs-minify-plugin" + +reportResults diff --git a/test/uv-tool/install_tool_using_uv_version.sh b/test/uv-tool/install_tool_using_uv_version.sh new file mode 100644 index 000000000..a29cc784d --- /dev/null +++ b/test/uv-tool/install_tool_using_uv_version.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "uv tool version" sh -c "uv --version | grep 0.9.6" +check "httpie is installed" http --version + +reportResults diff --git a/test/uv-tool/install_tool_using_with.sh b/test/uv-tool/install_tool_using_with.sh new file mode 100644 index 000000000..2c8171f04 --- /dev/null +++ b/test/uv-tool/install_tool_using_with.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "mkdocs-material theme is available" sh -c "cd $(uv tool dir)/mkdocs && uv pip list | grep mkdocs-material" + +reportResults diff --git a/test/uv-tool/install_tool_using_with_executables_from_and_python.sh b/test/uv-tool/install_tool_using_with_executables_from_and_python.sh new file mode 100644 index 000000000..863dabb4f --- /dev/null +++ b/test/uv-tool/install_tool_using_with_executables_from_and_python.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ansible is installed" ansible --version +check "ansible-core executables are available" ansible-playbook --version +check "ansible-lint is available" ansible-lint --version + +check "ansible uses Python 3.13" sh -c "ansible --version | grep 'python version = 3.13'" + +reportResults diff --git a/test/uv-tool/scenarios.json b/test/uv-tool/scenarios.json new file mode 100644 index 000000000..aa7425c9a --- /dev/null +++ b/test/uv-tool/scenarios.json @@ -0,0 +1,48 @@ +{ + "install_tool_using_uv_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "uv-tool": { + "uv_version": "0.9.6", + "package": "httpie" + } + } + }, + "install_tool_using_from": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "uv-tool": { + "package": "httpie", + "from": "git+https://github.com/httpie/cli" + } + } + }, + "install_tool_using_with": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "uv-tool": { + "package": "mkdocs", + "with": "mkdocs-material" + } + } + }, + "install_tool_using_multiple_with": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "uv-tool": { + "package": "mkdocs", + "with": "mkdocs-material mkdocs-minify-plugin" + } + } + }, + "install_tool_using_with_executables_from_and_python": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "uv-tool": { + "package": "ansible", + "with-executables-from": "ansible-core,ansible-lint", + "python": "3.13" + } + } + } +} 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 diff --git a/vars.yml b/vars.yml new file mode 100644 index 000000000..4040ae38c --- /dev/null +++ b/vars.yml @@ -0,0 +1,6 @@ +ID: opencode +Description: The open source coding agent. +Repository: sst/opencode +BinaryNames: opencode +BinaryVersionCommands: --version +BinaryNonLatestVersions: 1.0.179