From 0ed4087d33a477fb80835d3780bcb40d97667dae Mon Sep 17 00:00:00 2001 From: rhiroe Date: Tue, 19 May 2026 10:08:58 +0000 Subject: [PATCH] feat(ruby)!: rewrite to use ruby-build with optional rbenv/rvm (v2.0.0) Replace the rvm-only install with ruby-build under /usr/local/rubies, exposed via the 'current' symlink on the PATH. Add a versionManager option ("none" | "rbenv" | "rvm") that installs the chosen manager and delegates 'install' to it. Detect build deps across apt, dnf/yum, apk, zypper, and pacman. Replace ruby_fallback_test with ruby_rbenv / ruby_rvm scenarios. --- src/ruby/NOTES.md | 9 +- src/ruby/README.md | 9 +- src/ruby/devcontainer-feature.json | 24 +- src/ruby/install.sh | 619 +++++++++----------- test/ruby/install_additional_ruby.sh | 10 +- test/ruby/install_additional_ruby_trixie.sh | 11 +- test/ruby/install_ruby_trixie_base.sh | 8 +- test/ruby/ruby_fallback_test.sh | 310 ---------- test/ruby/ruby_rbenv.sh | 19 + test/ruby/ruby_rvm.sh | 19 + test/ruby/scenarios.json | 22 +- 11 files changed, 381 insertions(+), 679 deletions(-) delete mode 100644 test/ruby/ruby_fallback_test.sh create mode 100755 test/ruby/ruby_rbenv.sh create mode 100755 test/ruby/ruby_rvm.sh diff --git a/src/ruby/NOTES.md b/src/ruby/NOTES.md index 19fe92f31..53da243c1 100644 --- a/src/ruby/NOTES.md +++ b/src/ruby/NOTES.md @@ -2,6 +2,13 @@ ## OS Support -This Feature should work on recent versions of Debian/Ubuntu-based distributions with the `apt` package manager installed. +This Feature supports Linux images that ship one of the following package managers: `apt`, `dnf`/`yum`, `apk`, `zypper`, or `pacman`. The script detects the available package manager and installs the build dependencies that ruby-build needs. `bash` is required to execute the `install.sh` script. + +## Layout + +- Ruby is installed under `/usr/local/rubies/` by ruby-build. +- The default Ruby is exposed via the `/usr/local/rubies/current` symlink, which is placed on the `PATH` through `containerEnv`. +- `ruby-build` itself is cloned to `/usr/local/share/ruby-build` and symlinked into `/usr/local/bin/ruby-build` so additional versions can be installed later. +- A shared `ruby` group owns `/usr/local/rubies`; the configured non-root user is added to it so `gem install` can write into the active Ruby tree without `sudo`. diff --git a/src/ruby/README.md b/src/ruby/README.md index 7df6965c7..351ffe906 100644 --- a/src/ruby/README.md +++ b/src/ruby/README.md @@ -1,13 +1,13 @@ -# Ruby (via rvm) (ruby) +# Ruby (via ruby-build) (ruby) -Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies. +Installs Ruby using ruby-build, with optional rbenv or rvm for version management. ## Example Usage ```json "features": { - "ghcr.io/devcontainers/features/ruby:1": {} + "ghcr.io/devcontainers/features/ruby:2": {} } ``` @@ -16,6 +16,7 @@ Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies. | Options Id | Description | Type | Default Value | |-----|-----|-----|-----| | version | Select or enter a Ruby version to install | string | latest | +| versionManager | Version manager to install alongside Ruby: 'rbenv', 'rvm', or 'none' (ruby-build only) | string | none | ## Customizations @@ -27,7 +28,7 @@ Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies. ## OS Support -This Feature should work on recent versions of Debian/Ubuntu-based distributions with the `apt` package manager installed. +This Feature supports Linux images that ship one of the following package managers: `apt`, `dnf`/`yum`, `apk`, `zypper`, or `pacman`. The script detects the available package manager and installs the build dependencies that ruby-build needs. `bash` is required to execute the `install.sh` script. diff --git a/src/ruby/devcontainer-feature.json b/src/ruby/devcontainer-feature.json index 661c46bf0..841afbc3d 100644 --- a/src/ruby/devcontainer-feature.json +++ b/src/ruby/devcontainer-feature.json @@ -1,20 +1,26 @@ { "id": "ruby", - "version": "1.3.2", - "name": "Ruby (via rvm)", + "version": "2.0.0", + "name": "Ruby (via ruby-build)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/ruby", - "description": "Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies.", + "description": "Installs Ruby using ruby-build, with optional rbenv or rvm for version management.", "options": { "version": { "type": "string", "proposals": [ "latest", "none", - "3.4", - "3.2" + "4.0", + "3.4" ], "default": "latest", "description": "Select or enter a Ruby version to install" + }, + "versionManager": { + "type": "string", + "enum": ["none", "rbenv", "rvm"], + "default": "none", + "description": "Version manager to install alongside Ruby: 'rbenv', 'rvm', or 'none' (ruby-build only)" } }, "customizations": { @@ -25,17 +31,15 @@ "settings": { "github.copilot.chat.codeGeneration.instructions": [ { - "text": "This dev container includes Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies pre-installed and available on the `PATH`, along with the Ruby language extension for Ruby development." + "text": "This dev container installs Ruby via ruby-build. rbenv or rvm may also be available depending on the versionManager option. The default Ruby is on the PATH via /usr/local/rubies/current/bin (or rbenv shims if rbenv is the version manager)." } ] } } }, "containerEnv": { - "GEM_PATH": "/usr/local/rvm/gems/default:/usr/local/rvm/gems/default@global", - "GEM_HOME": "/usr/local/rvm/gems/default", - "MY_RUBY_HOME": "/usr/local/rvm/rubies/default", - "PATH": "/usr/local/rvm/gems/default/bin:/usr/local/rvm/gems/default@global/bin:/usr/local/rvm/rubies/default/bin:/usr/local/share/rbenv/bin:${PATH}" + "RBENV_ROOT": "/usr/local/share/rbenv", + "PATH": "/usr/local/share/rbenv/shims:/usr/local/share/rbenv/bin:/usr/local/rubies/current/bin:${PATH}" }, "installsAfter": [ "ghcr.io/devcontainers/features/common-utils" diff --git a/src/ruby/install.sh b/src/ruby/install.sh index 39cb5be03..6c53d66d1 100755 --- a/src/ruby/install.sh +++ b/src/ruby/install.sh @@ -10,24 +10,26 @@ RUBY_VERSION="${VERSION:-"latest"}" USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}" -UPDATE_RC="${UPDATE_RC:-"true"}" INSTALL_RUBY_TOOLS="${INSTALL_RUBY_TOOLS:-"true"}" -# Comma-separated list of ruby versions to be installed (with rvm) -# alongside RUBY_VERSION, but not set as default. +# Comma-separated list of ruby versions to be installed alongside RUBY_VERSION, +# but not set as default. ADDITIONAL_VERSIONS="${ADDITIONALVERSIONS:-""}" -# Note: ruby-debug-ide will install the right version of debase if missing and -# installing debase directly fails on Ruby 3.1.0 as of 1/7/2022, so omitting. -# installing ruby-debug-ide on debian fails, so omitting. +VERSION_MANAGER="${VERSIONMANAGER:-"none"}" + DEFAULT_GEMS="rake" -RVM_GPG_KEYS="409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB" +RUBY_BUILD_DIR="/usr/local/share/ruby-build" +RUBIES_DIR="/usr/local/rubies" +RUBY_GROUP="ruby" +RBENV_ROOT="/usr/local/share/rbenv" +RVM_PATH="/usr/local/rvm" set -e -# Clean up -rm -rf /var/lib/apt/lists/* +# Force apt to refresh its lists below by clearing them up front (no-op on non-apt systems). +rm -rf /var/lib/apt/lists/* 2>/dev/null || true if [ "$(id -u)" -ne 0 ]; then echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' @@ -39,7 +41,6 @@ rm -f /etc/profile.d/00-restore-env.sh echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh chmod +x /etc/profile.d/00-restore-env.sh -# 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)") @@ -56,389 +57,341 @@ elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then USERNAME=root fi -updaterc() { - if [ "${UPDATE_RC}" = "true" ]; then - echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." - if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then - echo -e "$1" >> /etc/bash.bashrc - fi - if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then - echo -e "$1" >> /etc/zsh/zshrc - fi +architecture="$(uname -m)" +if [ "${architecture}" != "amd64" ] && [ "${architecture}" != "x86_64" ] && [ "${architecture}" != "arm64" ] && [ "${architecture}" != "aarch64" ]; then + echo "(!) Architecture $architecture unsupported" + exit 1 +fi + +clone_or_update_repo() { + local repo=$1 dest=$2 + if [ ! -d "${dest}" ]; then + git clone --depth=1 \ + -c core.eol=lf \ + -c core.autocrlf=false \ + -c fsck.zeroPaddedFilemode=ignore \ + -c fetch.fsck.zeroPaddedFilemode=ignore \ + -c receive.fsck.zeroPaddedFilemode=ignore \ + "${repo}" "${dest}" + else + git -C "${dest}" fetch --depth=1 origin && \ + git -C "${dest}" reset --hard origin/HEAD || true fi } -# Get the list of GPG key servers that are reachable -get_gpg_key_servers() { - declare -A keyservers_curl_map=( - ["hkp://keyserver.ubuntu.com"]="http://keyserver.ubuntu.com:11371" - ["hkp://keyserver.ubuntu.com:80"]="http://keyserver.ubuntu.com" - ["hkps://keys.openpgp.org"]="https://keys.openpgp.org" - ["hkp://keyserver.pgp.com"]="http://keyserver.pgp.com:11371" - ) - - local curl_args="" - local keyserver_reachable=false # Flag to indicate if any keyserver is reachable - - if [ ! -z "${KEYSERVER_PROXY}" ]; then - curl_args="--proxy ${KEYSERVER_PROXY}" - fi +apply_group_perms() { + local dir=$1 + chgrp -R "${RUBY_GROUP}" "${dir}" 2>/dev/null || true + chmod -R g+rw "${dir}" 2>/dev/null || true + find "${dir}" -type d -exec chmod g+s {} + 2>/dev/null || true +} - for keyserver in "${!keyservers_curl_map[@]}"; do - local keyserver_curl_url="${keyservers_curl_map[${keyserver}]}" - if curl -s ${curl_args} --max-time 5 ${keyserver_curl_url} > /dev/null; then - echo "keyserver ${keyserver}" - keyserver_reachable=true - else - echo "(*) Keyserver ${keyserver} is not reachable." >&2 - fi - done +default_ruby_version() { + [ -L "${RUBIES_DIR}/current" ] && basename "$(readlink "${RUBIES_DIR}/current")" +} - if ! $keyserver_reachable; then - echo "(!) No keyserver is reachable." >&2 +install_build_deps() { + if command -v apt-get > /dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + apt-get update -y + # libgdbm-dev pulls the appropriate libgdbm runtime, so no version-specific package is needed. + apt-get -y install --no-install-recommends \ + curl ca-certificates git autoconf bison patch build-essential \ + libssl-dev libyaml-dev libreadline-dev zlib1g-dev libgmp-dev \ + libncurses-dev libffi-dev libgdbm-dev libdb-dev uuid-dev + elif command -v dnf > /dev/null 2>&1 || command -v yum > /dev/null 2>&1; then + local pm + pm="$(command -v dnf || command -v yum)" + "${pm}" install -y \ + curl ca-certificates git gcc make patch autoconf bison \ + openssl-devel libyaml-devel zlib-devel libffi-devel \ + readline-devel ncurses-devel gdbm-devel + elif command -v apk > /dev/null 2>&1; then + apk add --no-cache \ + bash curl ca-certificates git build-base linux-headers \ + autoconf bison patch openssl-dev yaml-dev zlib-dev \ + readline-dev ncurses-dev libffi-dev gdbm-dev + elif command -v zypper > /dev/null 2>&1; then + zypper --non-interactive install --no-recommends \ + curl ca-certificates git gcc-c++ make patch \ + autoconf automake libtool bison \ + libopenssl-devel libyaml-devel zlib-devel libffi-devel \ + readline-devel ncurses-devel gdbm-devel + elif command -v pacman > /dev/null 2>&1; then + pacman -Sy --noconfirm --needed \ + curl ca-certificates git base-devel autoconf bison \ + openssl libyaml zlib libffi readline ncurses gdbm + else + echo "(!) No supported package manager found. Install Ruby build dependencies manually." exit 1 fi } -# Import the specified key in a variable name passed in as -receive_gpg_keys() { - local keys=${!1} - local keyring_args="" - if [ ! -z "$2" ]; then - keyring_args="--no-default-keyring --keyring \"$2\"" - fi +install_ruby_build() { + clone_or_update_repo "https://github.com/rbenv/ruby-build.git" "${RUBY_BUILD_DIR}" + ln -sf "${RUBY_BUILD_DIR}/bin/ruby-build" /usr/local/bin/ruby-build +} - # Install curl - if ! type curl > /dev/null 2>&1; then - check_packages curl - fi +resolve_ruby_version() { + local requested=$1 + local definitions_dir="${RUBY_BUILD_DIR}/share/ruby-build" + local stable_versions + stable_versions="$(ls "${definitions_dir}" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V)" - # Use a temporary location for gpg keys to avoid polluting image - export GNUPGHOME="/tmp/tmp-gnupg" - mkdir -p ${GNUPGHOME} - chmod 700 ${GNUPGHOME} - echo -e "disable-ipv6\n$(get_gpg_key_servers)" > ${GNUPGHOME}/dirmngr.conf - # GPG key download sometimes fails for some reason and retrying fixes it. - local retry_count=0 - local gpg_ok="false" - set +e - until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; - do - echo "(*) Downloading GPG key..." - ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" - if [ "${gpg_ok}" != "true" ]; then - echo "(*) Failed getting key, retrying in 10s..." - (( retry_count++ )) - sleep 10s - fi - done - set -e - if [ "${gpg_ok}" = "false" ]; then - echo "(!) Failed to get gpg key." + if [ -z "${stable_versions}" ]; then + echo "(!) ruby-build has no version definitions at ${definitions_dir}." >&2 exit 1 fi + + case "${requested}" in + latest|current|lts) + echo "${stable_versions}" | tail -n 1 + ;; + *) + if echo "${stable_versions}" | grep -qx "${requested}"; then + echo "${requested}" + return + fi + # Resolve a partial X.Y to the highest matching X.Y.Z. + local match + match="$(echo "${stable_versions}" | grep -E "^${requested//./\\.}\\.[0-9]+$" | sort -V | tail -n 1)" + if [ -n "${match}" ]; then + echo "${match}" + return + fi + echo "(!) Ruby version '${requested}' is not known to ruby-build." >&2 + exit 1 + ;; + esac } -# Figure out correct version of a three part version number is not passed -find_version_from_git_tags() { - local variable_name=$1 - local requested_version=${!variable_name} - if [ "${requested_version}" = "none" ]; then return; fi - local repository=$2 - local prefix=${3:-"tags/v"} - local separator=${4:-"."} - local last_part_optional=${5:-"false"} - if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then - local escaped_separator=${separator//./\\.} - local last_part - if [ "${last_part_optional}" = "true" ]; then - last_part="(${escaped_separator}[0-9]+)?" - else - last_part="${escaped_separator}[0-9]+" - fi - local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" - local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" - if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then - declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" - else - set +e - declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" - set -e +install_ruby_version() { + local requested=$1 + local set_default=$2 + local resolved + resolved="$(resolve_ruby_version "${requested}")" + local prefix="${RUBIES_DIR}/${resolved}" + + if [ -x "${prefix}/bin/ruby" ]; then + echo "(!) Ruby ${resolved} already installed at ${prefix}. Skipping..." + elif [ "${VERSION_MANAGER}" = "rbenv" ] && [ -x "${RBENV_ROOT}/bin/rbenv" ]; then + echo "Installing Ruby ${resolved} via rbenv..." + mkdir -p "${RUBIES_DIR}" + LANG="${LANG:-C.UTF-8}" RBENV_ROOT="${RBENV_ROOT}" \ + "${RBENV_ROOT}/bin/rbenv" install --skip-existing "${resolved}" + # Mirror into RUBIES_DIR so the rest of the script uses a consistent path. + ln -sfn "${RBENV_ROOT}/versions/${resolved}" "${prefix}" + elif [ "${VERSION_MANAGER}" = "rvm" ] && [ -s "${RVM_PATH}/scripts/rvm" ]; then + echo "Installing Ruby ${resolved} via rvm..." + mkdir -p "${RUBIES_DIR}" + # shellcheck disable=SC1091 + source "${RVM_PATH}/scripts/rvm" + LANG="${LANG:-C.UTF-8}" rvm install "${resolved}" + local rvm_ruby="${RVM_PATH}/rubies/ruby-${resolved}" + if [ -d "${rvm_ruby}" ]; then + ln -sfn "${rvm_ruby}" "${prefix}" fi + else + mkdir -p "${RUBIES_DIR}" + echo "Installing Ruby ${resolved} via ruby-build..." + # Ensure a UTF-8 locale so that rdoc (and other tools bundled with Ruby) + # can process non-ASCII bytes during `make install`, even on minimal + # base images that ship with no LANG set (e.g. Debian 11 bullseye). + LANG="${LANG:-C.UTF-8}" ruby-build "${resolved}" "${prefix}" fi - if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then - echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 - exit 1 + + if [ "${set_default}" = "true" ]; then + ln -sfn "${prefix}" "${RUBIES_DIR}/current" fi - echo "${variable_name}=${!variable_name}" } -# Use semver logic to decrement a version number then look for the closest match -find_prev_version_from_git_tags() { - local variable_name=$1 - local current_version=${!variable_name} - local repository=$2 - # Normally a "v" is used before the version number, but support alternate cases - local prefix=${3:-"tags/v"} - # Some repositories use "_" instead of "." for version number part separation, support that - local separator=${4:-"."} - # Some tools release versions that omit the last digit (e.g. go) - local last_part_optional=${5:-"false"} - # Some repositories may have tags that include a suffix (e.g. actions/node-versions) - local version_suffix_regex=$6 - # Try one break fix version number less if we get a failure. Use "set +e" since "set -e" can cause failures in valid scenarios. - set +e - major="$(echo "${current_version}" | grep -oE '^[0-9]+' || echo '')" - minor="$(echo "${current_version}" | grep -oP '^[0-9]+\.\K[0-9]+' || echo '')" - breakfix="$(echo "${current_version}" | grep -oP '^[0-9]+\.[0-9]+\.\K[0-9]+' 2>/dev/null || echo '')" - - if [ "${minor}" = "0" ] && [ "${breakfix}" = "0" ]; then - ((major=major-1)) - declare -g ${variable_name}="${major}" - # Look for latest version from previous major release - find_version_from_git_tags "${variable_name}" "${repository}" "${prefix}" "${separator}" "${last_part_optional}" - # Handle situations like Go's odd version pattern where "0" releases omit the last part - elif [ "${breakfix}" = "" ] || [ "${breakfix}" = "0" ]; then - ((minor=minor-1)) - declare -g ${variable_name}="${major}.${minor}" - # Look for latest version from previous minor release - find_version_from_git_tags "${variable_name}" "${repository}" "${prefix}" "${separator}" "${last_part_optional}" - else - ((breakfix=breakfix-1)) - if [ "${breakfix}" = "0" ] && [ "${last_part_optional}" = "true" ]; then - declare -g ${variable_name}="${major}.${minor}" - else - declare -g ${variable_name}="${major}.${minor}.${breakfix}" - fi - fi - set -e -} +# Called before ruby versions are installed so that install_ruby_version() +# can delegate to 'rbenv install'. +install_rbenv() { + echo "Installing rbenv..." + clone_or_update_repo "https://github.com/rbenv/rbenv.git" "${RBENV_ROOT}" -apt_get_update() -{ - if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then - echo "Running apt-get update..." - apt-get update -y - fi -} + ln -sf "${RBENV_ROOT}/bin/rbenv" /usr/local/bin/rbenv -# Checks if packages are installed and installs them if not -check_packages() { - if ! dpkg -s "$@" > /dev/null 2>&1; then - apt_get_update - apt-get -y install --no-install-recommends "$@" + # Wire the already-installed ruby-build as an rbenv plugin so that + # 'rbenv install' works out of the box. + mkdir -p "${RBENV_ROOT}/plugins" + if [ ! -e "${RBENV_ROOT}/plugins/ruby-build" ]; then + ln -sfn "${RUBY_BUILD_DIR}" "${RBENV_ROOT}/plugins/ruby-build" fi + mkdir -p "${RBENV_ROOT}/versions" + echo "rbenv ready at ${RBENV_ROOT}." } -# Ensure apt is in non-interactive to avoid prompts -export DEBIAN_FRONTEND=noninteractive +# Called after ruby versions are installed. +finalize_rbenv() { + # When rubies were installed via ruby-build (not 'rbenv install'), symlink them + # into rbenv's versions directory so 'rbenv versions' shows them. + # Skip entries that already point into RBENV_ROOT to avoid circular symlinks. + for ruby_dir in "${RUBIES_DIR}"/[0-9]*/; do + [ -d "${ruby_dir}" ] || continue + local ver + ver="$(basename "${ruby_dir%/}")" + local real_target + real_target="$(readlink -f "${ruby_dir%/}" 2>/dev/null || true)" + if [ "${real_target}" = "${RBENV_ROOT}/versions/${ver}" ]; then + continue + fi + ln -sfn "${ruby_dir%/}" "${RBENV_ROOT}/versions/${ver}" + done -architecture="$(uname -m)" -if [ "${architecture}" != "amd64" ] && [ "${architecture}" != "x86_64" ] && [ "${architecture}" != "arm64" ] && [ "${architecture}" != "aarch64" ]; then - echo "(!) Architecture $architecture unsupported" - exit 1 -fi + # Set the rbenv global version to match the ruby-build default. + local default_ver + default_ver="$(default_ruby_version)" + if [ -n "${default_ver}" ]; then + echo "${default_ver}" > "${RBENV_ROOT}/version" + fi -# Install dependencies -# Removed software-properties-common package from here as it has been removed for debian trixie(13) -check_packages curl ca-certificates build-essential gnupg2 libreadline-dev \ - procps dirmngr gawk autoconf automake bison libffi-dev libgdbm-dev libncurses5-dev \ - libsqlite3-dev libtool libyaml-dev pkg-config sqlite3 zlib1g-dev libgmp-dev libssl-dev -if ! type git > /dev/null 2>&1; then - check_packages git -fi + apply_group_perms "${RBENV_ROOT}" -# Conditionally install software-properties-common (skip on Debian Trixie) -if type apt-get >/dev/null 2>&1; then - if [ -f /etc/os-release ]; then - . /etc/os-release - if [ "${ID}" = "debian" ] && [ "${VERSION_CODENAME}" = "trixie" ]; then - echo "Skipping software-properties-common on Debian Trixie." - else - check_packages software-properties-common - fi - else - # Fallback for apt-based systems without /etc/os-release - check_packages software-properties-common - fi -fi + # Profile script for login shells (non-login shells rely on containerEnv + # which already prepends RBENV_ROOT/shims and RBENV_ROOT/bin). + cat > /etc/profile.d/rbenv.sh << 'RBENV_PROFILE' +export RBENV_ROOT=/usr/local/share/rbenv +export PATH="${RBENV_ROOT}/bin:${RBENV_ROOT}/shims:${PATH}" +eval "$(rbenv init - --no-rehash)" 2>/dev/null || true +RBENV_PROFILE + chmod +x /etc/profile.d/rbenv.sh -# Function to fetch the version released prior to the latest version -get_previous_version() { - local url=$1 - local repo_url=$2 - variable_name=$3 - prev_version=${!variable_name} - - output=$(curl -s "$repo_url"); - - #install jq - check_packages jq - - message=$(echo "$output" | jq -r '.message') - - if [[ $message == "API rate limit exceeded"* ]]; then - echo -e "\nAn attempt to find latest version using GitHub Api Failed... \nReason: ${message}" - echo -e "\nAttempting to find latest version using GitHub tags." - find_prev_version_from_git_tags prev_version "$url" "tags/v" "_" - declare -g ${variable_name}="${prev_version}" - else - echo -e "\nAttempting to find latest version using GitHub Api." - version=$(echo "$output" | jq -r '.tag_name' | tr '_' '.') - declare -g ${variable_name}="${version#v}" - fi - echo "${variable_name}=${!variable_name}" + RBENV_ROOT="${RBENV_ROOT}" "${RBENV_ROOT}/bin/rbenv" rehash 2>/dev/null || true + echo "rbenv configured." } -get_github_api_repo_url() { - local url=$1 - echo "${url/https:\/\/github.com/https:\/\/api.github.com\/repos}/releases/latest" +# Called before ruby versions are installed so that install_ruby_version() +# can delegate to 'rvm install'. +install_rvm() { + echo "Installing rvm..." + + # Best-effort GPG key import; some networks block keyservers. + gpg --keyserver hkp://keyserver.ubuntu.com \ + --recv-keys \ + 409B6B1796C275462A1703113804BB82D39DC0E3 \ + 7D2BAF1CF37B13E2069D6956105BD0E739499BDB 2>/dev/null \ + || curl -sSL https://rvm.io/mpapis.asc | gpg --import - 2>/dev/null \ + || curl -sSL https://rvm.io/pkuczynski.asc | gpg --import - 2>/dev/null \ + || true + + curl -sSL https://get.rvm.io | bash -s stable --path "${RVM_PATH}" + + # rvm is a shell function, so we must source it before calling 'rvm' below. + # shellcheck disable=SC1091 + if [ -s "${RVM_PATH}/scripts/rvm" ]; then + source "${RVM_PATH}/scripts/rvm" + fi + echo "rvm ready at ${RVM_PATH}." } +finalize_rvm() { + # shellcheck disable=SC1091 + [ -s "${RVM_PATH}/scripts/rvm" ] && source "${RVM_PATH}/scripts/rvm" || true + + # When rubies were installed via ruby-build (not 'rvm install'), mount them + # into rvm so 'rvm list' shows them. Skip entries that already live under + # RVM_PATH to avoid double-mounting. + if [ -d "${RUBIES_DIR}" ]; then + for ruby_dir in "${RUBIES_DIR}"/[0-9]*/; do + [ -d "${ruby_dir}" ] || continue + local ver + ver="$(basename "${ruby_dir%/}")" + local real_target + real_target="$(readlink -f "${ruby_dir%/}" 2>/dev/null || true)" + if [[ "${real_target}" == "${RVM_PATH}/rubies/"* ]]; then + continue + fi + rvm mount "${ruby_dir%/}" -n "${ver}" 2>/dev/null || true + done + fi -# Figure out correct version of a three part version number is not passed -RUBY_URL="https://github.com/ruby/ruby" -ORIGINAL_RUBY_VERSION=$RUBY_VERSION -find_version_from_git_tags RUBY_VERSION $RUBY_URL "tags/v" "_" - -set_rvm_install_args() { - RUBY_VERSION=$1 - if [ "${RUBY_VERSION}" = "none" ]; then - RVM_INSTALL_ARGS="" - elif [[ "$(ruby -v)" = *"${RUBY_VERSION}"* ]]; then - echo "(!) Ruby is already installed with version ${RUBY_VERSION}. Skipping..." - RVM_INSTALL_ARGS="" - else - if [ "${RUBY_VERSION}" = "latest" ] || [ "${RUBY_VERSION}" = "current" ] || [ "${RUBY_VERSION}" = "lts" ]; then - RVM_INSTALL_ARGS="--ruby" - RUBY_VERSION="" + # Set the rvm default to match the ruby-build default. + local default_ver + default_ver="$(default_ruby_version)" + if [ -n "${default_ver}" ]; then + local real_current + real_current="$(readlink -f "${RUBIES_DIR}/current" 2>/dev/null || true)" + if [[ "${real_current}" == "${RVM_PATH}/rubies/"* ]]; then + # Installed via 'rvm install': use the version name directly. + rvm use "${default_ver}" --default 2>/dev/null || true else - RVM_INSTALL_ARGS="--ruby=${RUBY_VERSION}" - fi - if [ "${INSTALL_RUBY_TOOLS}" = "true" ]; then - SKIP_GEM_INSTALL="true" - else - DEFAULT_GEMS="" + # Installed via ruby-build and mounted: use the 'ext-' prefix. + rvm use "ext-${default_ver}" --default 2>/dev/null || true fi fi -} -install_previous_version() { - if [[ $ORIGINAL_RUBY_VERSION == "latest" ]]; then - repo_url=$(get_github_api_repo_url "$RUBY_URL") - get_previous_version "${RUBY_URL}" "${repo_url}" RUBY_VERSION - set_rvm_install_args $RUBY_VERSION - curl -sSL https://get.rvm.io | bash -s stable --ignore-dotfiles ${RVM_INSTALL_ARGS} --with-default-gems="${DEFAULT_GEMS}" 2>&1 - else - echo "Failed to install Ruby version $ORIGINAL_RUBY_VERSION. Exiting..." + echo "source ${RVM_PATH}/scripts/rvm" > /etc/profile.d/rvm.sh + chmod +x /etc/profile.d/rvm.sh + + if [ "${USERNAME}" != "root" ] && id -u "${USERNAME}" > /dev/null 2>&1; then + usermod -aG rvm "${USERNAME}" 2>/dev/null || true fi + echo "rvm configured." } -# Just install Ruby if RVM already installed -if rvm --version > /dev/null; then - echo "Ruby Version Manager already exists." - if [[ "$(ruby -v)" = *"${RUBY_VERSION}"* ]]; then - echo "(!) Ruby is already installed with version ${RUBY_VERSION}. Skipping..." - elif [ "${RUBY_VERSION}" != "none" ]; then - echo "Installing specified Ruby version." - su ${USERNAME} -c "rvm install ruby ${RUBY_VERSION}" - fi - SKIP_GEM_INSTALL="false" - SKIP_RBENV_RBUILD="true" -else - # Install RVM - receive_gpg_keys RVM_GPG_KEYS - # Determine appropriate settings for rvm installer - set_rvm_install_args $RUBY_VERSION - # Create rvm group as a system group to reduce the odds of conflict with local user UIDs - if ! cat /etc/group | grep -e "^rvm:" > /dev/null 2>&1; then - groupadd -r rvm - fi - # Install rvm - curl -sSL https://get.rvm.io | bash -s stable --ignore-dotfiles ${RVM_INSTALL_ARGS} --with-default-gems="${DEFAULT_GEMS}" 2>&1 || install_previous_version - usermod -aG rvm ${USERNAME} - source /usr/local/rvm/scripts/rvm - rvm fix-permissions system - rm -rf ${GNUPGHOME} -fi +install_build_deps +install_ruby_build -if [ "${INSTALL_RUBY_TOOLS}" = "true" ]; then - # Non-root user may not have "gem" in path when script is run and no ruby version - # is installed by rvm, so handle this by using root's default gem in this case - ROOT_GEM="$(which gem || echo "")" - ${ROOT_GEM} install ${DEFAULT_GEMS} +# Create a shared "ruby" group so the configured user can write under the rubies tree. +if ! getent group "${RUBY_GROUP}" > /dev/null 2>&1; then + groupadd -r "${RUBY_GROUP}" 2>/dev/null || addgroup -S "${RUBY_GROUP}" 2>/dev/null || true +fi +mkdir -p "${RUBIES_DIR}" +chgrp "${RUBY_GROUP}" "${RUBIES_DIR}" 2>/dev/null || true +chmod 2775 "${RUBIES_DIR}" 2>/dev/null || true + +# Set up the version manager BEFORE installing Ruby versions so that +# install_ruby_version() can delegate to it when requested. +if [ "${VERSION_MANAGER}" = "rbenv" ]; then + install_rbenv +elif [ "${VERSION_MANAGER}" = "rvm" ]; then + install_rvm fi -# VS Code server usually first in the path, so silence annoying rvm warning (that does not apply) and then source it -updaterc "if ! grep rvm_silence_path_mismatch_check_flag \$HOME/.rvmrc > /dev/null 2>&1; then echo 'rvm_silence_path_mismatch_check_flag=1' >> \$HOME/.rvmrc; fi\nsource /usr/local/rvm/scripts/rvm > /dev/null 2>&1" +if [ "${RUBY_VERSION}" != "none" ]; then + install_ruby_version "${RUBY_VERSION}" "true" +fi -# Additional ruby versions to be installed but not be set as default. if [ ! -z "${ADDITIONAL_VERSIONS}" ]; then OLDIFS=$IFS IFS="," read -a additional_versions <<< "$ADDITIONAL_VERSIONS" for version in "${additional_versions[@]}"; do - # Figure out correct version of a three part version number is not passed - find_version_from_git_tags version $RUBY_URL "tags/v" "_" - source /usr/local/rvm/scripts/rvm - rvm install ruby ${version} + install_ruby_version "${version}" "false" done IFS=$OLDIFS fi -# Install rbenv/ruby-build for good measure -if [ "${SKIP_RBENV_RBUILD}" != "true" ]; then +# Expose the default Ruby on the PATH for all login shells. +echo 'export PATH="/usr/local/rubies/current/bin:${PATH}"' > /etc/profile.d/ruby.sh +chmod +x /etc/profile.d/ruby.sh - if [[ ! -d "/usr/local/share/rbenv" ]]; then - git clone --depth=1 \ - -c core.eol=lf \ - -c core.autocrlf=false \ - -c fsck.zeroPaddedFilemode=ignore \ - -c fetch.fsck.zeroPaddedFilemode=ignore \ - -c receive.fsck.zeroPaddedFilemode=ignore \ - https://github.com/rbenv/rbenv.git /usr/local/share/rbenv - fi - - if [[ ! -d "/usr/local/share/ruby-build" ]]; then - git clone --depth=1 \ - -c core.eol=lf \ - -c core.autocrlf=false \ - -c fsck.zeroPaddedFilemode=ignore \ - -c fetch.fsck.zeroPaddedFilemode=ignore \ - -c receive.fsck.zeroPaddedFilemode=ignore \ - https://github.com/rbenv/ruby-build.git /usr/local/share/ruby-build - mkdir -p /root/.rbenv/plugins - - ln -s /usr/local/share/ruby-build /root/.rbenv/plugins/ruby-build - fi - - if [ "${USERNAME}" != "root" ]; then - mkdir -p /home/${USERNAME}/.rbenv/plugins - - if [[ ! -d "/home/${USERNAME}/.rbenv/plugins/ruby-build" ]]; then - ln -s /usr/local/share/ruby-build /home/${USERNAME}/.rbenv/plugins/ruby-build - fi - - # Oryx expects ruby to be installed in this specific path, else it breaks the oryx magic for ruby projects. - if [ ! -f /usr/local/rvm/gems/default/bin/ruby ]; then - ln -s /usr/local/rvm/rubies/default/bin/ruby /usr/local/rvm/gems/default/bin - fi +if [ "${RUBY_VERSION}" != "none" ] && [ "${INSTALL_RUBY_TOOLS}" = "true" ]; then + "${RUBIES_DIR}/current/bin/gem" install --no-document ${DEFAULT_GEMS} +fi - chown -R "${USERNAME}:rvm" "/home/${USERNAME}/.rbenv/" - chmod -R g+r+w "/home/${USERNAME}/.rbenv" - find "/home/${USERNAME}/.rbenv" -type d | xargs -n 1 chmod g+s +# Make sure the configured user can install gems against the shared rubies tree. +if [ "${USERNAME}" != "root" ] && id -u "${USERNAME}" > /dev/null 2>&1; then + if command -v usermod > /dev/null 2>&1; then + usermod -aG "${RUBY_GROUP}" "${USERNAME}" || true + elif command -v addgroup > /dev/null 2>&1; then + addgroup "${USERNAME}" "${RUBY_GROUP}" || true fi fi -chown -R "${USERNAME}:rvm" "/usr/local/rvm/" -chmod -R g+r+w "/usr/local/rvm/" -find "/usr/local/rvm/" -type d | xargs -n 1 chmod g+s +apply_group_perms "${RUBIES_DIR}" -# Clean up -rvm cleanup all -${ROOT_GEM} cleanup +if command -v apt-get > /dev/null 2>&1; then + rm -rf /var/lib/apt/lists/* +fi -# Clean up -rm -rf /var/lib/apt/lists/* +# Finalize the version manager now that all ruby versions are installed. +if [ "${VERSION_MANAGER}" = "rbenv" ]; then + finalize_rbenv +elif [ "${VERSION_MANAGER}" = "rvm" ]; then + finalize_rvm +fi echo "Done!" diff --git a/test/ruby/install_additional_ruby.sh b/test/ruby/install_additional_ruby.sh index 12b77def3..90c34f724 100644 --- a/test/ruby/install_additional_ruby.sh +++ b/test/ruby/install_additional_ruby.sh @@ -5,11 +5,11 @@ set -e # Optional: Import test library source dev-container-features-test-lib -check "ruby version 3.4.2 installed as default" ruby -v | grep 3.4.2 -check "ruby version 3.2.8 installed" rvm list | grep 3.2.8 -check "ruby version 3.3.2 installed" rvm list | grep 3.3.2 - -check "rbenv" bash -c 'eval "$(rbenv init -)" && rbenv --version' +check "ruby-build available" ruby-build --version +check "ruby version 3.4.2 installed as default" bash -c "ruby -v | grep 3.4.2" +check "ruby version 3.4.2 prefix present" test -x /usr/local/rubies/3.4.2/bin/ruby +check "ruby version 3.3.2 prefix present" test -x /usr/local/rubies/3.3.2/bin/ruby +check "ruby version 3.2 series installed" bash -c "ls /usr/local/rubies | grep -E '^3\\.2\\.'" check "rake" bash -c "gem list | grep rake" # Report result diff --git a/test/ruby/install_additional_ruby_trixie.sh b/test/ruby/install_additional_ruby_trixie.sh index 76f7c9028..90c34f724 100644 --- a/test/ruby/install_additional_ruby_trixie.sh +++ b/test/ruby/install_additional_ruby_trixie.sh @@ -5,13 +5,12 @@ set -e # Optional: Import test library source dev-container-features-test-lib -check "ruby version 3.4.2 installed as default" ruby -v | grep 3.4.2 -check "ruby version 3.2.8 installed" rvm list | grep 3.2.8 -check "ruby version 3.3.2 installed" rvm list | grep 3.3.2 - -check "rbenv" bash -c 'eval "$(rbenv init -)" && rbenv --version' +check "ruby-build available" ruby-build --version +check "ruby version 3.4.2 installed as default" bash -c "ruby -v | grep 3.4.2" +check "ruby version 3.4.2 prefix present" test -x /usr/local/rubies/3.4.2/bin/ruby +check "ruby version 3.3.2 prefix present" test -x /usr/local/rubies/3.3.2/bin/ruby +check "ruby version 3.2 series installed" bash -c "ls /usr/local/rubies | grep -E '^3\\.2\\.'" check "rake" bash -c "gem list | grep rake" # Report result reportResults - diff --git a/test/ruby/install_ruby_trixie_base.sh b/test/ruby/install_ruby_trixie_base.sh index f90c76cc4..609187ccb 100644 --- a/test/ruby/install_ruby_trixie_base.sh +++ b/test/ruby/install_ruby_trixie_base.sh @@ -5,11 +5,11 @@ set -e # Optional: Import test library source dev-container-features-test-lib -# Definition specific tests -check "ruby version" ruby --version -check "rvm" rvm --version +# The feature was invoked with version=none on a base image that already ships +# Ruby. ruby-build should still be installed so additional versions can be added. +check "ruby version" ruby --version check "gem version" gem --version +check "ruby-build available" ruby-build --version # Report result reportResults - diff --git a/test/ruby/ruby_fallback_test.sh b/test/ruby/ruby_fallback_test.sh deleted file mode 100644 index a4ec9a6b5..000000000 --- a/test/ruby/ruby_fallback_test.sh +++ /dev/null @@ -1,310 +0,0 @@ -#!/bin/bash - -set -e - -# Optional: Import test library -source dev-container-features-test-lib - -USERNAME="automatic" -echo -e "\nRVM version installed previously by ruby feature ..." -check "rvm" rvm --version -check "ruby" ruby -v - -trap 'echo "Last executed command failed at line ${LINENO}"' ERR - -RVM_GPG_KEYS="409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB" - -# Clean up -rm -rf /var/lib/apt/lists/* - -# 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 - -# Ensure apt is in non-interactive to avoid prompts -export DEBIAN_FRONTEND=noninteractive - -architecture="$(uname -m)" -if [ "${architecture}" != "amd64" ] && [ "${architecture}" != "x86_64" ] && [ "${architecture}" != "arm64" ] && [ "${architecture}" != "aarch64" ]; then - echo "(!) Architecture $architecture unsupported" - exit 1 -fi - -apt_get_update() -{ - if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then - echo "Running apt-get update..." - apt-get update -y - fi -} - -# Checks if packages are installed and installs them if not -check_packages() { - if ! dpkg -s "$@" > /dev/null 2>&1; then - apt_get_update - apt-get -y install --no-install-recommends "$@" - fi -} - -# Get the list of GPG key servers that are reachable -get_gpg_key_servers() { - declare -A keyservers_curl_map=( - ["hkp://keyserver.ubuntu.com"]="http://keyserver.ubuntu.com:11371" - ["hkp://keyserver.ubuntu.com:80"]="http://keyserver.ubuntu.com" - ["hkps://keys.openpgp.org"]="https://keys.openpgp.org" - ["hkp://keyserver.pgp.com"]="http://keyserver.pgp.com:11371" - ) - - local curl_args="" - local keyserver_reachable=false # Flag to indicate if any keyserver is reachable - - if [ ! -z "${KEYSERVER_PROXY}" ]; then - curl_args="--proxy ${KEYSERVER_PROXY}" - fi - - for keyserver in "${!keyservers_curl_map[@]}"; do - local keyserver_curl_url="${keyservers_curl_map[${keyserver}]}" - if curl -s ${curl_args} --max-time 5 ${keyserver_curl_url} > /dev/null; then - echo "keyserver ${keyserver}" - keyserver_reachable=true - else - echo "(*) Keyserver ${keyserver} is not reachable." >&2 - fi - done - - if ! $keyserver_reachable; then - echo "(!) No keyserver is reachable." >&2 - exit 1 - fi -} - -# Import the specified key in a variable name passed in as -receive_gpg_keys() { - local keys=${!1} - local keyring_args="" - if [ ! -z "$2" ]; then - keyring_args="--no-default-keyring --keyring \"$2\"" - fi - - # Install curl - if ! type curl > /dev/null 2>&1; then - check_packages curl - fi - - # Use a temporary location for gpg keys to avoid polluting image - export GNUPGHOME="/tmp/tmp-gnupg" - mkdir -p ${GNUPGHOME} - chmod 700 ${GNUPGHOME} - echo -e "disable-ipv6\n$(get_gpg_key_servers)" | tee ${GNUPGHOME}/dirmngr.conf > /dev/null - # GPG key download sometimes fails for some reason and retrying fixes it. - local retry_count=0 - local gpg_ok="false" - set +e - until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; - do - echo "(*) Downloading GPG key..." - ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" - if [ "${gpg_ok}" != "true" ]; then - echo "(*) Failed getting key, retrying in 10s..." - (( retry_count++ )) - sleep 10s - fi - done - set -e - if [ "${gpg_ok}" = "false" ]; then - echo "(!) Failed to get gpg key." - exit 1 - fi -} - -# Figure out correct version of a three part version number is not passed -find_version_from_git_tags() { - local variable_name=$1 - local requested_version=${!variable_name} - if [ "${requested_version}" = "none" ]; then return; fi - local repository=$2 - local prefix=${3:-"tags/v"} - local separator=${4:-"."} - local last_part_optional=${5:-"false"} - if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then - local escaped_separator=${separator//./\\.} - local last_part - if [ "${last_part_optional}" = "true" ]; then - last_part="(${escaped_separator}[0-9]+)?" - else - last_part="${escaped_separator}[0-9]+" - fi - local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" - local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" - if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then - declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" - else - set +e - declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" - set -e - fi - fi - if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then - echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 - exit 1 - fi - echo "${variable_name}=${!variable_name}" -} - -# Use semver logic to decrement a version number then look for the closest match -find_prev_version_from_git_tags() { - local variable_name=$1 - local current_version=${!variable_name} - local repository=$2 - # Normally a "v" is used before the version number, but support alternate cases - local prefix=${3:-"tags/v"} - # Some repositories use "_" instead of "." for version number part separation, support that - local separator=${4:-"."} - # Some tools release versions that omit the last digit (e.g. go) - local last_part_optional=${5:-"false"} - # Some repositories may have tags that include a suffix (e.g. actions/node-versions) - local version_suffix_regex=$6 - # Try one break fix version number less if we get a failure. Use "set +e" since "set -e" can cause failures in valid scenarios. - set +e - major="$(echo "${current_version}" | grep -oE '^[0-9]+' || echo '')" - minor="$(echo "${current_version}" | grep -oP '^[0-9]+\.\K[0-9]+' || echo '')" - breakfix="$(echo "${current_version}" | grep -oP '^[0-9]+\.[0-9]+\.\K[0-9]+' 2>/dev/null || echo '')" - - if [ "${minor}" = "0" ] && [ "${breakfix}" = "0" ]; then - ((major=major-1)) - declare -g ${variable_name}="${major}" - # Look for latest version from previous major release - find_version_from_git_tags "${variable_name}" "${repository}" "${prefix}" "${separator}" "${last_part_optional}" - # Handle situations like Go's odd version pattern where "0" releases omit the last part - elif [ "${breakfix}" = "" ] || [ "${breakfix}" = "0" ]; then - ((minor=minor-1)) - declare -g ${variable_name}="${major}.${minor}" - # Look for latest version from previous minor release - find_version_from_git_tags "${variable_name}" "${repository}" "${prefix}" "${separator}" "${last_part_optional}" - else - ((breakfix=breakfix-1)) - if [ "${breakfix}" = "0" ] && [ "${last_part_optional}" = "true" ]; then - declare -g ${variable_name}="${major}.${minor}" - else - declare -g ${variable_name}="${major}.${minor}.${breakfix}" - fi - fi - set -e -} - -# Function to fetch the version released prior to the latest version -get_previous_version() { - local url=$1 - local repo_url=$2 - local variable_name=$3 - local mode=$4 - prev_version=${!variable_name} - - output=$(curl -s "$repo_url"); - - #install jq - check_packages jq - - message=$(echo "$output" | jq -r '.message') - - if [[ $mode == "mode1" ]]; then - message="API rate limit exceeded" - else - message="" - fi - - if [[ $message == "API rate limit exceeded"* ]]; then - echo -e "\nAn attempt to find latest version using GitHub Api Failed... \nReason: ${message}" - echo -e "\nAttempting to find latest version using GitHub tags." - find_prev_version_from_git_tags prev_version "$url" "tags/v" "_" - declare -g ${variable_name}="${prev_version}" - else - echo -e "\nAttempting to find latest version using GitHub Api." - version=$(echo "$output" | jq -r '.tag_name' | tr '_' '.') - declare -g ${variable_name}="${version#v}" - fi - echo "${variable_name}=${!variable_name}" -} - -get_github_api_repo_url() { - local url=$1 - echo "${url/https:\/\/github.com/https:\/\/api.github.com\/repos}/releases/latest" -} - - -# Figure out correct version of a three part version number is not passed -ruby_url="https://github.com/ruby/ruby" - -RUBY_VERSION="3.4.xyz" - -set_rvm_install_args() { - RUBY_VERSION=$1 - if [ "${RUBY_VERSION}" = "none" ]; then - RVM_INSTALL_ARGS="" - elif [[ "$(ruby -v)" = *"${RUBY_VERSION}"* ]]; then - echo "(!) Ruby is already installed with version ${RUBY_VERSION}. Skipping..." - RVM_INSTALL_ARGS="" - else - if [ "${RUBY_VERSION}" = "latest" ] || [ "${RUBY_VERSION}" = "current" ] || [ "${RUBY_VERSION}" = "lts" ]; then - RVM_INSTALL_ARGS="--ruby" - RUBY_VERSION="" - else - RVM_INSTALL_ARGS="--ruby=${RUBY_VERSION}" - fi - if [ "${INSTALL_RUBY_TOOLS}" = "true" ]; then - SKIP_GEM_INSTALL="true" - else - DEFAULT_GEMS="" - fi - fi -} - -install_previous_version() { - mode=$1 - repo_url=$(get_github_api_repo_url "$ruby_url") - get_previous_version "${ruby_url}" "${repo_url}" RUBY_VERSION $mode - set_rvm_install_args $RUBY_VERSION - curl -sSL https://get.rvm.io | bash -s stable --ignore-dotfiles ${RVM_INSTALL_ARGS} --with-default-gems="${DEFAULT_GEMS}" 2>&1 -} - -install_rvm() { - mode=$1 - # Install RVM - receive_gpg_keys RVM_GPG_KEYS - # Determine appropriate settings for rvm installer - set_rvm_install_args $RUBY_VERSION - # Create rvm group as a system group to reduce the odds of conflict with local user UIDs - if ! cat /etc/group | grep -e "^rvm:" > /dev/null 2>&1; then - groupadd -r rvm - fi - # Install rvm - curl -sSL https://get.rvm.io | bash -s stable --ignore-dotfiles ${RVM_INSTALL_ARGS} --with-default-gems="${DEFAULT_GEMS}" 2>&1 || install_previous_version "$mode" - sudo usermod -aG rvm ${USERNAME} - source /usr/local/rvm/scripts/rvm - rvm fix-permissions system - rm -rf ${GNUPGHOME} -} - -install_rvm "mode1" -echo -e "\n👉🏻👉🏻RVM version installed by test file ... (mode: 1 - install using find_prev_version_from_git_tags):" -check "rvm" rvm --version - -install_rvm "mode2" -echo -e "\n👉🏻👉🏻RVM version installed by test file ... (mode: 1 - install using GitHub Api):" -check "rvm" rvm --version - -# Report result -reportResults \ No newline at end of file diff --git a/test/ruby/ruby_rbenv.sh b/test/ruby/ruby_rbenv.sh new file mode 100755 index 000000000..685dc6828 --- /dev/null +++ b/test/ruby/ruby_rbenv.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# rbenv (and its shims/bin dirs) is placed on PATH via containerEnv, +# so these commands should work in non-login shells too. +check "ruby version 3.4.2 active" bash -c "ruby -v | grep 3.4.2" +check "rbenv available" rbenv --version +check "rbenv lists ruby 3.4.2" bash -c "rbenv versions | grep 3.4.2" +check "rbenv global is 3.4.2" bash -c "rbenv global | grep 3.4.2" +check "rbenv shim for ruby" test -x /usr/local/share/rbenv/shims/ruby +check "ruby-build wired as rbenv plugin" test -d /usr/local/share/rbenv/plugins/ruby-build +check "rake gem installed" bash -c "gem list | grep rake" + +# Report result +reportResults diff --git a/test/ruby/ruby_rvm.sh b/test/ruby/ruby_rvm.sh new file mode 100755 index 000000000..6cea7901a --- /dev/null +++ b/test/ruby/ruby_rvm.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Ruby installed via rvm lives under /usr/local/rvm/rubies and is also +# exposed via the /usr/local/rubies/current PATH entry from containerEnv. +check "ruby version 3.4.2 active" bash -c "ruby -v | grep 3.4.2" +check "rvm binary available" /usr/local/rvm/bin/rvm --version +check "rvm ruby 3.4.2 directory exists" test -d /usr/local/rvm/rubies/ruby-3.4.2 +# rvm is implemented as a shell function, so source it before calling. +check "rvm default points to 3.4.2" bash -c "source /usr/local/rvm/scripts/rvm && rvm current | grep 3.4.2" +check "rvm profile.d hook installed" test -x /etc/profile.d/rvm.sh +check "rake gem installed" bash -c "gem list | grep rake" + +# Report result +reportResults diff --git a/test/ruby/scenarios.json b/test/ruby/scenarios.json index 7aa7c5f8e..6eea9174e 100644 --- a/test/ruby/scenarios.json +++ b/test/ruby/scenarios.json @@ -1,8 +1,8 @@ -{ +{ "install_ruby_trixie_base": { "build": { "dockerfile": "Dockerfile" - }, + }, "features": { "ghcr.io/devcontainers/features/common-utils:2": { "installZsh": "true", @@ -28,7 +28,7 @@ "additionalVersions": "3.2,3.3.2" } } - }, + }, "install_additional_ruby": { "image": "ubuntu:noble", "features": { @@ -44,12 +44,22 @@ "ruby": {} } }, - "ruby_fallback_test": { + "ruby_rbenv": { + "image": "ubuntu:noble", + "features": { + "ruby": { + "version": "3.4.2", + "versionManager": "rbenv" + } + } + }, + "ruby_rvm": { "image": "mcr.microsoft.com/devcontainers/base:bullseye", "features": { "ruby": { - "version": "latest" + "version": "3.4.2", + "versionManager": "rvm" } } } -} \ No newline at end of file +}