#!/usr/bin/env bash# Install, uninstall or report the apt toolchains: C, Lua, Python and Ruby.# =========================================================================# Manages C/C++, Lua, Python, and Ruby development environments on Ubuntu/Debian.## Usage:# ./setup_dev_env.sh [--dry-run] {install|uninstall|status} {all|c|lua|python|ruby} [...]## Commands:# install - Install development environment(s) (default: all)# uninstall - Uninstall development environment(s) (default: all, protects base packages)# status - Show installation status (default command and target)## Targets:# all - All environments (C/C++, Lua, Python, Ruby)# c - C/C++ (GCC, clang, cmake, gdb, valgrind, autotools)# lua - Lua 5.4 (runtime and dev packages)# python - Python 3.12, 3.13, 3.14, 3.15 (system default protected)# ruby - Ruby (runtime, dev, gems)## Examples:# ./setup_dev_env.sh # Show status (all)# ./setup_dev_env.sh install c lua # Install C/C++ and Lua# ./setup_dev_env.sh uninstall python # Uninstall Python (protects system default)# ./setup_dev_env.sh --dry-run install all # Test without changes## Features:# - Dry-run mode for safe testing# - Auto-detects and protects system Python version# - Deactivates venvs before install/uninstall# - Installs pip via ensurepip for non-system Python versions# - Never removes base packages or PPAs## Author: Vikas Munshi <vikas.munshi@gmail.com># Copyright (c) 2024-2026. Licensed under the MIT License.# ============================================================================# Package Array Definitions# ============================================================================# Base packages - never removed (the uninstall path always filters the base target out)declare-abase_packages=("bash""git""gh""build-essential""neovim""openssl""shellcheck""software-properties-common""tmux")# shellcheck disable=SC2034 # used dynamically# The neovim package's executable is nvim, not neovim — check for the command that exists.declare-abase_commands=("bash""git""gh""make""nvim""openssl""shellcheck""tmux")# PPAs (deadsnakes/ppa for Python) - never removed# PPAs (deadsnakes/ppa for Python) - never removed# Notes:# base_packages: see the array above — never uninstalled# package_ppas: ppa:deadsnakes/ppadeclare-apackage_ppas=("ppa:deadsnakes/ppa")# Valid targets: dynamically populated as c, lua, ruby, pythondeclare-aall_targets=()# ============================================================================# C/C++ (build-essential in base_packages provides gcc, g++, make, libc-dev)# ============================================================================all_targets+=(c)# shellcheck disable=SC2034 # used dynamicallydeclare-ac_packages=("autoconf""automake""clang""clang-format""clang-tidy""cmake""gdb""libc++-dev""libc++abi-dev""libgmp-dev""libmpfr-dev""libssl-dev""libtool""lldb""pkg-config""valgrind")# shellcheck disable=SC2034 # used dynamicallydeclare-ac_commands=("autoconf""automake""clang""clang++""clang-format""clang-tidy""cmake""g++""gcc""gdb""libtool""lldb""pkg-config""valgrind")# shellcheck disable=SC2034 # used dynamicallydeclare-ac_versions=("C""C++")# ============================================================================# Lua# ============================================================================all_targets+=(lua)# shellcheck disable=SC2034 # used dynamicallydeclare-alua_packages=("lua5.4""liblua5.4-dev")# shellcheck disable=SC2034 # used dynamicallydeclare-alua_commands=("lua""luac")# ============================================================================# primesieve# ============================================================================all_targets+=(primesieve)# shellcheck disable=SC2034 # used dynamicallydeclare-aprimesieve_packages=("primesieve""libprimesieve-dev")# shellcheck disable=SC2034 # used dynamicallydeclare-aprimesieve_commands=("primesieve")# ============================================================================# Ruby# ============================================================================all_targets+=(ruby)# shellcheck disable=SC2034 # used dynamicallydeclare-aruby_packages=("ruby""ruby-dev""ruby-git")# shellcheck disable=SC2034 # used dynamicallydeclare-aruby_commands=("ruby""gem""irb""rake""ri")# ============================================================================# Python (system default auto-detected and protected)# ============================================================================all_targets+=(python)declare-apython_versions=("3.12""3.13""3.14""3.15")declare-apython_packages=()python_packages+=("python3-pyqt5")forversionin"${python_versions[@]}";dopython_packages+=("python${version}""python${version}-dev""python${version}-venv")donedeclare-apython_commands=("python3")forversionin"${python_versions[@]}";dopython_commands+=("python${version}")done# ============================================================================# Package Management Arrays# ============================================================================declare-apackages=()# Combined packages to install/uninstalldeclare-acommands=()# Combined commands to checkdeclare-ainstalled_packages=()# Currently installed packages# ============================================================================# Helper Functions# ============================================================================# Function: populate_packages# Description: Populates packages/commands arrays from targets (sorted, deduplicated)populate_packages(){packages=()commands=()fortargetin"$@";dolocal-npackages_ref="${target}_packages"local-ncommands_ref="${target}_commands"packages+=("${packages_ref[@]}")commands+=("${commands_ref[@]}")doneif[[${#packages[@]}-gt0]];thenmapfile-tpackages<<(printf'%s\n'"${packages[@]}"|sort-u)fiif[[${#tools[@]}-gt0]];thenmapfile-ttools<<(printf'%s\n'"${tools[@]}"|sort-u)fi}# Function: populate_installed# Description: Filters packages array to only installed packages (via dpkg)populate_installed(){installed_packages=()forpkgin"${packages[@]}";doifdpkg-l"$pkg"2>/dev/null|grep-q"^ii\s\+${pkg}";theninstalled_packages+=("$pkg")fidoneif[[${#installed_packages[@]}-gt0]];thenmapfile-tinstalled_packages<<(printf'%s\n'"${installed_packages[@]}"|sort-u)fi}# System default python versiondeclarepython_system_version# Function: find_python_system_version# Description: Deactivates venv (if active), detects system Python version, sets python_system_version globalfind_python_system_version(){if[[-n"$VIRTUAL_ENV"]];thenprintf"→ Deactivating Python virtual environment: %s\n""$VIRTUAL_ENV"localvenv_path="$VIRTUAL_ENV"ifdeclare-fdeactivate>/dev/null2>&1;thendeactivate2>/dev/null||truefiunsetVIRTUAL_ENVunsetVIRTUAL_ENV_PROMPTif[[-n"$venv_path"]];thenPATH="${PATH//${venv_path}\/bin:/}"PATH="${PATH%:}"exportPATHfiunsetPYTHONHOMEfipython_system_version="$(python3--version2>&1|grep-oP'Python \K3\.\d+')"}# Function: ensure_essentials# Description: Add PPAs and repos and install packages if missing (idempotent)ensure_essentials(){execute_with_dry_runsudoaptupdate>/dev/null2>&1forppain"${package_ppas[@]}";dolocalppa_name="${ppa#ppa:}"localppa_user="${ppa_name%%/*}"localppa_repo="${ppa_name#*/}"if!grep-rq"${ppa_user}.*${ppa_repo}"/etc/apt/sources.list.d/2>/dev/null&&\!grep-q"${ppa_user}.*${ppa_repo}"/etc/apt/sources.list2>/dev/null;thenexecute_with_dry_runsudoadd-apt-repository-y"$ppa"execute_with_dry_runsudoaptupdate>/dev/null2>&1fidoneif!grep-q"cli.github.com/packages"/etc/apt/sources.list.d/github-cli.list2>/dev/null;thenexecute_with_dry_runsudomkdir-p-m755/etc/apt/keyringslocalgh_keyring="/etc/apt/keyrings/githubcli-archive-keyring.gpg"if[[!-f"$gh_keyring"]];thenlocaltmp_keytmp_key=$(mktemp)execute_with_dry_runwget-nv-O"$tmp_key"https://cli.github.com/packages/githubcli-archive-keyring.gpgexecute_with_dry_runsudotee"$gh_keyring"<"$tmp_key">/dev/nullexecute_with_dry_runrm-f"$tmp_key"execute_with_dry_runsudochmodgo+r"$gh_keyring"fiexecute_with_dry_runsudomkdir-p-m755/etc/apt/sources.list.dprintf"deb [arch=%s signed-by=%s] https://cli.github.com/packages stable main\n"\"$(dpkg--print-architecture)""$gh_keyring"|\execute_with_dry_runsudotee/etc/apt/sources.list.d/github-cli.list>/dev/nullexecute_with_dry_runsudoaptupdate>/dev/null2>&1filocal-apackages_to_install=()forpkgin"${base_packages[@]}";doif!dpkg-l|grep-q"^ii\s\+${pkg}\s";thenpackages_to_install+=("$pkg")fidoneif[[${#packages_to_install[@]}-gt0]];thenexecute_with_dry_runsudoaptinstall-y"${packages_to_install[@]}"fi}# Function: python_ensurepip ($1=version)# Description: Installs pip via ensurepip if not already available (idempotent)python_ensurepip(){localversion="$1"if"python${version}"-mpip--version>/dev/null2>&1;thenreturn0fiexecute_with_dry_run"python${version}"-mensurepip}# Function: python_uninstall_user_site_packages ($1=version)# Description: Removes user site-packages and pip for non-system Python versionspython_uninstall_user_site_packages(){localversion="$1"if!command-v"python${version}">/dev/null2>&1;thenreturn0filocaluser_site_packagesuser_site_packages=$("python${version}"-msite--user-site2>/dev/null||echo"")if[[-n"$user_site_packages"]]&&[[-d"$user_site_packages"]];thenexecute_with_dry_runrm-rf"$user_site_packages"filocaluser_pip="${HOME}/.local/bin/pip${version}"if[[-f"$user_pip"]];thenexecute_with_dry_runrm-f"$user_pip"fi}# ============================================================================# Main Action Functions# ============================================================================# Function: install ($@=targets)# Description: Installs packages for targets (ensures base packages/PPAs, runs ensurepip for non-system Python)install(){ensure_essentialspopulate_packages"$@"populate_installedlocal-apackages_to_install=()forpkgin"${packages[@]}";doif!printf'%s\n'"${installed_packages[@]}"|grep-q"^${pkg}$";thenpackages_to_install+=("$pkg")fidoneif[[${#packages_to_install[@]}-eq0]];thenprintf"→ No packages to install\n"return0fiprintf"→ Installing packages: %s\n""${packages_to_install[*]}"execute_with_dry_runsudoaptinstall-y"${packages_to_install[@]}"ifprintf'%s\n'"$@"|grep-q"^python$";thenfind_python_system_versionforversionin"${python_versions[@]}";doif[["$version"!="$python_system_version"]];thenpython_ensurepip"$version"fidonefiprintf"\n→ Installation complete!\n"}# Function: uninstall ($@=targets)# Description: Uninstalls packages (protects base packages, system Python, cleans user site-packages)uninstall(){local-afiltered_targets=()fortargetin"$@";doif[["$target"!="base"]];thenfiltered_targets+=("$target")fidonepopulate_packages"${filtered_targets[@]}"populate_installedifprintf'%s\n'"$@"|grep-q"^python$";thenfind_python_system_versionlocal-afiltered_packages=()forpkgin"${installed_packages[@]}";doif[["$pkg"!="python${python_system_version}"]];thenfiltered_packages+=("$pkg")fidoneinstalled_packages=("${filtered_packages[@]}")forversionin"${python_versions[@]}";doif[["$version"!="$python_system_version"]];thenpython_uninstall_user_site_packages"$version"fidonefiif[[${#installed_packages[@]}-eq0]];thenprintf"→ No packages to uninstall\n"return0fiprintf"→ Uninstalling packages: %s\n""${installed_packages[*]}"execute_with_dry_runsudoaptpurge-y"${installed_packages[@]}"execute_with_dry_runsudoaptautoremove-yprintf"\n→ Uninstallation complete!\n"}# Function: status ($@=targets)# Description: Shows packages (dpkg versions) and commands (runtime versions) for targetsstatus(){populate_packages"$@"populate_installedprintf"Development Environment Status:\n"printf"================================\n"printf"\nPackages (apt-managed):\n"printf"=======================\n"forpkgin"${packages[@]}";doifprintf'%s\n'"${installed_packages[@]}"|grep-q"^${pkg}$";thenlocalversionversion=$(dpkg-query-W-f='${Version}'"$pkg"2>/dev/null||echo"unknown")printf" ✓ %-30s : %s\n""$pkg""$version"elseprintf" ✗ %-30s : not installed\n""$pkg"fidoneprintf"\nCommands (executables):\n"printf"=======================\n"forclicmdin"${commands[@]}";doifcommand-v"$clicmd"&>/dev/null;thenprintf" ✓ %-12s : ""$clicmd"# -V before -v: for tmux, -v is not "version" but "verbose logging" — it starts# a server and writes tmux-{client,server}-*.log into the current directory.# Everything here answers --version or -V; -v stays last for the likes of lua.# First *non-blank* line, not simply the first: shellcheck --version leads with# an empty one, which would print the name and then nothing at all.("$clicmd"--version2>/dev/null||"$clicmd"-V2>/dev/null||"$clicmd"-v2>/dev/null)\|grep-m1-v'^[[:space:]]*$'elseprintf" ✗ %-12s : not available\n""$clicmd"fidoneprintf"\n"}# ============================================================================# Usage and Help# ============================================================================# Function: usage# Description: Displays help text with commands, targets, and examplesusage(){printf"Usage: %s [OPTIONS] [COMMAND] [TARGETS...]\n""$0"printf" %s [COMMAND] (implies: all targets)\n""$0"printf" %s (implies: status all)\n""$0"printf"\n"printf"Manages development environments (C/C++, Lua, Python, Ruby) on Ubuntu/Debian.\n"printf"Automatically protects base packages, PPAs, and system Python version.\n"printf"\n"printf"OPTIONS:\n"printf" --help Display this help message and exit\n"printf" --dry-run Preview commands without executing (test mode)\n"printf"\n"printf"COMMANDS (default: status):\n"printf" install Install packages for specified target(s)\n"printf" uninstall Remove packages for specified target(s)\n"printf" status Show installation status for specified target(s)\n"printf"\n"printf"TARGETS (default: all):\n"printf" all All development environments\n"fortargetin"${all_targets[@]}";docase"$target"inc)printf" c C/C++ (gcc, g++, clang, cmake, gdb, valgrind, autotools)\n";;lua)printf" lua Lua 5.4 (runtime, compiler, dev libraries)\n";;primesieve)printf" primesieve primesieve and libprimesieve-dev C libraries\n";;ruby)printf" ruby Ruby (runtime, gems, dev libraries)\n";;python)local-nversions_ref="${target}_versions"localversions_strversions_str=$(printf"%s, ""${versions_ref[@]}"|sed's/, $//')printf" python Python %s (auto-detects and protects system default)\n""$versions_str";;*)printf" %-11s %s development environment\n""$target""${target^}";;esacdoneprintf"\n"printf"PROTECTED FROM REMOVAL:\n"localbase_packages_strbase_packages_str=$(printf"%s, ""${base_packages[@]}"|sed's/, $//')printf" • Base packages: %s\n""${base_packages_str}"localpackage_ppas_strpackage_ppas_str=$(printf"%s, ""${package_ppas[@]}"|sed's/, $//')printf" • Package repositories: %s\n""${package_ppas_str}"printf" • System default Python version (auto-detected)\n"printf"\n"printf"EXAMPLES:\n"printf" %s # Show status for all environments\n""$0"printf" %s install all # Install all environments\n""$0"printf" %s install c lua # Install C/C++ and Lua only\n""$0"printf" %s uninstall python # Uninstall Python (except system version)\n""$0"printf" %s status c # Show C/C++ installation status\n""$0"printf" %s --dry-run install all # Preview installation without changes\n""$0"printf"\n"printf"NOTES:\n"printf" • Multiple targets can be specified (e.g., 'install c lua python')\n"printf" • Duplicates are automatically removed\n"printf" • Options can appear anywhere in arguments\n"printf" • Virtual environments are automatically deactivated before operations\n"}# ============================================================================# Dry Run Support# ============================================================================declaredry_run=false# Function: execute_with_dry_run ($@=command)# Description: Logs and executes command (or skips if dry_run=true), indents output, returns exit codeexecute_with_dry_run(){localexit_code=0printf"→ %s\n""$*">&2if[["$dry_run"==false]];then"$@"2>&1|sed's/^/\t/'exit_code="${PIPESTATUS[0]}"if[[$exit_code-eq0]];thenprintf"✓ %s [exit: %d]\n""$*""$exit_code">&2elseprintf"✗ %s [exit: %d]\n""$*""$exit_code">&2fireturn"$exit_code"elseprintf" [DRY RUN - skipped]\n">&2return0fi}# ============================================================================# Main Entry Point# ============================================================================# Function: main ($@=arguments)# Description: Parses args (--dry-run, action, targets), validates, expands "all", dispatches to action functionmain(){if[[$EUID-eq0]];thenprintf"Error: This script should not be run as root (sudo will be used when needed)\n">&2exit1filocalaction=""local-atargets=()localvalid_args=trueforargin"$@";docase"$arg"in-h|--help)usagereturn0;;--dry-run)dry_run=trueprintf"═══════════════════════════════════════════════════════════════\n">&2printf" DRY RUN MODE ENABLED - No system changes will be made\n">&2printf"═══════════════════════════════════════════════════════════════\n">&2;;install|uninstall|status)if[[${#targets[@]}-gt0]];thenprintf"Error: Action '%s' cannot be specified after targets: %s\n""$arg""${targets[*]}"printf"\n"valid_args=falsefiif[[-n"$action"]];thenprintf"Error: Duplicate action '%s' specified (already have '%s')\n""$arg""$action"printf"\n"valid_args=falsefiaction="$arg";;all)[[-z"$action"||"$action"=="status"]]&&all_targets+=(base)targets=("${all_targets[@]}");;*)ifprintf'%s\n'"${all_targets[@]}"|grep-q"^${arg}$";thentargets+=("$arg")elseprintf"Error: Invalid argument '%s'\n""$arg"printf"\n"valid_args=falsefi;;esacdoneif[["$valid_args"==false]];thenusageexit1fiif[[-z"$action"]];thenaction="status"fiif[[${#targets[@]}-eq0]];then[["$action"=="status"]]&&all_targets+=(base)targets=("${all_targets[@]}")fimapfile-ttargets<<(printf'%s\n'"${targets[@]}"|sort-u)printf"→ Executing: %s %s\n""$action""${targets[*]}""$action""${targets[@]}"}# ============================================================================# Script Execution Guard# ============================================================================# Only run main if script is executed directly (not sourced)# This allows the script to be imported as a library without side effectsif[["${BASH_SOURCE[0]}"=="${0}"]];thenmain"$@"fi