Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions bin/install/homebrew.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env bash

# Homebrew Installation Script
# Installs and configures Homebrew package manager for macOS

set -euo pipefail

# Source utility functions
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
dotfiles_root="$(cd "$script_dir/../.." && pwd)"

# shellcheck source=../../lib/homebrew-utils.bash
source "$dotfiles_root/lib/homebrew-utils.bash"

# Main installation function
install_homebrew() {
echo "🍺 Setting up Homebrew..."

# Check if Homebrew is already installed
if detect_homebrew; then
# Ensure it's properly configured
ensure_homebrew_in_path

# Validate the installation
if validate_homebrew_installation; then
echo "βœ… Homebrew is already installed and functional"
return 0
else
echo "⚠️ Homebrew found but not functional, attempting to fix..."
fi
else
echo "πŸ“¦ Installing Homebrew..."

# Install Homebrew using official installation script
if ! /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"; then
echo "❌ Failed to install Homebrew" >&2
return 1
fi

echo "βœ… Homebrew installation completed"
fi

# Ensure Homebrew is in PATH after installation
ensure_homebrew_in_path

# Final validation
if validate_homebrew_installation; then
echo "βœ… Homebrew installation and configuration successful"

# Display version information
echo "πŸ” Homebrew information:"
get_homebrew_version

return 0
else
echo "❌ Homebrew installation failed validation" >&2
return 1
fi
}

# Install packages from Brewfile if it exists
install_brewfile_packages() {
local brewfile_path="$dotfiles_root/macos/Brewfile"

if [[ -f "$brewfile_path" ]]; then
echo "πŸ“¦ Installing packages from Brewfile..."

# Use brew bundle to install packages
if brew bundle --file="$brewfile_path"; then
echo "βœ… Brewfile packages installed successfully"
else
echo "⚠️ Some Brewfile packages may have failed to install" >&2
# Don't fail the entire script for package installation issues
fi
else
echo "πŸ“¦ No Brewfile found at $brewfile_path, skipping package installation"
fi
}

# Update Homebrew and installed packages
update_homebrew() {
echo "πŸ”„ Updating Homebrew and packages..."

if brew update && brew upgrade; then
echo "βœ… Homebrew update completed"
else
echo "⚠️ Homebrew update encountered issues" >&2
# Don't fail for update issues
fi
}

# Main execution
main() {
local action="${1:-install}"

case "$action" in
"install")
install_homebrew
install_brewfile_packages
;;
"update")
# Ensure Homebrew is available first
ensure_homebrew_in_path
update_homebrew
;;
"packages")
# Ensure Homebrew is available first
ensure_homebrew_in_path
install_brewfile_packages
;;
*)
echo "Usage: $0 [install|update|packages]" >&2
echo " install - Install Homebrew and packages (default)" >&2
echo " update - Update Homebrew and packages" >&2
echo " packages - Install only Brewfile packages" >&2
exit 1
;;
esac
}

# Only run main if script is executed directly (not sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
99 changes: 99 additions & 0 deletions lib/homebrew-utils.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env bash

# Homebrew utility functions for installation scripts
# Provides reusable functionality for detecting and working with Homebrew

set -euo pipefail

# Detect if Homebrew is installed on the system
# Prints informative messages and returns appropriate exit codes
# Returns: 0 if installed, 1 if not installed
detect_homebrew() {
# Check if brew command exists and actually works
if command -v brew >/dev/null 2>&1 && brew --version >/dev/null 2>&1; then
printf "🍺 Homebrew is already installed\n"
return 0
else
printf "🍺 Homebrew is not installed\n"
return 1
fi
}

# Get Homebrew version information
# Returns: 0 if brew is available and version retrieved, 1 otherwise
get_homebrew_version() {
if command -v brew >/dev/null 2>&1; then
brew --version 2>/dev/null
return 0
else
echo "Homebrew not available"
return 1
fi
}

# Check if Homebrew installation is functional
# Returns: 0 if working properly, 1 if issues detected
validate_homebrew_installation() {
if ! command -v brew >/dev/null 2>&1; then
echo "Homebrew binary not found"
return 1
fi

# Test that brew can execute basic commands
if ! brew --version >/dev/null 2>&1; then
echo "Homebrew binary found but not functional"
return 1
fi

echo "Homebrew installation is functional"
return 0
}

# Get the appropriate Homebrew prefix based on system architecture
# Returns: /opt/homebrew for Apple Silicon, /usr/local for Intel
get_homebrew_prefix() {
local arch
arch=$(uname -m 2>/dev/null || echo "unknown")

case "$arch" in
"arm64")
echo "/opt/homebrew"
;;
*)
# Default to Intel prefix for x86_64 and unknown architectures
echo "/usr/local"
;;
esac
}

# Ensure Homebrew bin directory is in PATH
# Adds the appropriate Homebrew prefix to PATH if not already present
ensure_homebrew_in_path() {
local homebrew_prefix
homebrew_prefix=$(get_homebrew_prefix)
local homebrew_bin="$homebrew_prefix/bin"

# Check if already in PATH
if [[ ":$PATH:" != *":$homebrew_bin:"* ]]; then
export PATH="$homebrew_bin:$PATH"
fi
}

# Check if a specific Homebrew package is installed
# Args: package_name - name of the package to check
# Returns: 0 if installed, 1 if not installed
is_homebrew_package_installed() {
local package_name="$1"

if [[ -z "$package_name" ]]; then
echo "Package name is required" >&2
return 1
fi

# Use brew list to check if package is installed
if brew list --formula 2>/dev/null | grep -q "^${package_name}$"; then
return 0
else
return 1
fi
}
146 changes: 146 additions & 0 deletions test/install/test-homebrew-utils.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env bats

