#!/bin/bash # forge - A home-manager like script for Fedora # Provides dotfile management and package installation functionality set -euo pipefail # Configuration FORGE_DIR="${FORGE_DIR:-$HOME/.config/forge}" FORGE_TOML="$FORGE_DIR/forge.toml" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Logging functions log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } # Initialize command - creates forge.toml and .config directory init() { log_info "Initializing forge..." # Create directory mkdir -p "$FORGE_DIR" # Create initial forge.toml if it doesn't exist if [[ ! -f "$FORGE_TOML" ]]; then cat >"$FORGE_TOML" <<'EOF' # Forge Configuration File # User-level configuration similar to home-manager/nixos [forge] version = "1.0" last_switch = null [copr] # COPR repositories to enable # Just list COPR repository names - presence means enable, absence means don't enable # Examples: # copr.fedorainfracloud.org/username/repository # copr.fedorainfracloud.org/anotheruser/anotherrepo [packages] # List of packages to install using dnf # Just list package names - presence means install, absence means don't install # Examples: # git # vim # curl [dotfiles] # Dotfiles to manage with symlinks # Format: "target_path" = "source_path" # target_path: where the symlink should be created (relative to home directory) # source_path: where the actual file is stored (relative to forge directory) # Note: source_path automatically prefixed with "dotfiles/" if not present # Examples: # ".bashrc" = ".bashrc" # ".config/vimrc" = "vimrc" # ".config/alacritty/alacritty.yml" = "alacritty.yml" [options] # Additional options backup = true backup_dir = "backup" EOF log_success "Created forge.toml: $FORGE_TOML" fi log_success "Initialization complete!" log_info "Edit $FORGE_TOML to configure packages and dotfiles" log_info "Run 'forge switch' to apply your configuration" } # Parse TOML file (basic implementation) parse_toml() { local section="" while IFS= read -r line; do # Skip comments and empty lines [[ -z "$line" || "$line" == \#* ]] && continue # Check for section headers if [[ "$line" =~ ^\[(.+)\]$ ]]; then section="${BASH_REMATCH[1]}" continue fi # Parse key-value pairs for non-packages sections if [[ "$section" != "packages" && "$line" =~ ^([^=]+)=(.+)$ ]]; then local key="${BASH_REMATCH[1]}" local value="${BASH_REMATCH[2]}" # Trim leading/trailing whitespace from key and value key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # Remove quotes from quoted strings if [[ "$key" =~ ^\"(.*)\"$ ]]; then key="${BASH_REMATCH[1]}" fi if [[ "$value" =~ ^\"(.*)\"$ ]]; then value="${BASH_REMATCH[1]}" fi echo "$section:$key=$value" # Parse package names (just keys without values in packages section) elif [[ "$section" == "packages" && "$line" =~ ^([^=]+)$ ]]; then local package="${BASH_REMATCH[1]// /}" echo "$section:$package" # Parse COPR repository names (just keys without values in copr section) elif [[ "$section" == "copr" && "$line" =~ ^([^=]+)$ ]]; then local copr_repo="${BASH_REMATCH[1]// /}" echo "$section:$copr_repo" fi done <"$FORGE_TOML" } # Enable COPR repositories from forge.toml enable_copr_repos() { log_info "Enabling COPR repositories..." local copr_enabled=false while IFS='=' read -r entry; do local section="${entry%%:*}" local copr_repo="${entry#*:}" if [[ "$section" == "copr" && -n "$copr_repo" ]]; then copr_enabled=true log_info "Enabling COPR repository: $copr_repo" if sudo dnf copr enable -y "$copr_repo"; then log_success "Enabled COPR: $copr_repo" else log_error "Failed to enable COPR: $copr_repo" fi fi done < <(parse_toml) if [[ "$copr_enabled" == false ]]; then log_info "No COPR repositories to enable" fi } # Disable COPR repositories that are no longer in forge.toml disable_copr_repos() { log_info "Checking for COPR repositories to disable..." # Get currently enabled COPR repos local enabled_repos=$(sudo dnf copr list --enabled 2>/dev/null | grep -E "copr\.fedorainfracloud\.org" | awk '{print $1}' || true) # Get configured COPR repos from forge.toml local configured_repos="" while IFS='=' read -r entry; do local section="${entry%%:*}" local copr_repo="${entry#*:}" if [[ "$section" == "copr" && -n "$copr_repo" ]]; then configured_repos="$configured_repos $copr_repo" fi done < <(parse_toml) # Disable repos that are enabled but not configured for repo in $enabled_repos; do if [[ ! " $configured_repos " =~ " $repo " ]]; then log_info "Disabling COPR repository: $repo" if sudo dnf copr disable -y "$repo"; then log_success "Disabled COPR: $repo" else log_error "Failed to disable COPR: $repo" fi fi done } # Install packages from forge.toml install_packages() { log_info "Installing packages..." local packages_installed=false while IFS='=' read -r entry; do local section="${entry%%:*}" local package="${entry#*:}" if [[ "$section" == "packages" && -n "$package" ]]; then packages_installed=true log_info "Installing package: $package" if sudo dnf install -y "$package"; then log_success "Installed $package" else log_error "Failed to install $package" fi fi done < <(parse_toml) if [[ "$packages_installed" == false ]]; then log_info "No packages to install" fi } # Deploy dotfiles using symlinks deploy_dotfiles() { log_info "Deploying dotfiles..." local dotfiles_deployed=false local dotfiles_dir="$FORGE_DIR/dotfiles" while IFS='=' read -r entry; do local section="${entry%%:*}" local key_value="${entry#*:}" local target="${key_value%%=*}" local source="${key_value##*=}" if [[ "$section" == "dotfiles" ]]; then dotfiles_deployed=true # Normalize source path - if it doesn't start with dotfiles/, add it if [[ ! "$source" =~ ^dotfiles/ ]]; then source="dotfiles/$source" fi local target_path="$HOME/$target" local source_path="$FORGE_DIR/$source" # Verify source file exists if [[ ! -f "$source_path" && ! -d "$source_path" ]]; then log_error "Source file not found: $source_path" continue fi # Create target directory if it doesn't exist local target_dir=$(dirname "$target_path") mkdir -p "$target_dir" # Handle existing file/symlink if [[ -e "$target_path" || -L "$target_path" ]]; then # Check if it's already a correct symlink if [[ -L "$target_path" ]]; then local current_target=$(readlink "$target_path") if [[ "$current_target" == "$source_path" ]]; then log_info "Symlink already correct: $target_path" continue fi fi # Backup existing file if it exists and is not a symlink if [[ -e "$target_path" && ! -L "$target_path" ]]; then local backup_dir="$FORGE_DIR/backup" mkdir -p "$backup_dir" local backup_path="$backup_dir/$(basename "$target").$(date +%Y%m%d_%H%M%S).bak" cp -r "$target_path" "$backup_path" log_info "Backed up $target_path to $backup_path" fi # Remove existing file/symlink rm -rf "$target_path" fi # Create relative symlink for better portability local relative_source if [[ -d "$source_path" ]]; then relative_source=$(realpath --relative-to="$target_dir" "$source_path") else relative_source=$(realpath --relative-to="$target_dir" "$source_path") fi # Create symlink using relative path if ln -s "$relative_source" "$target_path"; then log_success "Created symlink: $target_path -> $relative_source" else log_error "Failed to create symlink: $target_path -> $relative_source" fi fi done < <(parse_toml) if [[ "$dotfiles_deployed" == false ]]; then log_info "No dotfiles to deploy" fi } # Switch command - apply configuration switch() { log_info "Starting forge switch..." # Check if forge.toml exists if [[ ! -f "$FORGE_TOML" ]]; then log_error "forge.toml not found. Run 'forge init' first." exit 1 fi # Enable COPR repositories enable_copr_repos # Disable COPR repositories that are no longer configured disable_copr_repos # Install packages install_packages # Deploy dotfiles deploy_dotfiles # Update last_switch timestamp if command -v sed >/dev/null 2>&1; then sed -i "s/last_switch = .*/last_switch = \"$(date -Iseconds)\"/" "$FORGE_TOML" fi log_success "Switch completed successfully!" } # Status command - show current configuration status() { log_info "Forge Status" echo " Config directory: $FORGE_DIR" echo " Config file: $FORGE_TOML" if [[ -f "$FORGE_TOML" ]]; then echo log_info "Configuration:" # Show last switch time local last_switch=$(grep "last_switch" "$FORGE_TOML" | cut -d'=' -f2 | tr -d ' "') if [[ -n "$last_switch" && "$last_switch" != "null" ]]; then echo " Last switch: $last_switch" else echo " Last switch: Never" fi echo log_info "COPR Repositories:" local copr_count=0 while IFS='=' read -r entry; do local section="${entry%%:*}" local copr_repo="${entry#*:}" if [[ "$section" == "copr" && -n "$copr_repo" ]]; then echo " $copr_repo" ((copr_count++)) fi done < <(parse_toml) if [[ $copr_count -eq 0 ]]; then echo " No COPR repositories configured" else echo " Total: $copr_count COPR repositories" fi echo log_info "Packages:" local package_count=0 while IFS='=' read -r entry; do local section="${entry%%:*}" local package="${entry#*:}" if [[ "$section" == "packages" && -n "$package" ]]; then echo " $package" ((package_count++)) fi done < <(parse_toml) if [[ $package_count -eq 0 ]]; then echo " No packages configured" else echo " Total: $package_count packages" fi echo log_info "Dotfiles:" local dotfile_count=0 while IFS='=' read -r entry; do local section="${entry%%:*}" local key_value="${entry#*:}" local target="${key_value%%=*}" local source="${key_value##*=}" if [[ "$section" == "dotfiles" ]]; then echo " $target -> $source" ((dotfile_count++)) fi done < <(parse_toml) if [[ $dotfile_count -eq 0 ]]; then echo " No dotfiles configured" else echo " Total: $dotfile_count dotfiles" fi else echo " No configuration file found" fi } # Show help show_help() { cat < COMMANDS: init Initialize configuration structure (creates forge.toml) switch Apply configuration (enable COPR, install packages, deploy dotfiles) status Show current configuration status help Show this help message FEATURES: - TOML-based configuration in forge.toml - COPR repository management (enable/disable automatically) - Package management via dnf - Dotfile management via symlinks - Automatic backup of existing files - User-level configuration like home-manager/nixos FILES: $FORGE_DIR/ Main configuration directory $FORGE_TOML Configuration file for COPR, packages and dotfiles EXAMPLES: forge init # Edit $FORGE_TOML to add COPR repos, packages and dotfiles forge switch forge status EOF } # Main script logic main() { case "${1:-}" in "init") init ;; "switch") switch ;; "status") status ;; "help" | "--help" | "-h") show_help ;; "") log_error "No command specified. Use 'help' for usage information." exit 1 ;; *) log_error "Unknown command: $1" show_help exit 1 ;; esac } # Run main function with all arguments main "$@"