Last active
July 28, 2025 12:22
-
-
Save FalconNL93/7b243e2daeac66c8edc9755114a94b76 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -e | |
DOTNET_DIR="$HOME/.dotnet" | |
INSTALL_SCRIPT="$HOME/dotnet-install.sh" | |
DOTNET_RELEASES_URL="https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json" | |
DOTNET_INSTALL_SH_URL="https://dot.net/v1/dotnet-install.sh" | |
DEPENDENCIES_MARKER="$HOME/.dotnet_deps_installed" | |
REMOVE_OLD_MINOR_VERSIONS=false # Set to true to enable removal of older minor versions | |
# Dependency lists per distro | |
DEPS_DEBIAN=(libc6 libgcc1 libgssapi-krb5-2 libicu70 libssl3 libstdc++6 zlib1g libcurl4 libkrb5-3 libunwind8) | |
DEPS_FEDORA=(glibc libgcc krb5-libs libicu openssl libstdc++ zlib libcurl libunwind) | |
DEPS_ARCH=(glibc gcc krb5 icu openssl libstdc++ zlib curl libunwind) | |
# Shell RC files to update | |
SHELL_RC_FILES=("$HOME/.bashrc" "$HOME/.zshrc") | |
# Dotnet tools to manage | |
DOTNET_TOOLS=(dotnet-ef dotnet-outdated-tool) | |
# Ensure required tools (curl, jq) are installed | |
ensure_base_tools() { | |
local missing=() | |
for tool in curl jq; do | |
command -v "$tool" &>/dev/null || missing+=("$tool") | |
done | |
if [ ${#missing[@]} -eq 0 ]; then return; fi | |
echo "Installing missing tools: ${missing[*]}" | |
if [ -f /etc/os-release ]; then | |
. /etc/os-release | |
if [[ "$ID" =~ ^(ubuntu|debian)$ || "$ID_LIKE" == *"debian"* || "$ID_LIKE" == *"ubuntu"* ]]; then | |
sudo apt update && sudo apt install -y "${missing[@]}" | |
elif [[ "$ID" =~ ^(fedora|rhel|centos)$ || "$ID_LIKE" == *"rhel"* || "$ID_LIKE" == *"fedora"* ]]; then | |
sudo dnf install -y "${missing[@]}" | |
elif [[ "$ID" == "arch" || "$ID_LIKE" == *"arch"* ]]; then | |
sudo pacman -Sy --noconfirm "${missing[@]}" | |
else | |
echo "Unsupported OS. Please install: ${missing[*]} manually." | |
exit 1 | |
fi | |
else | |
echo "Unable to detect OS. Please install: ${missing[*]} manually." | |
exit 1 | |
fi | |
} | |
# Ensure script is run with sudo/root | |
ensure_sudo() { | |
if [ "$EUID" -ne 0 ]; then | |
if ! command -v sudo &>/dev/null; then | |
echo "This script requires root or sudo privileges. Please install sudo or run as root." | |
exit 1 | |
fi | |
sudo -v || { | |
echo "Sudo authentication failed." | |
exit 1 | |
} | |
fi | |
} | |
# Remove all older patch versions for all installed major.minor SDKs | |
clean_old_minor_versions() { | |
echo "Cleaning up older patch versions for all installed .NET SDKs..." | |
[ ! -d "$DOTNET_DIR/sdk" ] && echo "No SDKs found." && return 0 | |
for major_minor in $(ls "$DOTNET_DIR/sdk" | awk -F. '{print $1"."$2}' | sort -u); do | |
mapfile -t versions_found < <(ls "$DOTNET_DIR/sdk" | grep "^$major_minor\." | sort -V) | |
if [ "${#versions_found[@]}" -gt 1 ]; then | |
for ((i = 0; i < ${#versions_found[@]} - 1; i++)); do | |
echo "Removing older .NET SDK version ${versions_found[$i]}..." | |
rm -rf "$DOTNET_DIR/sdk/${versions_found[$i]}" | |
done | |
fi | |
done | |
echo "Cleanup complete." | |
} | |
# Ensure the "dialog" package is installed | |
ensure_dialog_installed() { | |
command -v dialog &>/dev/null && return | |
echo "The 'dialog' package is not installed. Installing it now..." | |
if [ -f /etc/os-release ]; then | |
. /etc/os-release | |
if [[ "$ID" =~ ^(ubuntu|debian)$ || "$ID_LIKE" == *"debian"* || "$ID_LIKE" == *"ubuntu"* ]]; then | |
sudo apt update && sudo apt install -y dialog | |
elif [[ "$ID" =~ ^(fedora|rhel|centos)$ || "$ID_LIKE" == *"rhel"* || "$ID_LIKE" == *"fedora"* ]]; then | |
sudo dnf install -y dialog | |
elif [[ "$ID" == "arch" || "$ID_LIKE" == *"arch"* ]]; then | |
sudo pacman -Sy --noconfirm dialog | |
else | |
echo "Unsupported OS. Please install 'dialog' manually." | |
exit 1 | |
fi | |
else | |
echo "Unable to detect OS. Please install 'dialog' manually." | |
exit 1 | |
fi | |
} | |
# Install required dependencies | |
install_dependencies() { | |
[ -f "$DEPENDENCIES_MARKER" ] && return | |
echo "Installing required system dependencies..." | |
if [ -f /etc/os-release ]; then | |
. /etc/os-release | |
if [[ "$ID" =~ ^(ubuntu|debian)$ || "$ID_LIKE" == *"debian"* || "$ID_LIKE" == *"ubuntu"* ]]; then | |
sudo apt update && sudo apt install -y --no-install-recommends "${DEPS_DEBIAN[@]}" | |
elif [[ "$ID" =~ ^(fedora|rhel|centos)$ || "$ID_LIKE" == *"rhel"* || "$ID_LIKE" == *"fedora"* ]]; then | |
sudo dnf install -y "${DEPS_FEDORA[@]}" | |
elif [[ "$ID" == "arch" || "$ID_LIKE" == *"arch"* ]]; then | |
sudo pacman -Sy --noconfirm "${DEPS_ARCH[@]}" | |
else | |
echo "Unsupported OS. Please install dependencies manually." | |
exit 1 | |
fi | |
else | |
echo "Unable to detect OS. Please install dependencies manually." | |
exit 1 | |
fi | |
touch "$DEPENDENCIES_MARKER" | |
} | |
# Retrieve all major .NET SDK versions dynamically, including preview versions | |
get_dotnet_versions() { | |
curl -s "$DOTNET_RELEASES_URL" | | |
grep -oP '"channel-version":\s*"\K[0-9]+\.[0-9]+(\.[0-9]+(-[a-zA-Z0-9.]+)?)?' | | |
sort -uV | |
} | |
# Display the latest release per channel from releases-index.json | |
show_versions() { | |
echo "Fetching the latest .NET SDK releases per channel..." | |
curl -s "$DOTNET_RELEASES_URL" | | |
jq -r '."releases-index"[] | "\(.["channel-version"]): \(.["latest-release"])"' | |
exit 0 | |
} | |
# Select .NET SDK versions to install | |
select_versions() { | |
if [ "$UPDATE_ALL" = true ]; then return; fi | |
echo "Select the .NET SDK versions you want to install:" | |
echo "(Use arrow keys and space to select, enter to confirm.)" | |
# Get already installed full versions | |
[ ! -d "$DOTNET_DIR/sdk" ] && INSTALLED="" || INSTALLED=$(ls "$DOTNET_DIR/sdk" | awk -F. '{print $1"."$2}' | sort -u) | |
# Build dialog checklist options: version "on"/"off" | |
OPTIONS=() | |
for version in "${VERSIONS[@]}"; do | |
if echo "$INSTALLED" | grep -qx "$version"; then | |
OPTIONS+=("$version" "$version" "on") | |
else | |
OPTIONS+=("$version" "$version" "off") | |
fi | |
done | |
TMPFILE=$(mktemp) | |
trap 'rm -f "$TMPFILE"' EXIT | |
dialog --clear --no-tags --checklist "Select .NET SDK versions to install:" 20 60 10 \ | |
"${OPTIONS[@]}" 2>"$TMPFILE" | |
if [ $? -ne 0 ]; then | |
echo "No versions selected. Exiting." | |
exit 1 | |
fi | |
SELECTED=$(cat "$TMPFILE" | tr -d '"') | |
if [ -z "$SELECTED" ]; then | |
echo "No versions selected. Exiting." | |
exit 1 | |
fi | |
SELECTED_VERSIONS=($SELECTED) | |
# Uninstall deselected versions | |
if [ -d "$DOTNET_DIR/sdk" ]; then | |
for full_version in $(ls "$DOTNET_DIR/sdk"); do | |
major_minor=$(echo "$full_version" | awk -F. '{print $1"."$2}') | |
if ! echo "${SELECTED_VERSIONS[@]}" | grep -qw "$major_minor"; then | |
echo "Uninstalling .NET SDK version $full_version..." | |
SDK_DIR="$DOTNET_DIR/sdk/$full_version" | |
if [ -d "$SDK_DIR" ]; then | |
rm -rf "$SDK_DIR" | |
echo "Removed $SDK_DIR" | |
else | |
echo "SDK version $full_version not found in $DOTNET_DIR/sdk" | |
fi | |
fi | |
done | |
fi | |
trap - EXIT | |
} | |
# Download the dotnet-install.sh script | |
download_install_script() { | |
[ -f "$INSTALL_SCRIPT" ] && return | |
echo "Downloading dotnet-install.sh..." | |
curl -sSL "$DOTNET_INSTALL_SH_URL" -o "$INSTALL_SCRIPT" | |
chmod +x "$INSTALL_SCRIPT" | |
} | |
# Install or update a list of .NET SDK major.minor versions | |
update_dotnet_sdks_for_versions() { | |
local versions=("$@") | |
install_dependencies | |
download_install_script | |
update_path | |
mkdir -p "$DOTNET_DIR" | |
for version in "${versions[@]}"; do | |
echo "Installing or updating .NET SDK version $version..." | |
"$INSTALL_SCRIPT" --channel "$version" --install-dir "$DOTNET_DIR" || { | |
echo "Failed to install .NET SDK version $version. Skipping cleanup." | |
return 1 | |
} | |
done | |
# Remove older patch versions if the switch is enabled and installation was successful | |
if [ "$REMOVE_OLD_MINOR_VERSIONS" = true ]; then | |
clean_old_minor_versions | |
fi | |
} | |
# Update PATH permanently (idempotent) | |
update_path() { | |
for rc in "${SHELL_RC_FILES[@]}"; do | |
# Remove any previous .NET SDK setup block | |
if grep -q '# BEGIN .NET SDK setup' "$rc"; then | |
# Remove lines between markers | |
sed -i '/# BEGIN .NET SDK setup/,/# END .NET SDK setup/d' "$rc" | |
fi | |
# Add the block only if not present | |
if ! grep -q '# BEGIN .NET SDK setup' "$rc"; then | |
{ | |
echo -e '\n# BEGIN .NET SDK setup' | |
echo "export DOTNET_ROOT=\"$DOTNET_DIR\"" | |
echo "export PATH=\"$DOTNET_DIR:\$HOME/.dotnet/tools:\$PATH\"" | |
echo '# END .NET SDK setup' | |
} >>"$rc" | |
fi | |
done | |
# Ensure $HOME/.dotnet/tools is in PATH for the current session | |
case ":$PATH:" in | |
*:"$HOME/.dotnet/tools":*) ;; | |
*) export PATH="$HOME/.dotnet/tools:$PATH" ;; | |
esac | |
} | |
# Prompt user to enable or disable cleanup of older minor versions | |
configure_cleanup() { | |
echo "Do you want to enable cleanup of older minor .NET SDK versions?" | |
dialog --clear --yesno "Enable cleanup of older minor .NET SDK versions?" 10 50 | |
if [ $? -eq 0 ]; then | |
REMOVE_OLD_MINOR_VERSIONS=true | |
echo "Cleanup of older minor versions is enabled." | |
else | |
REMOVE_OLD_MINOR_VERSIONS=false | |
echo "Cleanup of older minor versions is disabled." | |
fi | |
} | |
# Main menu dialog | |
main_menu() { | |
local CHOICE | |
CHOICE=$(dialog --clear --no-tags --menu "Select an action:" 15 60 5 \ | |
1 "Install / Update dotnet SDK versions" \ | |
2 "Install / Update dotnet tools" \ | |
3>&1 1>&2 2>&3) | |
echo "$CHOICE" | |
} | |
# Install/Update dotnet tools dialog and logic | |
manage_dotnet_tools() { | |
# Show a please wait dialog while retrieving installed tools | |
TMPWAIT=$(mktemp) | |
dialog --infobox "Please wait... Retrieving installed dotnet tools." 7 50 & | |
WAITPID=$! | |
INSTALLED_TOOLS=$(dotnet tool list -g | awk 'NR>2 {print $1}') | |
kill $WAITPID 2>/dev/null; wait $WAITPID 2>/dev/null | |
rm -f "$TMPWAIT" | |
OPTIONS=() | |
for tool in "${DOTNET_TOOLS[@]}"; do | |
if echo "$INSTALLED_TOOLS" | grep -qx "$tool"; then | |
OPTIONS+=("$tool" "$tool" "on") | |
else | |
OPTIONS+=("$tool" "$tool" "off") | |
fi | |
done | |
TMPFILE=$(mktemp) | |
trap 'rm -f "$TMPFILE"' EXIT | |
dialog --clear --no-tags --checklist "Select dotnet tools to install globally:" 15 60 5 \ | |
"${OPTIONS[@]}" 2>"$TMPFILE" | |
if [ $? -ne 0 ]; then | |
echo "No tools selected. Exiting." | |
exit 1 | |
fi | |
SELECTED_TOOLS=$(cat "$TMPFILE" | tr -d '"') | |
# Convert to array | |
SELECTED_TOOLS_ARR=($SELECTED_TOOLS) | |
# Install selected tools | |
for tool in "${DOTNET_TOOLS[@]}"; do | |
if echo "${SELECTED_TOOLS_ARR[@]}" | grep -qw "$tool"; then | |
if ! echo "$INSTALLED_TOOLS" | grep -qx "$tool"; then | |
echo "Installing $tool..." | |
dotnet tool install -g "$tool" | |
else | |
echo "$tool is already installed. Updating..." | |
dotnet tool update -g "$tool" | |
fi | |
else | |
if echo "$INSTALLED_TOOLS" | grep -qx "$tool"; then | |
echo "Uninstalling $tool..." | |
dotnet tool uninstall -g "$tool" | |
fi | |
fi | |
done | |
echo -e "\nAll requested dotnet tools have been installed, updated, or removed as selected!" | |
trap - EXIT | |
} | |
# Main execution | |
ensure_base_tools | |
ensure_sudo | |
if [[ "$1" == "versions" ]]; then | |
show_versions | |
fi | |
if [[ "$1" == "clean" ]]; then | |
REMOVE_OLD_MINOR_VERSIONS=true | |
clean_old_minor_versions | |
exit 0 | |
fi | |
if [[ "$1" == "update" ]]; then | |
# Unattended update: update all installed major.minor versions and dotnet tools | |
CLEAN=false | |
for arg in "$@"; do | |
if [[ "$arg" == "--clean" ]]; then | |
CLEAN=true | |
REMOVE_OLD_MINOR_VERSIONS=true | |
fi | |
done | |
if [ -d "$DOTNET_DIR/sdk" ]; then | |
mapfile -t INSTALLED_VERSIONS < <(ls "$DOTNET_DIR/sdk" | awk -F. '{print $1"."$2}' | sort -u) | |
if [ ${#INSTALLED_VERSIONS[@]} -eq 0 ]; then | |
echo "No .NET SDKs are currently installed. Exiting." | |
exit 1 | |
fi | |
VERSIONS=($(get_dotnet_versions)) | |
# Only update installed major.minor versions | |
TO_UPDATE=() | |
for v in "${INSTALLED_VERSIONS[@]}"; do | |
if printf '%s\n' "${VERSIONS[@]}" | grep -qx "$v"; then | |
TO_UPDATE+=("$v") | |
fi | |
done | |
if [ ${#TO_UPDATE[@]} -eq 0 ]; then | |
echo "No matching .NET SDK channels found for installed versions. Exiting." | |
exit 1 | |
fi | |
update_dotnet_sdks_for_versions "${TO_UPDATE[@]}" | |
echo -e "\nAll installed .NET SDKs have been updated successfully!" | |
if [ "$CLEAN" = true ]; then | |
clean_old_minor_versions | |
fi | |
else | |
echo "No .NET SDKs are currently installed. Exiting." | |
exit 1 | |
fi | |
# Update all installed dotnet tools | |
echo "Updating all globally installed dotnet tools..." | |
dotnet tool update --global --all || echo "Some tools may not have updated successfully." | |
echo -e "\nAll globally installed dotnet tools have been updated (where possible)!" | |
exit 0 | |
fi | |
ensure_dialog_installed | |
CHOICE=$(main_menu) | |
case "$CHOICE" in | |
1) | |
# Show a please wait dialog while retrieving available SDK versions | |
TMPWAIT=$(mktemp) | |
VERSIONS=() | |
( | |
VERSIONS=($(get_dotnet_versions)) | |
echo done > "$TMPWAIT" | |
) & | |
dialog --infobox "Please wait... Retrieving available .NET SDK versions." 7 50 | |
while [ ! -s "$TMPWAIT" ]; do sleep 0.1; done | |
rm -f "$TMPWAIT" | |
# Re-fetch VERSIONS in main shell after background subshell | |
VERSIONS=($(get_dotnet_versions)) | |
SELECTED_VERSIONS=("${VERSIONS[@]}") | |
select_versions | |
# Only prompt for cleanup if not running with --clean | |
if [[ "$*" != *--clean* ]]; then | |
configure_cleanup | |
fi | |
update_dotnet_sdks_for_versions "${SELECTED_VERSIONS[@]}" | |
echo -e "\nAll requested .NET SDKs have been installed or updated successfully!" | |
;; | |
2) | |
install_dependencies | |
download_install_script | |
update_path | |
manage_dotnet_tools | |
;; | |
*) | |
echo "No valid option selected. Exiting." | |
exit 1 | |
;; | |
esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment