diff --git a/AGENTS.md b/AGENTS.md index d1a3572..13de857 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ This is a personal dotfiles repository targeting macOS (Apple Silicon + Intel) a Setup is platform-specific and runs from the repo root: -- **macOS:** `./setup_mac.sh` — brews dependencies, creates a `neovim` virtualenv, then calls `./clone_and_link_mac.sh`. Uses GNU coreutils' `gln` (not `ln`), so coreutils must be installed first. +- **macOS:** `./setup_mac.sh` — brews dependencies, installs the latest verified upstream Alacritty release, creates a `neovim` virtualenv, then calls `./clone_and_link_mac.sh`. Uses GNU coreutils' `gln` (not `ln`), so coreutils must be installed first. - **Arch:** `./setup_arch.sh` — pacman deps, then `./clone_and_link.sh` + `./copy_fonts_arch.sh`. - `clone_and_link*.sh` create the symlinks (`~/.zsh`, `~/.zshrc`, `~/.tmux`, `~/.tmux.conf`, `~/.config/nvim`, `~/.config/alacritty`, virtualenvwrapper hooks). `link_config.sh` is a simpler standalone variant. - `general_setup.sh` switches the login shell to zsh and triggers plugin installs. @@ -33,5 +33,5 @@ Practical rule: **edit the `.example` source, not the generated symlink target.* ## Maintenance -- `./update_mac.sh` — macOS update runner. Default (no args) runs `--brew-update --cleanup --diagnostics --update-others`; other flags: `--system-update`, `--permission-fix` (strips Caskroom/App quarantine xattrs). Run `./update_mac.sh --help` for the list. `--update-others` also runs `claude --update` and `Codex --update`. -- `brew_autoupdate` — sets up scheduled brew auto-updates. +- `./update_mac.sh` — macOS update runner. Default (no args) runs `--brew-update --cleanup --diagnostics --update-others`; other flags: `--system-update`, `--permission-fix` (strips Caskroom/App quarantine xattrs). Run `./update_mac.sh --help` for the list. `--update-others` also updates Alacritty from its verified upstream release, then runs `claude --update` and `Codex --update`. +- `brew_autoupdate` — runs scheduled brew and Alacritty auto-updates. diff --git a/brew_autoupdate b/brew_autoupdate index 74d343a..f44c085 100755 --- a/brew_autoupdate +++ b/brew_autoupdate @@ -6,5 +6,6 @@ export HOMEBREW_LOGS='/Users/ising/Library/Logs/Homebrew' export SUDO_ASKPASS='/Users/ising/Library/Application Support/com.github.domt4.homebrew-autoupdate/brew_autoupdate_sudo_gui' defaults export com.apple.dock "/tmp/dock-layout.plist" /bin/date && /opt/homebrew/bin/brew update && /opt/homebrew/bin/brew upgrade --formula -v && /opt/homebrew/bin/brew upgrade --cask -v && /opt/homebrew/bin/brew cleanup && /usr/bin/open -g /opt/homebrew/Library/Taps/homebrew/homebrew-autoupdate/notifier/brew-autoupdate.app +/Users/ising/dotfiles/install_alacritty_mac.sh defaults import com.apple.dock "/tmp/dock-layout.plist" killall dock diff --git a/install_alacritty_mac.sh b/install_alacritty_mac.sh new file mode 100755 index 0000000..4e27ac3 --- /dev/null +++ b/install_alacritty_mac.sh @@ -0,0 +1,265 @@ +#!/bin/bash + +set -euo pipefail + +readonly RELEASE_API="https://api.github.com/repos/alacritty/alacritty/releases/latest" +readonly APPLICATIONS_DIR="/Applications" +readonly APP_NAME="Alacritty.app" + +temp_dir="" +mount_point="" + +cleanup() { + if [[ -n "$mount_point" && -d "$mount_point" ]]; then + hdiutil detach "$mount_point" >/dev/null 2>&1 || true + fi + if [[ -n "$temp_dir" && -d "$temp_dir" ]]; then + rm -rf "$temp_dir" + fi +} + +trap cleanup EXIT INT TERM + +die() { + echo "Alacritty update failed: $*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +app_version() { + /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" \ + "$1/Contents/Info.plist" 2>/dev/null || true +} + +version_is_newer() { + local left="${1%%-*}" + local right="${2%%-*}" + local left_major left_minor left_patch + local right_major right_minor right_patch + + IFS=. read -r left_major left_minor left_patch <<< "$left" + IFS=. read -r right_major right_minor right_patch <<< "$right" + + [[ "$left_major" =~ ^[0-9]+$ && "$left_minor" =~ ^[0-9]+$ && "$left_patch" =~ ^[0-9]+$ ]] || return 1 + [[ "$right_major" =~ ^[0-9]+$ && "$right_minor" =~ ^[0-9]+$ && "$right_patch" =~ ^[0-9]+$ ]] || return 1 + + (( 10#$left_major > 10#$right_major )) && return 0 + (( 10#$left_major < 10#$right_major )) && return 1 + (( 10#$left_minor > 10#$right_minor )) && return 0 + (( 10#$left_minor < 10#$right_minor )) && return 1 + (( 10#$left_patch > 10#$right_patch )) +} + +run_for_applications() { + if [[ -w "$APPLICATIONS_DIR" ]]; then + "$@" + else + sudo "$@" + fi +} + +safe_link() { + local source="$1" + local destination="$2" + + if [[ -e "$destination" && ! -L "$destination" ]]; then + die "will not replace non-symlink: $destination" + fi + ln -sfn "$source" "$destination" +} + +assert_linkable() { + local destination="$1" + + if [[ -e "$destination" && ! -L "$destination" ]]; then + die "will not replace non-symlink: $destination" + fi +} + +validate_shell_artifacts() { + local app="$1" + local brew_prefix="$2" + local resources="$app/Contents/Resources" + local source manpage section + + for source in \ + "$app/Contents/MacOS/alacritty" \ + "$resources/completions/alacritty.bash" \ + "$resources/completions/alacritty.fish" \ + "$resources/completions/_alacritty" \ + "$resources/61/alacritty" \ + "$resources/61/alacritty-direct"; do + [[ -f "$source" ]] || die "required artifact is missing: ${source#"$app/"}" + done + + assert_linkable "$brew_prefix/bin/alacritty" + assert_linkable "$brew_prefix/etc/bash_completion.d/alacritty" + assert_linkable "$brew_prefix/share/fish/vendor_completions.d/alacritty.fish" + assert_linkable "$brew_prefix/share/zsh/site-functions/_alacritty" + assert_linkable "$HOME/.terminfo/61/alacritty" + assert_linkable "$HOME/.terminfo/61/alacritty-direct" + + for section in 1 5 7; do + for manpage in "$resources"/*."$section".gz; do + [[ -e "$manpage" ]] || continue + assert_linkable "$brew_prefix/share/man/man$section/${manpage##*/}" + done + done +} + +install_shell_artifacts() { + local app="$APPLICATIONS_DIR/$APP_NAME" + local resources="$app/Contents/Resources" + local brew_prefix="$1" + local manpage section + + mkdir -p \ + "$brew_prefix/bin" \ + "$brew_prefix/etc/bash_completion.d" \ + "$brew_prefix/share/fish/vendor_completions.d" \ + "$brew_prefix/share/zsh/site-functions" \ + "$brew_prefix/share/man/man1" \ + "$brew_prefix/share/man/man5" \ + "$brew_prefix/share/man/man7" \ + "$HOME/.terminfo/61" + + safe_link "$app/Contents/MacOS/alacritty" "$brew_prefix/bin/alacritty" + safe_link "$resources/completions/alacritty.bash" "$brew_prefix/etc/bash_completion.d/alacritty" + safe_link "$resources/completions/alacritty.fish" "$brew_prefix/share/fish/vendor_completions.d/alacritty.fish" + safe_link "$resources/completions/_alacritty" "$brew_prefix/share/zsh/site-functions/_alacritty" + safe_link "$resources/61/alacritty" "$HOME/.terminfo/61/alacritty" + safe_link "$resources/61/alacritty-direct" "$HOME/.terminfo/61/alacritty-direct" + + for section in 1 5 7; do + for manpage in "$resources"/*."$section".gz; do + [[ -e "$manpage" ]] || continue + safe_link "$manpage" "$brew_prefix/share/man/man$section/${manpage##*/}" + done + done +} + +restore_previous_app() { + local destination="$1" + local backup="$2" + + run_for_applications rm -rf "$destination" + if [[ -d "$backup" ]]; then + run_for_applications /usr/bin/ditto "$backup" "$destination" + fi +} + +main() { + local destination="$APPLICATIONS_DIR/$APP_NAME" + local release_json latest_tag latest_version asset_name asset_url asset_digest + local expected_sha actual_sha dmg staged_app staged_version installed_version + local previous_app brew_prefix brew_managed=false + + [[ "$(uname -s)" == "Darwin" ]] || die "this installer only supports macOS" + + require_command brew + require_command curl + require_command jq + require_command hdiutil + require_command codesign + + brew_prefix="$(brew --prefix)" + if brew list --cask alacritty >/dev/null 2>&1; then + brew_managed=true + fi + + temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/alacritty-update.XXXXXX")" + release_json="$temp_dir/release.json" + + curl --fail --location --silent --show-error --retry 3 \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "$RELEASE_API" -o "$release_json" + + latest_tag="$(jq -er '.tag_name' "$release_json")" + latest_version="${latest_tag#v}" + asset_name="Alacritty-${latest_tag}.dmg" + asset_url="$(jq -er --arg asset "$asset_name" \ + '.assets[] | select(.name == $asset) | .browser_download_url' "$release_json")" + asset_digest="$(jq -er --arg asset "$asset_name" \ + '.assets[] | select(.name == $asset) | .digest' "$release_json")" + + [[ "$asset_digest" == sha256:* ]] || die "GitHub did not provide a SHA-256 digest for $asset_name" + expected_sha="$(printf '%s' "${asset_digest#sha256:}" | tr '[:upper:]' '[:lower:]')" + [[ "$expected_sha" =~ ^[[:xdigit:]]{64}$ ]] || die "invalid SHA-256 digest for $asset_name" + + installed_version="" + if [[ -d "$destination" ]]; then + installed_version="$(app_version "$destination")" + fi + + if [[ "$installed_version" == "$latest_version" && "$brew_managed" == false ]]; then + validate_shell_artifacts "$destination" "$brew_prefix" + install_shell_artifacts "$brew_prefix" + echo "Alacritty $installed_version is already up to date." + return + fi + if [[ -n "$installed_version" ]] && version_is_newer "$installed_version" "$latest_version"; then + echo "Installed Alacritty $installed_version is newer than stable $latest_version; leaving it unchanged." + return + fi + + echo "Installing Alacritty $latest_version from the upstream release." + dmg="$temp_dir/$asset_name" + curl --fail --location --silent --show-error --retry 3 "$asset_url" -o "$dmg" + + actual_sha="$(shasum -a 256 "$dmg" | awk '{print $1}')" + [[ "$actual_sha" == "$expected_sha" ]] || die "SHA-256 mismatch for $asset_name" + + mount_point="$temp_dir/mount" + mkdir -p "$mount_point" + hdiutil attach "$dmg" -nobrowse -readonly -mountpoint "$mount_point" >/dev/null + [[ -d "$mount_point/$APP_NAME" ]] || die "$APP_NAME is missing from $asset_name" + + staged_app="$temp_dir/$APP_NAME" + /usr/bin/ditto "$mount_point/$APP_NAME" "$staged_app" + hdiutil detach "$mount_point" >/dev/null + mount_point="" + + staged_version="$(app_version "$staged_app")" + [[ "$staged_version" == "$latest_version" ]] || die "release metadata says $latest_version, app says $staged_version" + codesign --verify --deep --strict "$staged_app" || die "the staged app has an invalid ad-hoc signature" + xattr -dr com.apple.quarantine "$staged_app" 2>/dev/null || true + validate_shell_artifacts "$staged_app" "$brew_prefix" + + previous_app="$temp_dir/previous/$APP_NAME" + if [[ -d "$destination" ]]; then + mkdir -p "${previous_app%/*}" + /usr/bin/ditto "$destination" "$previous_app" + fi + + if [[ "$brew_managed" == true ]]; then + echo "Migrating Alacritty away from the disabled Homebrew cask." + if ! brew uninstall --cask alacritty; then + restore_previous_app "$destination" "$previous_app" + install_shell_artifacts "$brew_prefix" || true + die "Homebrew could not uninstall its Alacritty cask" + fi + fi + + if ! run_for_applications rm -rf "$destination" || \ + ! run_for_applications /usr/bin/ditto "$staged_app" "$destination"; then + restore_previous_app "$destination" "$previous_app" + die "could not install $destination" + fi + + if ! codesign --verify --deep --strict "$destination" || \ + [[ "$(app_version "$destination")" != "$latest_version" ]]; then + restore_previous_app "$destination" "$previous_app" + die "installed app validation failed; restored the previous app" + fi + + install_shell_artifacts "$brew_prefix" + echo "Alacritty $latest_version installed successfully." +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi diff --git a/setup_mac.sh b/setup_mac.sh index 8e5869a..87b2ff7 100755 --- a/setup_mac.sh +++ b/setup_mac.sh @@ -6,14 +6,16 @@ # Distributed under terms of the MIT license. # -brew install neovim tmux alacritty coreutils cmake golang npm virtualenvwrapper -source /opt/homebrew/bin/virtualenvwrapper.sh +brew install neovim tmux coreutils cmake golang jq npm virtualenvwrapper +./install_alacritty_mac.sh +BREW_PREFIX="$(brew --prefix)" +source "$BREW_PREFIX/bin/virtualenvwrapper.sh" mkvirtualenv neovim pip3 install pynvim npm install -g neovim -echo For alacritty to work seemlessly with tmux, we will have to add '/usr/local/bin'\ +echo For alacritty to work seamlessly with tmux, we will add the Homebrew bin directory\ to the path. This will require your root password. -sudo launchctl config user path /usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin +sudo launchctl config user path /usr/bin:/bin:/usr/sbin:/sbin:"$BREW_PREFIX/bin":/usr/local/bin cp $PWD/nvim/scheme.vim.template $PWD/nvim/scheme.vim cp $PWD/.config/alacritty/schemes.yml.template $PWD/.config/alacritty/schemes.yml ./clone_and_link_mac.sh diff --git a/update_mac.sh b/update_mac.sh index 147678f..aece0bf 100755 --- a/update_mac.sh +++ b/update_mac.sh @@ -1,5 +1,13 @@ #!/bin/bash +SCRIPT_SOURCE="${BASH_SOURCE[0]}" +while [[ -L "$SCRIPT_SOURCE" ]]; do + SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_SOURCE")" >/dev/null 2>&1 && pwd)" + SCRIPT_SOURCE="$(readlink "$SCRIPT_SOURCE")" + [[ "$SCRIPT_SOURCE" != /* ]] && SCRIPT_SOURCE="$SCRIPT_DIR/$SCRIPT_SOURCE" +done +SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_SOURCE")" >/dev/null 2>&1 && pwd)" + ARCH=`uname -m` if [[ $ARCH == "arm64" ]]; then PREFIX="/opt/homebrew/" @@ -55,6 +63,9 @@ function update_others { echo "Update other stuff" echo "----------" + echo "-- ALACRITTY" + "$SCRIPT_DIR/install_alacritty_mac.sh" || echo "Alacritty update failed." >&2 + if [ -x "$(command -v claude)" ]; then echo "-- CLAUDE" claude --update