From 25de744f41e8d070477cb7bb2d0ea768233d432b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 20:16:53 +0000 Subject: [PATCH] Add dotnet/install package with multi-version support Agent-Logs-Url: https://github.com/devlead/RWXpackages/sessions/212f836f-d8db-4235-a8a3-70d06dbee875 Co-authored-by: devlead <1647294+devlead@users.noreply.github.com> --- dotnet/install/README.md | 41 +++++++++ dotnet/install/bin/install-dotnet | 116 +++++++++++++++++++++++++ dotnet/install/rwx-package.yml | 44 ++++++++++ dotnet/install/rwx-test-base.json | 22 +++++ dotnet/install/rwx-test.yml | 73 ++++++++++++++++ dotnet/install/rwx-utils.sh | 137 ++++++++++++++++++++++++++++++ 6 files changed, 433 insertions(+) create mode 100644 dotnet/install/README.md create mode 100755 dotnet/install/bin/install-dotnet create mode 100644 dotnet/install/rwx-package.yml create mode 100644 dotnet/install/rwx-test-base.json create mode 100644 dotnet/install/rwx-test.yml create mode 100755 dotnet/install/rwx-utils.sh diff --git a/dotnet/install/README.md b/dotnet/install/README.md new file mode 100644 index 00000000..37a28dd1 --- /dev/null +++ b/dotnet/install/README.md @@ -0,0 +1,41 @@ +# dotnet/install + +Install one or more .NET SDK versions. + +## Install a single channel + +```yaml +tasks: + - key: dotnet + call: dotnet/install 1.0.0 + with: + dotnet-channel: "8.0" +``` + +## Install from global.json + +```yaml +tasks: + - key: dotnet + use: code + call: dotnet/install 1.0.0 + with: + global-json-file: global.json + filter: + - global.json +``` + +## Install multiple channels in one call + +```yaml +tasks: + - key: dotnet + call: dotnet/install 1.0.0 + with: + dotnet-channels: '["8.0", "9.0", "10.0"]' + dotnet-quality: preview +``` + +When installing multiple SDKs, `--skip-non-versioned-files` will automatically be appended to the install script for subsequent installs, so they do not overwrite non-versioned files from earlier installs. + +`dotnet/install` automatically configures `PATH` and `DOTNET_ROOT` for downstream tasks. diff --git a/dotnet/install/bin/install-dotnet b/dotnet/install/bin/install-dotnet new file mode 100755 index 00000000..58c9cd33 --- /dev/null +++ b/dotnet/install/bin/install-dotnet @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -ueo pipefail + +if [[ -z "${GLOBAL_JSON_FILE}" && -z "${DOTNET_CHANNEL}" && -z "${DOTNET_CHANNELS}" ]]; then + cat << EOF > "$(mktemp "$RWX_ERRORS/error-XXXX")" +Invalid parameters: set \`global-json-file\`, \`dotnet-channel\`, or \`dotnet-channels\`. +EOF + exit 2 +fi + +if [[ -n "${GLOBAL_JSON_FILE}" && ! -f "${GLOBAL_JSON_FILE}" ]]; then + cat << EOF > "$(mktemp "$RWX_ERRORS/error-XXXX")" +Invalid parameter: \`global-json-file\` path does not exist. + +Received \`${GLOBAL_JSON_FILE}\` +EOF + exit 2 +fi + +DOTNET_INSTALL_DIR="/opt/dotnet" +DOTNET_INSTALL_SCRIPT_DIR="$(mktemp -d)" +DOTNET_INSTALL_SCRIPT_PATH="${DOTNET_INSTALL_SCRIPT_DIR}/dotnet-install.sh" + +cleanup() { + rm -rf "$DOTNET_INSTALL_SCRIPT_DIR" +} +trap cleanup EXIT + +sudo mkdir -p "$DOTNET_INSTALL_DIR" +sudo chown "$(id -un):$(id -gn)" "$DOTNET_INSTALL_DIR" +curl -Lsfo "$DOTNET_INSTALL_SCRIPT_PATH" https://dot.net/v1/dotnet-install.sh +chmod +x "$DOTNET_INSTALL_SCRIPT_PATH" + +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER=0 +export DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX=2 +export DOTNET_ROOT="$DOTNET_INSTALL_DIR" +export PATH="$DOTNET_ROOT:$PATH" + +channels=() +if [[ -n "${DOTNET_CHANNELS}" ]]; then + normalized="$(echo "$DOTNET_CHANNELS" | tr -d '[:space:]')" + if [[ "$normalized" =~ ^\[.*\]$ ]]; then + inner="${normalized#[}" + inner="${inner%]}" + if [[ -n "$inner" ]]; then + IFS=',' read -r -a raw_channels <<< "$inner" + for raw_channel in "${raw_channels[@]}"; do + channel="${raw_channel#\"}" + channel="${channel%\"}" + channel="${channel#\'}" + channel="${channel%\'}" + if [[ -n "$channel" ]]; then + channels+=("$channel") + fi + done + fi + else + if [[ "$DOTNET_CHANNELS" == *","* ]]; then + IFS=',' read -r -a raw_channels <<< "$DOTNET_CHANNELS" + else + IFS=' ' read -r -a raw_channels <<< "$DOTNET_CHANNELS" + fi + + for raw_channel in "${raw_channels[@]}"; do + channel="$(echo "$raw_channel" | xargs)" + if [[ -n "$channel" ]]; then + channels+=("$channel") + fi + done + fi +fi + +if [[ ${#channels[@]} -eq 0 && -n "${DOTNET_CHANNEL}" ]]; then + channels=("$DOTNET_CHANNEL") +fi + +if [[ -n "${GLOBAL_JSON_FILE}" ]]; then + echo "Installing .NET SDK from ${GLOBAL_JSON_FILE}" + bash "$DOTNET_INSTALL_SCRIPT_PATH" --jsonfile "$GLOBAL_JSON_FILE" --install-dir "$DOTNET_INSTALL_DIR" --no-path +else + if [[ ${#channels[@]} -eq 0 ]]; then + cat << EOF > "$(mktemp "$RWX_ERRORS/error-XXXX")" +Invalid parameters: could not determine any .NET channel from \`dotnet-channel\` or \`dotnet-channels\`. +EOF + exit 2 + fi + + idx=0 + for channel in "${channels[@]}"; do + echo "Installing .NET SDK channel ${channel}" + + command=(bash "$DOTNET_INSTALL_SCRIPT_PATH" --channel "$channel" --install-dir "$DOTNET_INSTALL_DIR" --no-path) + + if [[ -n "${DOTNET_QUALITY}" ]]; then + command+=(--quality "$DOTNET_QUALITY") + fi + + if [[ ${#channels[@]} -gt 1 && $idx -gt 0 ]]; then + command+=(--skip-non-versioned-files) + fi + + "${command[@]}" + idx=$((idx + 1)) + done +fi + +echo "$DOTNET_INSTALL_DIR" >> "$RWX_ENV/PATH" +echo "$DOTNET_INSTALL_DIR" >> "$RWX_ENV/DOTNET_ROOT" +echo "$DOTNET_SKIP_FIRST_TIME_EXPERIENCE" >> "$RWX_ENV/DOTNET_SKIP_FIRST_TIME_EXPERIENCE" +echo "$DOTNET_CLI_TELEMETRY_OPTOUT" >> "$RWX_ENV/DOTNET_CLI_TELEMETRY_OPTOUT" +echo "$DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER" >> "$RWX_ENV/DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER" +echo "$DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX" >> "$RWX_ENV/DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX" + +dotnet --version diff --git a/dotnet/install/rwx-package.yml b/dotnet/install/rwx-package.yml new file mode 100644 index 00000000..8811c569 --- /dev/null +++ b/dotnet/install/rwx-package.yml @@ -0,0 +1,44 @@ +name: dotnet/install +version: 1.0.0 +description: Install the .NET SDK +source_code_url: https://github.com/rwx-cloud/packages/tree/main/dotnet/install +issue_tracker_url: https://github.com/rwx-cloud/packages/issues + +parameters: + dotnet-channel: + description: "Single .NET SDK channel/version to install, eg. 8.0" + required: false + dotnet-channels: + description: "Array-style list of .NET SDK channels/versions to install in one call, eg. [\"8.0\", \"9.0\"]" + required: false + dotnet-quality: + description: "Optional .NET SDK quality, eg. ga, preview, daily" + required: false + global-json-file: + description: "Path to a global.json file used to determine the SDK version" + required: false + +tasks: + - key: install-libicu-dev + run: | + if ! dpkg -s libicu-dev >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y --no-install-recommends libicu-dev + sudo apt-get clean + fi + + - key: install-dotnet + use: install-libicu-dev + run: | + source "$RWX_PACKAGE_PATH/rwx-utils.sh" + if ! rwx_os_package_manager_in apt; then + echo "Unsupported operating system or package manager \`$(rwx_os_package_manager)\`" > "$(mktemp "$RWX_ERRORS/error-XXXX")" + exit 1 + fi + + $RWX_PACKAGE_PATH/bin/install-dotnet + env: + DOTNET_CHANNEL: ${{ params.dotnet-channel }} + DOTNET_CHANNELS: ${{ params.dotnet-channels }} + DOTNET_QUALITY: ${{ params.dotnet-quality }} + GLOBAL_JSON_FILE: ${{ params.global-json-file }} diff --git a/dotnet/install/rwx-test-base.json b/dotnet/install/rwx-test-base.json new file mode 100644 index 00000000..093a55cc --- /dev/null +++ b/dotnet/install/rwx-test-base.json @@ -0,0 +1,22 @@ +[ + { + "image": "ubuntu:22.04", + "config": "rwx/base 1.0.0", + "arch": "x86_64" + }, + { + "image": "ubuntu:22.04", + "config": "rwx/base 1.0.0", + "arch": "arm64" + }, + { + "image": "ubuntu:24.04", + "config": "rwx/base 1.0.0", + "arch": "x86_64" + }, + { + "image": "ubuntu:24.04", + "config": "rwx/base 1.0.0", + "arch": "arm64" + } +] diff --git a/dotnet/install/rwx-test.yml b/dotnet/install/rwx-test.yml new file mode 100644 index 00000000..ba351769 --- /dev/null +++ b/dotnet/install/rwx-test.yml @@ -0,0 +1,73 @@ +base: + image: ${{ init.base-image }} + config: ${{ init.base-config }} + arch: ${{ init.base-arch }} + +tasks: + - key: install-channel-8 + call: ${{ init.package-digest }} + with: + dotnet-channel: "8.0" + + - key: install-channel-8--assert + use: install-channel-8 + run: dotnet --version | grep '^8\.' + + - key: install-channel-9 + call: ${{ init.package-digest }} + with: + dotnet-channel: "9.0" + + - key: install-channel-9--assert + use: install-channel-9 + run: dotnet --version | grep '^9\.' + + - key: install-channel-10-preview + call: ${{ init.package-digest }} + with: + dotnet-channel: "10.0" + dotnet-quality: preview + + - key: install-channel-10-preview--assert + use: install-channel-10-preview + run: dotnet --version | grep '^10\.' + + - key: write-global-json + run: | + cat > global.json << 'EOF' + { + "sdk": { + "version": "8.0.100" + } + } + EOF + + - key: install-global-json + use: write-global-json + call: ${{ init.package-digest }} + with: + global-json-file: global.json + filter: + - global.json + + - key: install-global-json--assert + use: install-global-json + run: dotnet --version | grep '^8\.' + + - key: install-multiple-sdks + call: ${{ init.package-digest }} + with: + dotnet-channels: '["8.0","9.0"]' + + - key: install-multiple-sdks--assert + use: install-multiple-sdks + run: | + dotnet --list-sdks | grep '^8\.' + dotnet --list-sdks | grep '^9\.' + + - key: dotnet-tool-install + use: install-channel-8 + run: | + dotnet tool install -g dotnet-format + export PATH="$HOME/.dotnet/tools:$PATH" + dotnet format --version diff --git a/dotnet/install/rwx-utils.sh b/dotnet/install/rwx-utils.sh new file mode 100755 index 00000000..1d1e0cf9 --- /dev/null +++ b/dotnet/install/rwx-utils.sh @@ -0,0 +1,137 @@ +#!/bin/sh +# rwx-utils version 2.0.0 + +detected_os="" +detected_os_version="" +detected_os_codename="" +detected_arch="" +detected_package_manager="" + +rwx__detect_os_arch() { + if [ -f /etc/os-release ]; then + . /etc/os-release + + detected_os="$ID" + detected_os_version="$VERSION_ID" + detected_os_codename="$VERSION_CODENAME" + + case "$ID" in + ubuntu|debian) + detected_package_manager="apt" + ;; + alpine) + detected_package_manager="apk" + ;; + esac + fi + + detected_arch=$(uname -m) +} + +rwx_os_name() { + if [ -z "$detected_os" ]; then + rwx__detect_os_arch + fi + printf '%s\n' "$detected_os" +} + +rwx_os_version() { + if [ -z "$detected_os_version" ]; then + rwx__detect_os_arch + fi + printf '%s\n' "$detected_os_version" +} + +# Output the name and version of the operating system as expected by RWX's `base.os` field. +rwx_os_name_version() { + printf '%s %s\n' "$(rwx_os_name)" "$(rwx_os_version)" +} + +rwx_os_codename() { + if [ -z "$detected_os_codename" ]; then + rwx__detect_os_arch + fi + printf '%s\n' "$detected_os_codename" +} + +rwx_arch() { + if [ -z "$detected_arch" ]; then + rwx__detect_os_arch + fi + printf '%s\n' "$detected_arch" +} + +rwx_arch_amd() { + arch="$(rwx_arch)" + if [ "$arch" = "x86_64" ]; then + printf '%s\n' "amd64" + else + printf '%s\n' "$arch" + fi +} + +rwx_os_package_manager() { + if [ -z "$detected_package_manager" ]; then + rwx__detect_os_arch + fi + printf '%s\n' "$detected_package_manager" +} + +rwx_os_version_gte() { + compare_version="$1" + printf '%s\n' "$compare_version" "$(rwx_os_version)" | sed 's/\([0-9.]*\).*/\1/' | sort -c -t. -k1,1n -k2,2n -k3,3n >/dev/null 2>&1 +} + +rwx_os_version_lte() { + compare_version="$1" + printf '%s\n' "$compare_version" "$(rwx_os_version)" | sed 's/\([0-9.]*\).*/\1/' | sort -c -t. -k1,1n -k2,2n -k3,3n -r >/dev/null 2>&1 +} + +# Convert a string something usable as a RWX key. +# +# Replaces all non-alphanumeric characters with hyphens, compressing multiple hyphens into one. +rwx_keyify() { + printf '%s' "$*" | tr -c -s '[:alnum:]' '-' +} + +# Check if the provided list contains a given element. +# +# Usage: rwx_contains two one two three +rwx_contains() { + if [ "$#" -eq 0 ]; then + return 1 + fi + + needle=$1 + shift + + for item do + if [ "$item" = "$needle" ]; then + return 0 + fi + done + + return 1 +} + +rwx_os_name_in() { + rwx_contains "$(rwx_os_name)" "$@" +} + +rwx_os_package_manager_in() { + rwx_contains "$(rwx_os_package_manager)" "$@" +} + +rwx_maybe_sudo() { + if [ "$(id -u)" -eq 0 ]; then + "$@" + else + # If sudo is available, use it + if command -v sudo >/dev/null 2>&1; then + sudo "$@" + else + echo "Error: need root privileges but 'sudo' not found" >&2 + return 1 + fi + fi +}