# GitHub Binary Installer Template This document provides instructions for creating a new installer script for binaries hosted on GitHub releases, following the patterns established in the git-credential-manager, borg-cli, and tea-cli installers. ## Overview The installer should: - Prefer system-wide installation (/usr/local/bin) for all users - Fall back to user-specific installation ($HOME/bin) if no sudo access - Handle version management with symlinks - Warn about and offer to remove conflicting user bin installations - Support command-line options for version specification and reinstallation - Always use GitHub releases for the latest binaries (avoid package managers) ## Script Structure ### 1. Header and Configuration ```bash #!/bin/bash # [Tool Name] Installer # Downloads and installs the latest [Tool Name] binary # Supports multiple platforms and installation methods set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration GITHUB_REPO="[owner]/[repo]" # e.g., "git-ecosystem/git-credential-manager" USER_BIN_DIR="$HOME/bin" SYSTEM_INSTALL_DIR="/usr/local/bin" TEMP_DIR="/tmp/[tool]-install" ``` ### 2. Core Utility Functions ```bash # Function to print colored output print_status() { echo -e "${BLUE}[INFO]${NC} $1" } print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } print_error() { echo -e "${RED}[ERROR]${NC} $1" } # Function to check if command exists command_exists() { command -v "$1" >/dev/null 2>&1 } ``` ### 3. System Detection Functions ```bash # Function to detect OS and package manager detect_os() { if [[ -f /etc/os-release ]]; then . /etc/os-release OS="$ID" OS_VERSION="$VERSION_ID" else print_error "Cannot detect operating system" exit 1 fi # Detect package manager if command_exists apt; then PKG_MANAGER="apt" elif command_exists yum; then PKG_MANAGER="yum" elif command_exists dnf; then PKG_MANAGER="dnf" elif command_exists pacman; then PKG_MANAGER="pacman" elif command_exists zypper; then PKG_MANAGER="zypper" else PKG_MANAGER="unknown" fi print_status "Detected OS: $OS $OS_VERSION" print_status "Package manager: $PKG_MANAGER" } # Function to get system architecture get_arch() { ARCH=$(uname -m) case $ARCH in x86_64) echo "amd64" ;; aarch64|arm64) echo "arm64" ;; armv7l) echo "arm" ;; *) print_error "Unsupported architecture: $ARCH" exit 1 ;; esac } ``` ### 4. Version Management Functions ```bash # Function to get current installed version and location get_current_version() { if command_exists [tool]; then CURRENT_VERSION=$([tool] --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -n 1) if [[ -z "$CURRENT_VERSION" ]]; then CURRENT_VERSION="unknown" fi # Determine installation location [TOOL]_PATH=$(which [tool]) if [[ "$[TOOL]_PATH" == "$USER_BIN_DIR"* ]]; then INSTALL_TYPE="user" elif [[ "$[TOOL]_PATH" == "/usr/local/bin"* ]] || [[ "$[TOOL]_PATH" == "/usr/bin"* ]]; then INSTALL_TYPE="system" else INSTALL_TYPE="other" fi else CURRENT_VERSION="not_installed" INSTALL_TYPE="none" fi echo "$CURRENT_VERSION:$INSTALL_TYPE" } # Function to compare versions compare_versions() { local version1="$1" local version2="$2" # Remove 'v' prefix if present version1=${version1#v} version2=${version2#v} # Split versions into arrays IFS='.' read -ra V1 <<< "$version1" IFS='.' read -ra V2 <<< "$version2" # Compare major, minor, patch for i in {0..2}; do local v1=${V1[$i]:-0} local v2=${V2[$i]:-0} if (( v1 > v2 )); then return 0 # version1 > version2 elif (( v1 < v2 )); then return 1 # version1 < version2 fi done return 0 # versions are equal } # Function to get latest release info get_latest_release() { print_status "Fetching latest [Tool Name] release information..." if command_exists curl; then RELEASE_INFO=$(curl -s "https://api.github.com/repos/$GITHUB_REPO/releases/latest") elif command_exists wget; then RELEASE_INFO=$(wget -qO- "https://api.github.com/repos/$GITHUB_REPO/releases/latest") else print_error "curl or wget is required" exit 1 fi if [[ -z "$RELEASE_INFO" ]]; then print_error "Failed to fetch release information" exit 1 fi # Extract version and download URLs LATEST_VERSION=$(echo "$RELEASE_INFO" | grep -o '"tag_name": *"[^"]*"' | sed -E 's/.*"([^"]+)".*/\1/') if [[ -z "$LATEST_VERSION" ]]; then print_error "Failed to parse release information" exit 1 fi print_success "Latest version: $LATEST_VERSION" } # Function to check if update is needed check_update_needed() { local latest_version="$1" local current_info=$(get_current_version) local current_version=$(echo "$current_info" | cut -d: -f1) local install_type=$(echo "$current_info" | cut -d: -f2) print_status "Current version: $current_version ($install_type)" print_status "Latest version: $latest_version" if [[ "$current_version" == "not_installed" ]]; then print_status "[Tool Name] is not installed" return 0 # Need to install fi if [[ "$current_version" == "unknown" ]]; then print_warning "Cannot determine current version, proceeding with update" return 0 # Assume update needed fi # Clean version strings for comparison local clean_current="${current_version#v}" local clean_latest="${latest_version#v}" if compare_versions "$clean_latest" "$clean_current"; then if [[ "$clean_latest" == "$clean_current" ]]; then print_success "You already have the latest version ($current_version) installed at $install_type location" if [[ "$force_reinstall" == "true" ]]; then print_status "Force reinstall requested, proceeding..." return 0 else return 1 # No update needed fi else print_status "Newer version available: $latest_version > $current_version" return 0 # Update needed fi else print_success "Your version ($current_version) is newer than latest release ($latest_version)" return 1 # No update needed fi } ``` ### 5. Installation Strategy Functions ```bash # Function to check if user bin directory exists and is in PATH check_user_bin_setup() { if [[ ! -d "$USER_BIN_DIR" ]]; then print_status "User bin directory doesn't exist" return 1 fi # Check if user bin is in PATH if [[ ":$PATH:" != *":$USER_BIN_DIR:"* ]]; then print_warning "User bin directory ($USER_BIN_DIR) is not in PATH" print_status "Add to PATH: export PATH=\"$USER_BIN_DIR:\$PATH\"" return 1 fi return 0 } # Function to check for version-specific symlink check_version_symlink() { local [tool]_path=$(which [tool] 2>/dev/null) if [[ -z "$[tool]_path" ]]; then return 1 fi # Check if it's a symlink if [[ -L "$[tool]_path" ]]; then SYMLINK_TARGET=$(readlink "$[tool]_path") # Extract version from symlink target if [[ "$SYMLINK_TARGET" =~ [tool]-([0-9]+\.[0-9]+\.[0-9]+) ]]; then CURRENT_SYMLINK_VERSION="${BASH_REMATCH[1]}" return 0 fi fi return 1 } # Function to determine installation strategy determine_install_strategy() { local current_info=$(get_current_version) local current_version=$(echo "$current_info" | cut -d: -f1) local install_type=$(echo "$current_info" | cut -d: -f2) # Strategy: Prefer system installation for all users if [[ $EUID -eq 0 ]] || sudo -n true 2>/dev/null; then print_status "Installing system-wide for all users" INSTALL_STRATEGY="system_install" TARGET_DIR="$SYSTEM_INSTALL_DIR" # Warn if user bin has [tool] if [[ -f "$USER_BIN_DIR/[tool]" ]]; then print_warning "[Tool Name] found in user bin directory ($USER_BIN_DIR)" print_status "This might take precedence if $USER_BIN_DIR is in PATH before system paths" fi else print_status "No sudo access, checking user bin directory" if check_user_bin_setup; then print_status "User bin directory is available and in PATH" # Check if [tool] exists in user bin if [[ -f "$USER_BIN_DIR/[tool]" ]]; then print_status "[Tool Name] found in user bin directory" # Check for version symlink if check_version_symlink; then print_status "Version symlink found: $CURRENT_SYMLINK_VERSION" INSTALL_STRATEGY="user_update" TARGET_DIR="$USER_BIN_DIR" else print_status "No version symlink found, will create one" INSTALL_STRATEGY="user_upgrade" TARGET_DIR="$USER_BIN_DIR" fi else print_status "[Tool Name] not found in user bin, will install there" INSTALL_STRATEGY="user_install" TARGET_DIR="$USER_BIN_DIR" fi else print_error "Cannot install system-wide (no sudo) and user bin not available" exit 1 fi fi print_status "Installation strategy: $INSTALL_STRATEGY" print_status "Target directory: $TARGET_DIR" } ``` ### 6. Download and Installation Functions ```bash # Function to download file download_file() { local url="$1" local filename="$2" print_status "Downloading $filename..." if command_exists curl; then curl -L -o "$TEMP_DIR/$filename" "$url" elif command_exists wget; then wget -O "$TEMP_DIR/$filename" "$url" else print_error "curl or wget is required" exit 1 fi if [[ ! -f "$TEMP_DIR/$filename" ]]; then print_error "Failed to download $filename" exit 1 fi print_success "Downloaded $filename" } # Function to install [Tool Name] from binary install_[tool]_from_binary() { local arch="$1" local version="$2" # Determine binary name pattern from GitHub releases local binary_name="[tool]-${version#v}-linux-${arch}" local download_url="https://github.com/$GITHUB_REPO/releases/download/$version/$binary_name" download_file "$download_url" "$binary_name" # Determine installation command based on target directory local install_cmd="install -m 755" local symlink_cmd="ln -sf" if [[ "$TARGET_DIR" == "$SYSTEM_INSTALL_DIR" ]]; then install_cmd="sudo $install_cmd" symlink_cmd="sudo $symlink_cmd" fi print_status "Installing [Tool Name] binary to $TARGET_DIR..." # Install the binary with version suffix $install_cmd "$TEMP_DIR/$binary_name" "$TARGET_DIR/[tool]-$version" # Verify installation if [[ ! -f "$TARGET_DIR/[tool]-$version" ]]; then print_error "Failed to install [Tool Name] binary" return 1 fi # Remove any existing [tool] symlink or file to avoid conflicts rm -f "$TARGET_DIR/[tool]" # Create/update symlink to point to this version $symlink_cmd "$TARGET_DIR/[tool]-$version" "$TARGET_DIR/[tool]" print_success "[Tool Name] binary installed successfully" print_status "Version symlink: [tool] -> [tool]-$version" } # Function to verify [Tool Name] installation verify_[tool]_installation() { print_status "Verifying [Tool Name] installation..." if command_exists [tool]; then local version=$([tool] --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -n 1) print_success "[Tool Name] installed: $version" # Check for user bin versions and offer removal if [[ -d "$USER_BIN_DIR" ]] && [[ "$TARGET_DIR" == "$SYSTEM_INSTALL_DIR" ]]; then local user_versions=() for file in "$USER_BIN_DIR"/[tool]*; do if [[ -f "$file" ]]; then user_versions+=("$file") fi done if [[ ${#user_versions[@]} -gt 0 ]]; then print_warning "Found [tool] binaries in user bin directory:" for file in "${user_versions[@]}"; do echo " $file" done echo read -p "Would you like to remove these user bin versions? (y/N): " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then for file in "${user_versions[@]}"; do rm -f "$file" print_status "Removed $file" done print_success "Removed all user bin versions" fi fi fi return 0 else print_error "[Tool Name] installation verification failed" return 1 fi } ``` ### 7. Utility Functions ```bash # Function to cleanup cleanup() { if [[ -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" print_status "Cleaned up temporary files" fi } # Function to show usage show_usage() { echo "[Tool Name] Installer" echo echo "Usage: $0 [OPTIONS]" echo echo "Options:" echo " -h, --help Show this help message" echo " -v, --version Install specific version (default: latest)" echo " -f, --force Force reinstall even if up-to-date" echo " -r, --reinstall Reinstall the current installed version" echo echo "Examples:" echo " $0 # Install latest version using best method" echo " $0 -v v1.0.0 # Install specific version" echo " $0 --reinstall # Reinstall current version" } ``` ### 8. Main Function ```bash # Main installation function main() { local target_version="" local force_reinstall=false local reinstall_current=false # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in -h|--help) show_usage exit 0 ;; -v|--version) target_version="$2" shift 2 ;; -f|--force) force_reinstall=true shift ;; -r|--reinstall) reinstall_current=true shift ;; *) print_error "Unknown option: $1" show_usage exit 1 ;; esac done echo "========================================" echo "[Tool Name] Installer" echo "========================================" echo # Check if running as root for system installation if [[ $EUID -ne 0 ]]; then print_warning "Not running as root. Some operations may require sudo." fi # Detect system detect_os ARCH=$(get_arch) # Handle reinstall current version if [[ "$reinstall_current" == "true" ]]; then local current_info=$(get_current_version) local current_version=$(echo "$current_info" | cut -d: -f1) if [[ "$current_version" == "not_installed" ]]; then print_error "No current version installed to reinstall" exit 1 fi target_version="$current_version" force_reinstall=true print_status "Reinstalling current version: $target_version" fi # Get latest version if not specified if [[ -z "$target_version" ]]; then get_latest_release target_version="$LATEST_VERSION" else print_status "Using specified version: $target_version" fi # Determine installation strategy determine_install_strategy # Check if update is needed if ! check_update_needed "$target_version"; then print_success "No installation needed. Exiting." exit 0 fi # Create temporary directory mkdir -p "$TEMP_DIR" # Trap cleanup on exit trap cleanup EXIT # Install from binary install_[tool]_from_binary "$ARCH" "$target_version" verify_[tool]_installation # Installation completed successfully echo print_success "Installation completed successfully!" echo echo "Installation details:" echo "- Strategy: $INSTALL_STRATEGY" echo "- Location: $TARGET_DIR" if [[ "$INSTALL_STRATEGY" == user* ]]; then echo "- Binary: [tool] (symlink to [tool]-$target_version)" echo "- Version: $target_version" fi echo echo "Next steps:" echo "1. Test installation: [tool] --version" echo "2. [Add tool-specific usage instructions]" echo if [[ "$INSTALL_STRATEGY" == user* ]]; then echo "User-specific installation notes:" echo "- Binary installed in: $USER_BIN_DIR" echo "- Ensure $USER_BIN_DIR is in your PATH" echo "- You can have multiple versions side-by-side" fi echo echo "For more information, visit: https://github.com/$GITHUB_REPO" } # Run main function main "$@" ``` ## Implementation Notes 1. **Replace Placeholders**: Replace `[tool]`, `[Tool Name]`, `[TOOL]`, `[owner]/[repo]` with actual values. 2. **Binary Naming Convention**: Adjust the `binary_name` in `install_[tool]_from_binary` to match the actual GitHub release asset names. 3. **Version Detection**: Modify the version extraction in `get_current_version` and `verify_[tool]_installation` to match the tool's `--version` output format. 4. **Package Manager**: Since we disable package manager installation, the script will always use the binary method. 5. **Testing**: Test the script on different systems and with different scenarios (system vs user installation, reinstall, etc.). 6. **Error Handling**: Ensure all error conditions are properly handled with appropriate exit codes. 7. **Documentation**: Update the usage examples and next steps section with tool-specific information. This template ensures consistency across all installer scripts and provides a robust installation experience for users. INSTALLER_CREATION_GUIDE.md