# Homebrew utility functions tests
# Tests the core Homebrew detection and validation functions

# Load the Homebrew utilities
load "../../lib/homebrew-utils.bash"

# Test setup and teardown
setup() {
# Create temporary directory for each test
export TEST_TEMP_DIR
TEST_TEMP_DIR="$(mktemp -d)"

# Save original HOME and PATH for restoration
export ORIGINAL_HOME="$HOME"
export ORIGINAL_PATH="$PATH"
}

teardown() {
# Restore original environment
export HOME="$ORIGINAL_HOME"
export PATH="$ORIGINAL_PATH"

# Clean up temporary directory
if [[ -n "${TEST_TEMP_DIR:-}" && -d "$TEST_TEMP_DIR" ]]; then
rm -rf "$TEST_TEMP_DIR"
fi
}

# Unit Tests for get_homebrew_prefix function

@test "get_homebrew_prefix returns /opt/homebrew on Apple Silicon" {
# Create a fake uname command that returns arm64
echo '#!/bin/bash
echo "arm64"' > "$TEST_TEMP_DIR/uname"
chmod +x "$TEST_TEMP_DIR/uname"
export PATH="$TEST_TEMP_DIR:$PATH"

run get_homebrew_prefix
[ "$status" -eq 0 ]
[ "$output" = "/opt/homebrew" ]
}

@test "get_homebrew_prefix returns /usr/local on Intel" {
# Create a fake uname command that returns x86_64
echo '#!/bin/bash
echo "x86_64"' > "$TEST_TEMP_DIR/uname"
chmod +x "$TEST_TEMP_DIR/uname"
export PATH="$TEST_TEMP_DIR:$PATH"

run get_homebrew_prefix
[ "$status" -eq 0 ]
[ "$output" = "/usr/local" ]
}

@test "get_homebrew_prefix returns /usr/local for unknown architecture" {
# Create a fake uname command that returns unknown value
echo '#!/bin/bash
echo "unknown"' > "$TEST_TEMP_DIR/uname"
chmod +x "$TEST_TEMP_DIR/uname"
export PATH="$TEST_TEMP_DIR:$PATH"

run get_homebrew_prefix
[ "$status" -eq 0 ]
[ "$output" = "/usr/local" ]
}

# Unit Tests for ensure_homebrew_in_path function

@test "ensure_homebrew_in_path adds homebrew prefix to PATH when missing" {
# Create a fake uname command that returns arm64
echo '#!/bin/bash
echo "arm64"' > "$TEST_TEMP_DIR/uname"
chmod +x "$TEST_TEMP_DIR/uname"
export PATH="$TEST_TEMP_DIR:/usr/bin:/bin"

# Function modifies PATH, so we need to source and check in same shell
ensure_homebrew_in_path
[ "$?" -eq 0 ]
# Check that /opt/homebrew/bin is now in PATH
[[ "$PATH" == *"/opt/homebrew/bin"* ]]
}

@test "ensure_homebrew_in_path does not duplicate when already in PATH" {
# Create a fake uname command that returns arm64
echo '#!/bin/bash
echo "arm64"' > "$TEST_TEMP_DIR/uname"
chmod +x "$TEST_TEMP_DIR/uname"
export PATH="/opt/homebrew/bin:$TEST_TEMP_DIR:/usr/bin:/bin"

run ensure_homebrew_in_path
[ "$status" -eq 0 ]
# Check that PATH doesn't have duplicate entries
local path_count=$(echo "$PATH" | tr ':' '\n' | grep -c "/opt/homebrew/bin")
[ "$path_count" -eq 1 ]
}

# Unit Tests for is_homebrew_package_installed function

@test "is_homebrew_package_installed returns 0 when package is installed" {
# Create a fake brew command that lists packages including git
echo '#!/bin/bash
if [[ "$1" == "list" && "$2" == "--formula" ]]; then
echo "git"
echo "node"
echo "python@3.11"
fi' > "$TEST_TEMP_DIR/brew"
chmod +x "$TEST_TEMP_DIR/brew"
export PATH="$TEST_TEMP_DIR:$PATH"

run is_homebrew_package_installed "git"
[ "$status" -eq 0 ]
}

@test "is_homebrew_package_installed returns 1 when package is not installed" {
# Create a fake brew command that lists packages NOT including git
echo '#!/bin/bash
if [[ "$1" == "list" && "$2" == "--formula" ]]; then
echo "node"
echo "python@3.11"
fi' > "$TEST_TEMP_DIR/brew"
chmod +x "$TEST_TEMP_DIR/brew"
export PATH="$TEST_TEMP_DIR:$PATH"

run is_homebrew_package_installed "git"
[ "$status" -eq 1 ]
}

@test "is_homebrew_package_installed returns 1 when brew list fails" {
# Create a fake brew command that fails
echo '#!/bin/bash
echo "Error: No such command" >&2
exit 1' > "$TEST_TEMP_DIR/brew"
chmod +x "$TEST_TEMP_DIR/brew"
export PATH="$TEST_TEMP_DIR:$PATH"

run is_homebrew_package_installed "git"
[ "$status" -eq 1 ]
}

@test "is_homebrew_package_installed requires package name argument" {
run is_homebrew_package_installed ""
[ "$status" -eq 1 ]
[[ "$output" == *"Package name is required"* ]]
}