From b60850ef14583e5e735e7a2ab4b7bb96ed7c8496 Mon Sep 17 00:00:00 2001 From: kdusek Date: Thu, 13 Nov 2025 18:02:21 +0100 Subject: [PATCH] Add comprehensive guide for creating new GitHub binary installers following the established patterns --- INSTALLER_CREATION_GUIDE.md | 621 ++++++++++++++++++++++++++++++++++++ 1 file changed, 621 insertions(+) create mode 100644 INSTALLER_CREATION_GUIDE.md diff --git a/INSTALLER_CREATION_GUIDE.md b/INSTALLER_CREATION_GUIDE.md new file mode 100644 index 0000000..472bf5e --- /dev/null +++ b/INSTALLER_CREATION_GUIDE.md @@ -0,0 +1,621 @@ +# 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 \ No newline at end of file