diff --git a/Bins/versions.json b/Bins/versions.json index 63d50d7..e32ec1c 100644 --- a/Bins/versions.json +++ b/Bins/versions.json @@ -12,6 +12,16 @@ "asset": "nvim-linux-arm64.tar.gz", "binary": "nvim-linux-arm64/bin/nvim" } + }, + "macos": { + "x86_64": { + "asset": "nvim-macos-x86_64.tar.gz", + "binary": "nvim-macos-x86_64/bin/nvim" + }, + "aarch64": { + "asset": "nvim-macos-arm64.tar.gz", + "binary": "nvim-macos-arm64/bin/nvim" + } } }, "eza": { @@ -27,6 +37,9 @@ "asset": "eza_aarch64-unknown-linux-gnu.tar.gz", "binary": "eza" } + }, + "macos": { + "formula": "eza" } } } diff --git a/Scripts/base.sh b/Scripts/base.sh index acc5a0c..2586e8b 100644 --- a/Scripts/base.sh +++ b/Scripts/base.sh @@ -18,11 +18,15 @@ echo -e "${BLUE} LOG:${YELLOW} Initializing Base System Layer...${NC}" # Confirm Architecture ARCH=$(uname -m) -if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then +if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ] && [ "$ARCH" != "arm64" ]; then echo -e "${RED} ERROR: Unsupported architecture: $ARCH${NC}" exit 1 fi +is_wsl() { + [ -f /proc/version ] && grep -qEi "(Microsoft|WSL)" /proc/version +} + # Package Installation PACKAGES=( curl wget git sudo @@ -51,6 +55,8 @@ for pkg in "${PACKAGES[@]}"; do FINAL_PACKAGES+=(build-essential) elif is_fedora_family; then FINAL_PACKAGES+=(gcc gcc-c++ make patch) + elif is_macos; then + : fi continue ;; @@ -61,6 +67,8 @@ for pkg in "${PACKAGES[@]}"; do FINAL_PACKAGES+=(python3 python3-pip python3-venv) elif is_fedora_family; then FINAL_PACKAGES+=(python3 python3-pip) + elif is_macos; then + FINAL_PACKAGES+=(python) fi continue ;; @@ -83,6 +91,8 @@ for pkg in "${PACKAGES[@]}"; do FINAL_PACKAGES+=(libssl-dev) elif is_fedora_family; then FINAL_PACKAGES+=(openssl openssl-devel) + elif is_macos; then + FINAL_PACKAGES+=(openssl@3) fi continue ;; @@ -93,6 +103,8 @@ for pkg in "${PACKAGES[@]}"; do FINAL_PACKAGES+=(dnsutils) elif is_fedora_family; then FINAL_PACKAGES+=(bind-utils) + elif is_macos; then + FINAL_PACKAGES+=(bind) fi continue ;; @@ -101,6 +113,8 @@ for pkg in "${PACKAGES[@]}"; do FINAL_PACKAGES+=(ninja) elif is_debian_family || is_fedora_family; then FINAL_PACKAGES+=(ninja-build) + elif is_macos; then + FINAL_PACKAGES+=(ninja) fi continue ;; @@ -111,12 +125,16 @@ for pkg in "${PACKAGES[@]}"; do FINAL_PACKAGES+=(libcurl4-openssl-dev) elif is_fedora_family; then FINAL_PACKAGES+=(libcurl-devel) + elif is_macos; then + FINAL_PACKAGES+=(curl) fi continue ;; "gnupg") if is_fedora_family; then FINAL_PACKAGES+=(gnupg2) + elif is_macos; then + FINAL_PACKAGES+=(gnupg) else FINAL_PACKAGES+=(gnupg) fi @@ -137,6 +155,18 @@ if is_fedora_family; then FINAL_PACKAGES+=(R-core gcc-gfortran bzip2 bzip2-devel readline-devel sqlite sqlite-devel tk-devel libffi-devel xz xz-devel ncurses-devel zlib-devel findutils llvm) fi +if is_macos; then + FINAL_PACKAGES=( + curl wget git zsh tmux unzip + python bison mercurial + ripgrep fd bat fzf jq + btop httpie gnupg + zoxide stow direnv + bind nmap socat + hexyl ninja just + ) +fi + echo -e "${BLUE} LOG:${YELLOW} Installing: ${NC}${FINAL_PACKAGES[*]}" install_status=0 @@ -160,48 +190,60 @@ bash "$REPO_ROOT/Scripts/bin/install.sh" --all if ! command -v rclone &> /dev/null; then echo -e "${BLUE} LOG:${YELLOW} Installing Rclone CLI...${NC}" - case "$ARCH" in - x86_64) - RCLONE_ARCH="amd64" - ;; - aarch64) - RCLONE_ARCH="arm64" - ;; - esac + if is_macos; then + brew install rclone + else - TEMP_DIR="$(mktemp -d)" - RCLONE_ZIP="$TEMP_DIR/rclone.zip" + case "$ARCH" in + x86_64) + RCLONE_ARCH="amd64" + ;; + aarch64|arm64) + RCLONE_ARCH="arm64" + ;; + esac - curl -fLsS "https://downloads.rclone.org/rclone-current-linux-${RCLONE_ARCH}.zip" -o "$RCLONE_ZIP" - unzip -q "$RCLONE_ZIP" -d "$TEMP_DIR" - install -m 755 "$TEMP_DIR"/rclone-*-linux-"${RCLONE_ARCH}"/rclone "$HOME/.local/bin/rclone" - rm -rf "$TEMP_DIR" + TEMP_DIR="$(mktemp -d)" + RCLONE_ZIP="$TEMP_DIR/rclone.zip" + + curl -fLsS "https://downloads.rclone.org/rclone-current-linux-${RCLONE_ARCH}.zip" -o "$RCLONE_ZIP" + unzip -q "$RCLONE_ZIP" -d "$TEMP_DIR" + install -m 755 "$TEMP_DIR"/rclone-*-linux-"${RCLONE_ARCH}"/rclone "$HOME/.local/bin/rclone" + rm -rf "$TEMP_DIR" + fi fi if ! command -v earthly &> /dev/null; then echo -e "${BLUE} LOG:${YELLOW} Installing Earthly CLI...${NC}" - case "$ARCH" in - x86_64) - EARTHLY_ARCH="amd64" - ;; - aarch64) - EARTHLY_ARCH="arm64" - ;; - esac + if is_macos; then + brew install earthly + else - curl -fLsS "https://github.com/earthly/earthly/releases/latest/download/earthly-linux-${EARTHLY_ARCH}" \ - -o "$HOME/.local/bin/earthly" - chmod +x "$HOME/.local/bin/earthly" + case "$ARCH" in + x86_64) + EARTHLY_ARCH="amd64" + ;; + aarch64|arm64) + EARTHLY_ARCH="arm64" + ;; + esac + + curl -fLsS "https://github.com/earthly/earthly/releases/latest/download/earthly-linux-${EARTHLY_ARCH}" \ + -o "$HOME/.local/bin/earthly" + chmod +x "$HOME/.local/bin/earthly" + fi fi # Docker Installation if command -v docker &> /dev/null; then echo -e "${GREEN} LOG: Docker is already installed.${NC}" else - if grep -qEi "(Microsoft|WSL)" /proc/version &> /dev/null; then + if is_wsl; then echo -e "${RED} LOG: WSL Detected! Skipping Native Docker.${NC}" echo -e "${RED} >>> Please install Docker Desktop on Windows.${NC}" + elif is_macos; then + echo -e "${YELLOW} NOTE:${NC} Docker is not installed. Please install Docker Desktop for macOS manually." else echo -e "${BLUE} LOG:${YELLOW} Installing Native Docker...${NC}" if is_arch_family; then @@ -220,7 +262,7 @@ else fi # Add user to group - if getent group docker >/dev/null 2>&1; then + if command -v getent >/dev/null 2>&1 && getent group docker >/dev/null 2>&1; then sudo usermod -aG docker $(whoami) fi fi @@ -239,7 +281,7 @@ mkdir -p "$ZSH_CUSTOM/plugins" [ ! -d "$ZSH_CUSTOM/plugins/zsh-autosuggestions" ] && git clone https://github.com/zsh-users/zsh-autosuggestions "$ZSH_CUSTOM/plugins/zsh-autosuggestions" # 5. Git Credentials (WSL Bridge) -if grep -qEi "(Microsoft|WSL)" /proc/version &> /dev/null; then +if is_wsl; then GCM_WIN="/mnt/c/Program Files/Git/mingw64/bin/git-credential-manager.exe" [ -f "$GCM_WIN" ] && git config --global credential.helper "! \"$GCM_WIN\"" else @@ -253,10 +295,21 @@ touch "$REPO_ROOT/Zsh/.zsh_secrets" # 7. Set Shell TARGET_SHELL="$(command -v zsh)" -CURRENT_LOGIN_SHELL="$(getent passwd "$(whoami)" | cut -d: -f7)" +CURRENT_LOGIN_SHELL="" + +if command -v getent >/dev/null 2>&1; then + CURRENT_LOGIN_SHELL="$(getent passwd "$(whoami)" | cut -d: -f7)" +elif is_macos && command -v dscl >/dev/null 2>&1; then + CURRENT_LOGIN_SHELL="$(dscl . -read "/Users/$(whoami)" UserShell 2>/dev/null | awk '{print $2}')" +fi if [ "$CURRENT_LOGIN_SHELL" != "$TARGET_SHELL" ]; then - if command -v chsh >/dev/null 2>&1; then + if [ ! -t 0 ]; then + echo -e "${YELLOW} NOTE:${NC} Non-interactive session detected. Skipping login shell change to $TARGET_SHELL." + echo -e "${YELLOW} NOTE:${NC} Run 'chsh -s $TARGET_SHELL' manually later if you want zsh as your login shell." + elif is_macos && command -v chsh >/dev/null 2>&1; then + chsh -s "$TARGET_SHELL" + elif command -v chsh >/dev/null 2>&1; then sudo chsh -s "$TARGET_SHELL" "$(whoami)" elif command -v usermod >/dev/null 2>&1; then sudo usermod -s "$TARGET_SHELL" "$(whoami)" diff --git a/Scripts/bin/install.sh b/Scripts/bin/install.sh index 9f85c5f..095d804 100644 --- a/Scripts/bin/install.sh +++ b/Scripts/bin/install.sh @@ -22,6 +22,17 @@ ensure_deps() { fi } +detect_platform() { + case "$(uname -s)" in + Linux) printf 'linux\n' ;; + Darwin) printf 'macos\n' ;; + *) + printf 'Unsupported operating system: %s\n' "$(uname -s)" >&2 + exit 1 + ;; + esac +} + detect_arch() { case "$(uname -m)" in x86_64) printf 'x86_64\n' ;; @@ -35,8 +46,9 @@ detect_arch() { install_tool() { local tool="$1" - local arch version owner repo asset binary_rel url tmp_dir archive_path extracted_path target_path + local platform arch version owner repo asset binary_rel formula url tmp_dir archive_path extracted_path target_path installed_path + platform="$(detect_platform)" arch="$(detect_arch)" if ! jq -e --arg tool "$tool" '.[$tool]' "$MANIFEST" >/dev/null; then @@ -47,11 +59,39 @@ install_tool() { version="$(jq -r --arg tool "$tool" '.[$tool].version' "$MANIFEST")" owner="$(jq -r --arg tool "$tool" '.[$tool].owner' "$MANIFEST")" repo="$(jq -r --arg tool "$tool" '.[$tool].repo' "$MANIFEST")" - asset="$(jq -r --arg tool "$tool" --arg arch "$arch" '.[$tool].linux[$arch].asset' "$MANIFEST")" - binary_rel="$(jq -r --arg tool "$tool" --arg arch "$arch" '.[$tool].linux[$arch].binary' "$MANIFEST")" + formula="$(jq -r --arg tool "$tool" --arg platform "$platform" '.[$tool][$platform].formula // empty' "$MANIFEST")" - if [ -z "$asset" ] || [ "$asset" = "null" ] || [ -z "$binary_rel" ] || [ "$binary_rel" = "null" ]; then - printf 'No Linux asset mapping for %s on %s\n' "$tool" "$arch" >&2 + if [ -n "$formula" ]; then + if ! command -v brew >/dev/null 2>&1; then + printf 'Homebrew is required to install %s on macOS\n' "$tool" >&2 + exit 1 + fi + + if ! brew list --formula "$formula" >/dev/null 2>&1; then + printf 'Installing %s via Homebrew formula %s\n' "$tool" "$formula" + brew install "$formula" + else + printf '%s already installed via Homebrew\n' "$tool" + fi + + installed_path="$(command -v "$tool" || true)" + if [ -z "$installed_path" ]; then + printf 'Installed formula %s but %s is not on PATH\n' "$formula" "$tool" >&2 + exit 1 + fi + + mkdir -p "$TARGET_DIR" + target_path="$TARGET_DIR/$tool" + ln -sf "$installed_path" "$target_path" + printf 'Linked %s -> %s\n' "$target_path" "$installed_path" + return 0 + fi + + asset="$(jq -r --arg tool "$tool" --arg platform "$platform" --arg arch "$arch" '.[$tool][$platform][$arch].asset // empty' "$MANIFEST")" + binary_rel="$(jq -r --arg tool "$tool" --arg platform "$platform" --arg arch "$arch" '.[$tool][$platform][$arch].binary // empty' "$MANIFEST")" + + if [ -z "$asset" ] || [ -z "$binary_rel" ]; then + printf 'No %s asset mapping for %s on %s\n' "$platform" "$tool" "$arch" >&2 exit 1 fi diff --git a/Scripts/go.sh b/Scripts/go.sh index 1ce871c..038ff95 100644 --- a/Scripts/go.sh +++ b/Scripts/go.sh @@ -13,13 +13,14 @@ NC='\033[0m' export GVM_ROOT="$HOME/.programming/go" BOOTSTRAP_GO="$GVM_ROOT/bootstrap" +OS_NAME="$(uname -s)" ARCH="$(uname -m)" case "$ARCH" in x86_64) GO_BOOTSTRAP_ARCH="amd64" ;; - aarch64) + aarch64|arm64) GO_BOOTSTRAP_ARCH="arm64" ;; *) @@ -28,6 +29,19 @@ case "$ARCH" in ;; esac +case "$OS_NAME" in + Linux) + GO_BOOTSTRAP_OS="linux" + ;; + Darwin) + GO_BOOTSTRAP_OS="darwin" + ;; + *) + echo -e "${RED} ERROR:${NC} Unsupported operating system for Go bootstrap: $OS_NAME" + exit 1 + ;; +esac + echo -e "${BLUE} LOG:${YELLOW} Setting up Go and GVM in ${GVM_ROOT}...${NC}" if [ ! -d "$GVM_ROOT/scripts" ]; then @@ -36,7 +50,15 @@ if [ ! -d "$GVM_ROOT/scripts" ]; then rm -rf "$GVM_ROOT/.git" echo -e "${BLUE} LOG:${YELLOW} Configuring GVM scripts...${NC}" cp "$GVM_ROOT/scripts/gvm-default" "$GVM_ROOT/scripts/gvm" - sed -i "s|^GVM_ROOT=.*|GVM_ROOT=\"$GVM_ROOT\"|" "$GVM_ROOT/scripts/gvm" + python3 - "$GVM_ROOT/scripts/gvm" "$GVM_ROOT" <<'PY' +from pathlib import Path +import sys + +path = Path(sys.argv[1]) +root = sys.argv[2] +text = path.read_text() +path.write_text(text.replace('GVM_ROOT="$HOME/.gvm"', f'GVM_ROOT="{root}"', 1)) +PY echo -e "${GREEN} SUCCESS:${NC} GVM cloned and configured." else @@ -47,7 +69,7 @@ if [ ! -d "$BOOTSTRAP_GO" ]; then echo -e "${BLUE} LOG:${YELLOW} Downloading Bootstrap Go...${NC}" mkdir -p "$BOOTSTRAP_GO" - wget -q "https://go.dev/dl/go1.20.5.linux-${GO_BOOTSTRAP_ARCH}.tar.gz" -O /tmp/go-bootstrap.tar.gz + wget -q "https://go.dev/dl/go1.20.5.${GO_BOOTSTRAP_OS}-${GO_BOOTSTRAP_ARCH}.tar.gz" -O /tmp/go-bootstrap.tar.gz tar -C "$BOOTSTRAP_GO" -xzf /tmp/go-bootstrap.tar.gz --strip-components=1 rm /tmp/go-bootstrap.tar.gz diff --git a/Scripts/lib/distro.sh b/Scripts/lib/distro.sh index d5fed1d..8e5c315 100644 --- a/Scripts/lib/distro.sh +++ b/Scripts/lib/distro.sh @@ -10,8 +10,11 @@ if [ -f /etc/os-release ]; then . /etc/os-release OS="${ID}" OS_LIKE="${ID_LIKE:-}" +elif [ "$(uname -s)" = "Darwin" ]; then + OS="macos" + OS_LIKE="darwin" else - echo "Unable to detect operating system: /etc/os-release not found." + echo "Unable to detect operating system: /etc/os-release not found and host is not macOS." return 1 2>/dev/null || exit 1 fi @@ -27,6 +30,10 @@ is_fedora_family() { [[ "$OS" == "fedora" || "$OS" == "bazzite" || " $OS_LIKE " == *" fedora "* || " $OS_LIKE " == *" rhel "* ]] } +is_macos() { + [[ "$OS" == "macos" ]] +} + is_atomic_fedora() { is_fedora_family && command -v rpm-ostree >/dev/null 2>&1 && [ -f /run/ostree-booted ] } @@ -126,6 +133,22 @@ install_packages() { return 0 fi + if is_macos; then + if ! command -v brew >/dev/null 2>&1; then + echo "Homebrew is required on macOS. Install it from https://brew.sh first." + return 1 + fi + + local pkg + for pkg in "${packages[@]}"; do + if brew list --formula "$pkg" >/dev/null 2>&1; then + continue + fi + brew install "$pkg" + done + return 0 + fi + echo "Unsupported OS: $OS" return 1 } diff --git a/Scripts/python.sh b/Scripts/python.sh index 47bddf6..73338b0 100644 --- a/Scripts/python.sh +++ b/Scripts/python.sh @@ -22,6 +22,9 @@ elif is_debian_family; then elif is_fedora_family; then echo -e "${BLUE} LOG:${YELLOW} Installing Fedora build dependencies...${NC}" PYTHON_BUILD_DEPS=(make gcc gcc-c++ patch zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel xz xz-devel ncurses-devel findutils git) +elif is_macos; then + echo -e "${BLUE} LOG:${YELLOW} Installing macOS build dependencies...${NC}" + PYTHON_BUILD_DEPS=(openssl@3 readline sqlite xz tcl-tk bzip2 zlib pkg-config git) else echo -e "${RED} ERROR:${NC} Unsupported OS: $OS" exit 1 diff --git a/Scripts/r.sh b/Scripts/r.sh index 0bd366a..4ef06a5 100644 --- a/Scripts/r.sh +++ b/Scripts/r.sh @@ -118,6 +118,19 @@ elif is_fedora_family; then write_r_wrapper "R" 'exec "$(command -v R)"' write_r_wrapper "Rscript" 'exec "$(command -v Rscript)"' +elif is_macos; then + + echo -e "${BLUE} LOG:${YELLOW} macOS detected. Using Homebrew R...${NC}" + install_status=0 + install_packages r || install_status=$? + if [ "$install_status" -ne 0 ]; then + exit "$install_status" + fi + + R_BIN="R" + write_r_wrapper "R" 'exec "$(command -v R)"' + write_r_wrapper "Rscript" 'exec "$(command -v Rscript)"' + else echo -e "${RED} ERROR:${NC} Unsupported OS: $OS" exit 1 diff --git a/Scripts/setup.sh b/Scripts/setup.sh index 9f2b65d..d1eb19f 100644 --- a/Scripts/setup.sh +++ b/Scripts/setup.sh @@ -7,6 +7,7 @@ REPO_ROOT="$(dirname "$SCRIPT_DIR")" REBOOT_MARKER="$REPO_ROOT/.setup-reboot-required" REPO_SECRET_FILE="$REPO_ROOT/Zsh/.zsh_secrets" HOME_SECRET_FILE="$HOME/.zsh_secrets" +BACKUP_ROOT="$HOME/.dotzsh-pre-stow-backup" handle_reboot_marker() { if [ -f "$REBOOT_MARKER" ]; then @@ -42,6 +43,28 @@ sync_repo_managed_secret_file() { touch "$REPO_SECRET_FILE" } +backup_conflicting_home_files() { + local backup_dir backed_up_any=0 source_file filename target_file + + backup_dir="$BACKUP_ROOT/$(date +%Y%m%d-%H%M%S)" + + while IFS= read -r source_file; do + filename="$(basename "$source_file")" + target_file="$HOME/$filename" + + if [ -e "$target_file" ] && [ ! -L "$target_file" ]; then + mkdir -p "$backup_dir" + mv "$target_file" "$backup_dir/$filename" + echo "Backed up existing $target_file -> $backup_dir/$filename" + backed_up_any=1 + fi + done < <(find "$REPO_ROOT/Zsh" -mindepth 1 -maxdepth 1 -type f -name '.*' ! -name '.zsh_secrets') + + if [ "$backed_up_any" -eq 1 ]; then + echo "Existing unmanaged dotfiles were backed up before stowing." + fi +} + main() { rm -f "$REBOOT_MARKER" @@ -58,6 +81,7 @@ main() { handle_reboot_marker sync_repo_managed_secret_file + backup_conflicting_home_files stow --dir="$REPO_ROOT" -D Zsh --target="$HOME" 2>/dev/null || true stow --dir="$REPO_ROOT" Zsh --target="$HOME"