diff --git a/README.md b/README.md index 391a3db..4a082a0 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Command-line interface tools/scripts - [install_kubectl.sh](docs/install_kubectl.md) - [install_loki.sh](docs/install_loki.md) - [install_promtail.sh](docs/install_promtail.md) +- [install_python.sh](docs/install_python.md) - [install_s3_mount.sh](docs/install_s3_mount.md) - [install_ssm_agent.sh](docs/install_ssm_agent.md) - [install_ssm_plugin.sh](docs/install_ssm_plugin.md) @@ -41,6 +42,7 @@ Refer to the respective files for detailed usage instructions: - **[install_kubectl.sh](docs/install_kubectl.md)**: Install or uninstall `kubectl` on a Linux system. - **[install_loki.sh](docs/install_loki.md)**: Install or uninstall Grafana Loki log aggregation system. - **[install_promtail.sh](docs/install_promtail.md)**: Install or uninstall Grafana Promtail log shipper with flexible configuration options. +- **[install_python.sh](docs/install_python.md)**: Install or uninstall Python with version pinning support on Linux, macOS, and Windows. - **[install_s3_mount.sh](docs/install_s3_mount.md)**: Install or uninstall AWS Mountpoint for Amazon S3 to mount S3 buckets as local filesystems. - **[install_ssm_agent.sh](docs/install_ssm_agent.md)**: Install or uninstall the AWS Systems Manager (SSM) Agent on various Linux distributions, macOS, and Windows. - **[install_ssm_plugin.sh](docs/install_ssm_plugin.md)**: Install or uninstall the AWS SSM Session Manager Plugin for remote EC2 access. diff --git a/docs/install_python.md b/docs/install_python.md new file mode 100644 index 0000000..aee0bee --- /dev/null +++ b/docs/install_python.md @@ -0,0 +1,235 @@ +# install_python.sh + +This script installs or uninstalls Python on Linux, macOS, and Windows systems. It supports version pinning and automatically installs the latest version when no version is specified. + +## Usage + +```bash +./install_python.sh [install|uninstall] +``` + +- **install** (default): Installs Python (latest version if not specified). +- **uninstall**: Removes Python from the system. + +## Optional Environment Variables + +- **PYTHON_VERSION**: Pin specific version (e.g., `3.12` or `3.12.5`) or leave empty for latest + +## Example Usage + +To install the latest Python version: + +```bash +./install_python.sh install +``` + +To install a specific major.minor version: + +```bash +PYTHON_VERSION=3.12 ./install_python.sh install +``` + +To install a specific full version: + +```bash +PYTHON_VERSION=3.11.5 ./install_python.sh install +``` + +To uninstall Python: + +```bash +./install_python.sh uninstall +``` + +## Running Without Cloning + +```bash +bash <(curl -s https://raw.githubusercontent.com/jdevto/cli-tools/main/scripts/install_python.sh) install +``` + +## Verification + +After installation, check the Python version: + +```bash +python3 --version +``` + +Test Python functionality: + +```bash +python3 --help +``` + +## Supported Operating Systems + +- **Linux** (Ubuntu/Debian, RHEL/CentOS/Fedora) +- **macOS** (Intel and Apple Silicon) +- **Windows** (limited support - provides instructions) + +## Installation Methods by OS + +### Linux (Ubuntu/Debian) + +- Uses system package manager (`apt`/`apt-get`) +- For newer versions, automatically adds `deadsnakes` PPA (Ubuntu only) +- **Note**: Deadsnakes PPA is Ubuntu-only; Debian systems will need to use pyenv or build from source +- Installs `python3.x`, `python3.x-dev`, and `python3.x-venv` + +### Linux (RHEL/CentOS/Fedora) + +- Uses system package manager (`dnf`/`yum`) +- Attempts EPEL repository for older RHEL/CentOS systems +- Falls back to compilation instructions if version not available + +### macOS + +- Uses Homebrew to install `pyenv` +- Installs Python via `pyenv` for better version management +- Automatically detects shell type (zsh, bash, or fallback to `.profile`) +- Configures shell profiles with pyenv integration (only if not already configured) + +### Windows + +- Provides manual installation instructions +- Recommends using `pyenv-win` for version management + +## Features + +- Automatically detects platform and package manager +- Intelligently handles sudo (works as root or with sudo) +- Installs latest Python version when no version is specified +- Supports version pinning (major.minor or full version) +- Idempotent: skips installation if requested version is already installed +- Automatically installs required dependencies (`curl`) +- Handles multiple installation methods per OS +- Verifies installation after completion +- Validates Ubuntu-only requirements for deadsnakes PPA +- Smart shell detection for pyenv configuration on macOS + +## Error Handling + +- If Python with the requested version is already installed, the script will skip reinstallation. +- If an unsupported OS or architecture is detected, the script exits with an error message. +- The script checks for required dependencies before attempting installation. +- Missing dependencies are automatically installed. +- Validates that sudo is available (or runs as root) before attempting package installation. +- Checks if latest version detection succeeds before proceeding. +- Validates Ubuntu-only requirement for deadsnakes PPA on Debian-based systems. + +## Version Detection + +The script normalizes version numbers: + +- `3.12.5` → `3.12` (major.minor) +- `3.12` → `3.12` (unchanged) +- Latest version is fetched from python.org + +## Prerequisites + +The script automatically installs required dependencies: + +- **curl** - for fetching latest version information + +### OS-Specific Prerequisites + +- **macOS**: Requires Homebrew (installed automatically if needed for pyenv) +- **Linux**: Requires appropriate package manager (apt, dnf, yum) +- **Windows**: Manual installation recommended + +## Idempotency + +The script is idempotent and will: + +- Check if Python is already installed +- Compare installed version with requested version +- Skip installation if versions match +- Proceed with installation if version differs or Python is not installed + +## Troubleshooting + +### Common Issues + +1. **Python not found after installation**: + - Restart your shell or run `source ~/.bashrc` (or `source ~/.zshrc`) + - Verify PATH: `echo $PATH | grep python` + - Check installation: `which python3` + +2. **Installation fails on Ubuntu/Debian**: + - Ensure `software-properties-common` is installed + - Check if deadsnakes PPA was added: `ls /etc/apt/sources.list.d/ | grep deadsnakes` + - **Note**: Deadsnakes PPA is Ubuntu-only. On Debian, use pyenv or build from source + +3. **Installation fails on RHEL/CentOS**: + - Ensure EPEL repository is enabled: `sudo yum install epel-release` + - Some versions may require compilation from source + +4. **macOS installation issues**: + - Ensure Homebrew is installed: `brew --version` + - If pyenv fails, try: `brew install python@3.12` directly + +5. **Version not available**: + - System repositories may not have the latest Python versions + - Consider using `pyenv` for better version management + - Check available versions in your distribution's repositories + +### Logs and Debugging + +- **Check installed version**: `python3 --version` +- **Check available versions (Ubuntu)**: `apt-cache search python3 | grep '^python3'` +- **Check available versions (RHEL/Fedora)**: `dnf list available python3*` +- **Check pyenv versions (macOS)**: `pyenv versions` + +### Manual Installation Methods + +If the script fails, you can install Python manually: + +**Ubuntu/Debian**: + +```bash +sudo apt update +sudo apt install software-properties-common +sudo add-apt-repository ppa:deadsnakes/ppa +sudo apt update +sudo apt install python3.12 python3.12-dev python3.12-venv +``` + +**RHEL/CentOS/Fedora**: + +```bash +sudo dnf install python3.12 +# Or for older systems: +sudo yum install epel-release +sudo yum install python312 +``` + +**macOS**: + +```bash +brew install pyenv +pyenv install 3.12.5 +pyenv global 3.12.5 +``` + +## Additional Resources + +- **Official Python Downloads**: +- **pyenv (Linux/macOS)**: +- **pyenv-win (Windows)**: +- **deadsnakes PPA (Ubuntu)**: + +## Cleanup + +- Temporary installation files are automatically removed after execution. +- The script uses `trap cleanup EXIT` to ensure cleanup even if interrupted. + +## Notes + +- System Python (usually `python3`) may be a system dependency and should not be removed +- The script installs additional Python versions alongside system Python +- On macOS, the script uses `pyenv` for better version management +- Windows support is limited; manual installation is recommended +- The script intelligently handles sudo - works when running as root or with sudo privileges +- **Uninstall warning**: Removing Python packages can affect system tools on non-container systems +- Deadsnakes PPA is Ubuntu-only; Debian users should use pyenv or build from source +- Shell integration for pyenv is only added if not already present (prevents duplicates) diff --git a/scripts/install_python.sh b/scripts/install_python.sh new file mode 100755 index 0000000..d72ff06 --- /dev/null +++ b/scripts/install_python.sh @@ -0,0 +1,399 @@ +#!/bin/bash + +set -e + +PYTHON_VERSION="${PYTHON_VERSION:-}" # Optional: pin version (e.g., 3.12), otherwise uses latest +TMP_DIR="/tmp/python-install" + +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +# Decide how to run privileged commands +SUDO="sudo" +if [ "$(id -u)" -eq 0 ]; then + SUDO="" +elif ! command -v sudo >/dev/null 2>&1; then + echo "sudo not found and you are not root; cannot install packages." + exit 1 +fi + +PACKAGE_MANAGER="" +PLATFORM="" + +normalize_version() { + local version="$1" + # Convert 3.12.5 to 3.12, or keep 3.12 as is + echo "$version" | cut -d. -f1,2 +} + +get_installed_python_version() { + local python_cmd="${1:-python3}" + if command -v "$python_cmd" >/dev/null 2>&1; then + # python3 --version -> "Python 3.12.3", extract 3.12 + "$python_cmd" --version 2>&1 | awk '{print $2}' | cut -d. -f1,2 | head -1 + else + echo "none" + fi +} + +get_latest_python_version() { + # Get latest stable Python version (major.minor) from python.org + curl -s "https://www.python.org/ftp/python/" \ + | grep -E 'href="[0-9]+\.[0-9]+\.[0-9]+/' \ + | sed -E 's/.*href="([0-9]+\.[0-9]+\.[0-9]+)\/".*/\1/' \ + | sort -V \ + | tail -1 \ + | cut -d. -f1,2 +} + +detect_package_manager() { + if command -v dnf >/dev/null 2>&1; then + PACKAGE_MANAGER="dnf" + elif command -v yum >/dev/null 2>&1; then + PACKAGE_MANAGER="yum" + elif command -v apt >/dev/null 2>&1 || command -v apt-get >/dev/null 2>&1; then + if command -v apt >/dev/null 2>&1; then + PACKAGE_MANAGER="apt" + else + PACKAGE_MANAGER="apt-get" + fi + elif command -v brew >/dev/null 2>&1; then + PACKAGE_MANAGER="brew" + else + echo "Unsupported package manager. Exiting." + exit 1 + fi +} + +detect_platform() { + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + PLATFORM="linux" + elif [[ "$OSTYPE" == "darwin"* ]]; then + PLATFORM="darwin" + elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then + PLATFORM="windows" + else + echo "Unsupported platform: $OSTYPE" + exit 1 + fi +} + +install_dependencies() { + detect_package_manager + echo "Detected package manager: $PACKAGE_MANAGER" + + local missing_packages=() + + if ! command -v curl >/dev/null 2>&1; then + missing_packages+=("curl") + fi + + if [ ${#missing_packages[@]} -eq 0 ]; then + echo "All dependencies are already installed." + return 0 + fi + + echo "Installing missing packages: ${missing_packages[*]}" + + case "$PACKAGE_MANAGER" in + apt|apt-get) + $SUDO $PACKAGE_MANAGER update + $SUDO $PACKAGE_MANAGER install -y "${missing_packages[@]}" + ;; + dnf) + $SUDO dnf install -y "${missing_packages[@]}" + ;; + yum) + $SUDO yum install -y "${missing_packages[@]}" + ;; + brew) + brew install "${missing_packages[@]}" + ;; + *) + echo "Unsupported package manager. Exiting." + exit 1 + ;; + esac +} + +install_python_linux_apt() { + local version="$1" + local major_minor + major_minor=$(normalize_version "$version") + + echo "Installing Python ${version} on Debian/Ubuntu..." + + local system_version + system_version=$(get_installed_python_version "python3") + if [ "$system_version" != "none" ]; then + local system_major_minor + system_major_minor=$(normalize_version "$system_version") + # If system Python is same or newer, use system repos + if [ "$(printf '%s\n' "$system_major_minor" "$major_minor" | sort -V | head -1)" = "$major_minor" ]; then + echo "Installing Python ${major_minor} from system repositories..." + $SUDO $PACKAGE_MANAGER update + $SUDO $PACKAGE_MANAGER install -y "python${major_minor}" "python${major_minor}-dev" "python${major_minor}-venv" + return 0 + fi + fi + + # For newer versions, use deadsnakes on Ubuntu only + if [ -f /etc/os-release ]; then + . /etc/os-release + if [[ "$ID" != "ubuntu" && "$ID_LIKE" != *"ubuntu"* ]]; then + echo "Deadsnakes PPA is Ubuntu only; cannot install Python ${major_minor} this way." + echo "Consider using pyenv or building from source." + exit 1 + fi + fi + + echo "Adding deadsnakes PPA for Python ${major_minor}..." + $SUDO $PACKAGE_MANAGER update + $SUDO $PACKAGE_MANAGER install -y software-properties-common + $SUDO add-apt-repository -y ppa:deadsnakes/ppa + $SUDO $PACKAGE_MANAGER update + $SUDO $PACKAGE_MANAGER install -y "python${major_minor}" "python${major_minor}-dev" "python${major_minor}-venv" +} + +install_python_linux_dnf() { + local version="$1" + local major_minor + major_minor=$(normalize_version "$version") + + echo "Installing Python ${version} on RHEL/CentOS/Fedora..." + + # Try system repositories first + if $SUDO $PACKAGE_MANAGER install -y "python${major_minor}" 2>/dev/null; then + echo "Python ${major_minor} installed from system repositories." + return 0 + fi + + # For older RHEL/CentOS, try EPEL if using yum + if [ "$PACKAGE_MANAGER" = "yum" ]; then + echo "Installing EPEL repository..." + $SUDO yum install -y epel-release + if $SUDO yum install -y "python${major_minor}" 2>/dev/null; then + echo "Python ${major_minor} installed from EPEL." + return 0 + fi + fi + + echo "Warning: Python ${major_minor} not available in repositories." + echo "Consider using pyenv for version management: https://github.com/pyenv/pyenv" + exit 1 +} + +install_python_linux() { + detect_package_manager + + if [ "$PACKAGE_MANAGER" = "apt" ] || [ "$PACKAGE_MANAGER" = "apt-get" ]; then + install_python_linux_apt "$1" + elif [ "$PACKAGE_MANAGER" = "dnf" ] || [ "$PACKAGE_MANAGER" = "yum" ]; then + install_python_linux_dnf "$1" + else + echo "Unsupported package manager for Linux: $PACKAGE_MANAGER" + exit 1 + fi +} + +configure_pyenv_shell_integration() { + local shell_rc + + if [ -n "$ZSH_VERSION" ]; then + shell_rc="${HOME}/.zshrc" + elif [ -n "$BASH_VERSION" ]; then + shell_rc="${HOME}/.bash_profile" + else + shell_rc="${HOME}/.profile" + fi + + if ! grep -q 'PYENV_ROOT="$HOME/.pyenv"' "$shell_rc" 2>/dev/null; then + { + echo 'export PYENV_ROOT="$HOME/.pyenv"' + echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' + echo 'eval "$(pyenv init -)"' + } >> "$shell_rc" + fi +} + +install_python_darwin() { + local version="$1" + local major_minor + major_minor=$(normalize_version "$version") + + echo "Installing Python ${version} on macOS..." + + if ! command -v brew >/dev/null 2>&1; then + echo "Error: Homebrew is required for Python installation on macOS." + echo "Install Homebrew: https://brew.sh" + exit 1 + fi + + # Use pyenv via Homebrew for better version management + if ! command -v pyenv >/dev/null 2>&1; then + echo "Installing pyenv via Homebrew..." + brew install pyenv + fi + + configure_pyenv_shell_integration + + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init -)" + + echo "Installing Python ${major_minor} via pyenv..." + pyenv install -s "${major_minor}" + pyenv global "${major_minor}" 2>/dev/null || true +} + +install_python_windows() { + echo "Windows Python installation is not fully automated." + echo "Please install Python manually from: https://www.python.org/downloads/" + echo "" + echo "Or use pyenv-win: https://github.com/pyenv-win/pyenv-win" + exit 1 +} + +install_python() { + detect_platform + install_dependencies + + # Determine version to install + if [ -z "$PYTHON_VERSION" ]; then + PYTHON_VERSION=$(get_latest_python_version) + if [ -z "$PYTHON_VERSION" ]; then + echo "Failed to detect latest Python version from python.org" >&2 + exit 1 + fi + echo "Latest Python version (major.minor): ${PYTHON_VERSION}" + else + echo "Using specified Python version: ${PYTHON_VERSION}" + fi + + local major_minor + major_minor=$(normalize_version "$PYTHON_VERSION") + local installed_version + installed_version=$(get_installed_python_version "python3") + + # Check if requested version is already installed + if [ "$installed_version" != "none" ]; then + local installed_major_minor + installed_major_minor=$(normalize_version "$installed_version") + if [ "$installed_major_minor" = "$major_minor" ]; then + echo "Python ${major_minor} is already installed (version ${installed_version})." + echo "Current version: $(python3 --version 2>&1)" + exit 0 + else + echo "Python ${installed_major_minor} is installed, but ${major_minor} is requested." + fi + fi + + echo "Installing Python ${PYTHON_VERSION}..." + + case "$PLATFORM" in + linux) + install_python_linux "$PYTHON_VERSION" + ;; + darwin) + install_python_darwin "$PYTHON_VERSION" + ;; + windows) + install_python_windows + ;; + *) + echo "Unsupported platform: $PLATFORM" + exit 1 + ;; + esac + + # Verify installation + local verify_cmd="python3" + if [ "$PLATFORM" = "darwin" ] && command -v pyenv >/dev/null 2>&1; then + verify_cmd="python" + fi + + if command -v "$verify_cmd" >/dev/null 2>&1; then + echo "Python installed successfully: $($verify_cmd --version 2>&1)" + else + echo "Warning: Python installation completed but binary not found in PATH" + echo "You may need to restart your shell or add Python to PATH manually" + fi +} + +uninstall_python() { + detect_platform + detect_package_manager + + local installed_version + installed_version=$(get_installed_python_version "python3") + + if [ "$installed_version" = "none" ]; then + echo "Python is not installed. Skipping uninstallation." + exit 0 + fi + + echo "Uninstalling Python ${installed_version}..." + + case "$PLATFORM" in + linux) + local major_minor + major_minor=$(normalize_version "$installed_version") + echo "Warning: removing Python packages can affect system tools on non-container systems." + case "$PACKAGE_MANAGER" in + apt|apt-get) + $SUDO $PACKAGE_MANAGER remove -y "python${major_minor}" "python${major_minor}-dev" "python${major_minor}-venv" 2>/dev/null || true + ;; + dnf|yum) + $SUDO $PACKAGE_MANAGER remove -y "python${major_minor}" 2>/dev/null || true + ;; + esac + ;; + darwin) + local major_minor + major_minor=$(normalize_version "$installed_version") + if command -v pyenv >/dev/null 2>&1; then + echo "Note: Python may be installed via pyenv. Use 'pyenv uninstall ${installed_version}' to remove." + else + echo "Note: Python may be installed via Homebrew. Use 'brew uninstall python@${major_minor}' to remove." + fi + ;; + windows) + echo "Please uninstall Python manually from Windows Settings or Control Panel." + ;; + esac + + echo "Python uninstallation completed." + echo "Note: system Python (python3) may still be available if it is a system dependency." +} + +usage() { + echo "Usage: $0 [install|uninstall]" + echo "" + echo "Optional environment variables:" + echo " PYTHON_VERSION - Pin specific version (e.g., 3.12 or 3.12.5) or leave empty for latest" + echo "" + echo "Examples:" + echo " $0 install # Install latest Python" + echo " PYTHON_VERSION=3.12 $0 install # Install Python 3.12" + echo " PYTHON_VERSION=3.11.5 $0 install # Install Python 3.11.5" + echo " $0 uninstall # Uninstall Python" + exit 1 +} + +if [ "$#" -eq 0 ]; then + install_python +elif [ "$1" = "install" ]; then + install_python +elif [ "$1" = "uninstall" ]; then + uninstall_python +else + usage +fi + +if [ "${1:-install}" != "uninstall" ]; then + echo "" + echo "Python installation completed (or already satisfied)." + echo "Run 'python3 --version' to verify installation." +fi