From 963f480ff61709d02e0aee630213a20bbe22702f Mon Sep 17 00:00:00 2001 From: Marley Date: Sun, 21 Jan 2024 20:49:34 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20feat:=20Initial=20bootstrapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- git/gitconfig.local.symlink.example | 8 + homebrew/brew.sh | 0 os/pref.sh | 0 script/dot.sh | 344 ++++++++++++++++++++++++++++ script/utils.sh | 247 ++++++++++++++++++++ 5 files changed, 599 insertions(+) create mode 100644 git/gitconfig.local.symlink.example create mode 100644 homebrew/brew.sh create mode 100644 os/pref.sh create mode 100644 script/dot.sh create mode 100644 script/utils.sh diff --git a/git/gitconfig.local.symlink.example b/git/gitconfig.local.symlink.example new file mode 100644 index 0000000..540d13f --- /dev/null +++ b/git/gitconfig.local.symlink.example @@ -0,0 +1,8 @@ +# vim:set ft=toml sw=4: + +[user] + name = AUTHORNAME + email = AUTHOREMAIL + +[credential] + helper = GIT_CREDENTIAL_HELPER diff --git a/homebrew/brew.sh b/homebrew/brew.sh new file mode 100644 index 0000000..e69de29 diff --git a/os/pref.sh b/os/pref.sh new file mode 100644 index 0000000..e69de29 diff --git a/script/dot.sh b/script/dot.sh new file mode 100644 index 0000000..11b5282 --- /dev/null +++ b/script/dot.sh @@ -0,0 +1,344 @@ +#!/usr/bin/env bash +# vim:set ft=bash: + +declare -r GITHUB_REPO="punkfairie/dotfiles" + +declare -r DOTFILES_ORIGIN="git@github.com:$GITHUB_REPO.git" +declare -r DOTFILES_TARBALL="https://github.com/$GITHUB_REPO/tarball/main" +declare -r DOTFILES_UTILS="https://raw.githubusercontent.com/$GITHUB_REPO/main/scripts/utils.sh" + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +declare dotfiles_dir="$HOME/dotfiles" +declare yes_to_all=false + +################################################################################ +# Download Dotfiles # +################################################################################ + +download() +{ + local url="$1" + local output="$2" + + if command -v "curl" &> /dev/null; then + curl \ + --location \ + --silent \ + --show-error \ + --output "$output" \ + "$url" \ + &> /dev/null + + return $? + + elif command -v "wget" &> /dev/null; then + wget \ + --quiet \ + --output-document="$output" \ + "$url" \ + &> /dev/null + + return $? + fi + + return 1 +} + +download_utils() +{ + local tmp_file="" + + tmp_file="$(mktemp /tmp/XXXXX)" + + download "$DOTFILES_UTILS" "$tmp_file" \ + && . "$tmp_file" \ + && rm -rf "$tmp_file" \ + && return 0 + + return 1 +} + +extract() +{ + local archive="$1" + local output_dir="$2" + + if command -v "tar" &> /dev/null; then + tar \ + --extract \ + --gzip \ + --file "$archive" \ + --strip-components 1 \ + --directory "$output_dir" + + return $? + fi + + return 1 +} + +download_dotfiles() +{ + local tmp_file="" + + print_title "Download and extract dotfiles archive" + + tmp_file="$(mktemp /tmp/XXXXX)" + + download "$DOTFILES_TARBALL" "$tmp_file" + print_result $? "Download archive" "true" + printf "\n" + + if ! $yes_to_all; then + + while [[ -e $dotfiles_dir ]]; do + ask "'$dotfiles_dir' already exists, do you want to (o)verwrite or (b)ackup the existing directory?" + answer="$(get_answer)" + + case $answer in + o ) rm -rf "$dotfiles_dir"; break;; + b ) mv "$dotfiles_dir" "$dotfiles_dir.bak"; break;; + * ) print_warning "Please enter a valid option." + esac + + done + + else + rm -rf "$dotfiles_dir" &> /dev/null + fi + + mkdir -p "$dotfiles_dir" + print_result $? "Create '$dotfiles_dir'" "true" + + # Extract archive. + extract "$tmp_file" "$dotfiles_dir" + print_result $? "Extract archive" "true" + + rm -rf "$tmp_file" + print_result $? "Remove archive" + + cd "$dotfiles_dir/script" \ + || return 1 +} + +################################################################################ +# Setup Gitconfig # +################################################################################ + +setup_gitconfig() +{ + cd "$dotfiles_dir" + + if ! [[ -f $dotfiles_dir/git/gitconfig.local.symlink ]]; then + print_title "Set up gitconfig" + + git_credential="cache" + + if [[ "$(uname)" == "Darwin" ]]; then + git_credential="osxkeychain" + fi + + print_question "What is your Github author name?" + read -e git_authorname + + print_question "What is your Github author email?" + read -e git_authoremail + + sed -e "s/AUTHORNAME/$git_authorname/g" \ + -e "s/AUTHOREMAIL/$git_authoremail/g" \ + -e "s/GIT_CREDENTIAL_HELPER/$git_credential/g" \ + $dotfiles_dir/git/gitconfig.local.symlink.example > $dotfiles_dir/gitconfig.local.symlink + + print_result $? "gitconfig" + fi +} + +################################################################################ +# Install Dotfiles # +################################################################################ + +link_file() +{ + local src=$1 + local dst=$2 + + local overwrite= + local backup= + local skip= + local action= + + if [[ -f "$dst" -o -d "$dst" -o -L "$dst" ]]; then + + if ! $overwrite_all && ! $backup_all && ! $skip_all; then + local current_src="$(readlink $dst)" + + if [[ "$current_src" == "$src" ]]; then + skip=true + + else + print_question "File already exists: $dst ($(basename "$src")), what do you want to do?\n\ + [s]kip, [S]kip all, [o]verwrite, [O]verwrite all, [b]ackup, [B]ackup all?" + read -n 1 action + + case "$action" in + o ) overwrite=true ;; + O ) overwrite_all=true ;; + b ) backup=true ;; + B ) backup_all=true ;; + s ) skip=true ;; + S ) skip_all=true ;; + * ) ;; + esac + fi + + fi + + overwrite=${overwrite:-$overwrite_all} + backup=${backup:-$backup_all} + skip=${skip:-$skip_all} + + if $overwrite; then + rm -rf "$dst" + print_success "Removed $dst" + fi + + if $backup; then + mv "$dst" "${dst}.bak" + print_success "Moved $dst to ${dst}.bak" + fi + + if $skip; then + print_success "Skipped $src" + fi + + fi + + if ! $skip; then + ln -s "$src" "$dst" + print_success "Linked $src to $dst" + fi +} + +install_dotfiles() +{ + print_title "Installing dotfiles" + + local overwrite_all=false + local backup_all=false + local skip_all=false + + for src in $(find -H "$dotfiles_dir" -maxdepth 2 -name "*.symlink" -not -path "*.git*"); do + dst="$HOME/$(basename "${src%.*}")" + link_file "$src" "$dst" + done +} + +################################################################################ +# Initialize Git Repo # +################################################################################ + +git_init() +{ + print_title "Initialize Git repository" + + if [[ -z "$DOTFILES_ORIGIN" ]]; then + print_error "Please provide a URL for the Git origin" + return 1 + fi + + if ! is_git_repository; then + cd $dotfiles_dir || print_error "Failed to cd $dotfiles_dir" + + execute \ + "git init && git remote add origin $DOTFILES_ORIGIN" \ + "Initialize the dotfiles Git repository" + fi +} + +################################################################################ +# Restart OS # +################################################################################ + +restart_os() +{ + print_title "Restart" + + ask_for_confirmation "Do you want to restart?" + printf "\n" + + if answer_is_yes; then + sudo shutdown -r now &> /dev/null + fi +} + +################################################################################ +# Main # +################################################################################ + +main() +{ + if [[ "$(uname)" != "Linux" ]] && [[ "$(uname)" != "Darwin" ]]; then + printf "Sorry, this script is intended only for macOS and Ubuntu!" + return 1 + fi + + # Load utils. + + if [[ -x "${dotfiles_dir}/script/utils.sh" ]]; then + . "${dotfiles_dir}/script/utils.sh" || exit 1 + else + download_utils || exit 1 + fi + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + skip_questions "$@" \ + && yes_to_all=true + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + ask_for_sudo + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + # Check if this script was run directly, and if not, dotfiles will need to be + # downloaded. + + printf "%s" "${BASH_SOURCE[0]}" | grep "dot.sh" &> /dev/null \ + || download_dotfiles + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + setup_gitconfig + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + install_dotfiles + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + $dotfiles_dir/os/pref.sh + + $dotfiles_dir/homebrew/brew.sh + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + find . -name install.sh | while read installer ; do sh -c "${installer}" ; done + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + if cmd_exists "git"; then + if [[ "$(git config --get remote.origin.url)" != "$DOTFILES_ORIGIN" ]]; then + git_init + fi + fi + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + if ! $yes_to_all; then + restart_os + fi +} + +main "$@" diff --git a/script/utils.sh b/script/utils.sh new file mode 100644 index 0000000..4d3ec53 --- /dev/null +++ b/script/utils.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +# vim:set ft=bash: + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +skip_questions() +{ + while :; do + case $1 in + -y | --yes ) return 0 ;; + * ) break ;; + esac + + shift 1 + done + + return 1 +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +ask_for_sudo() +{ + sudo -v &> /dev/null + + # Update existing 'sudo' timestamp until this script has finished. + # + # https://gist.github.com/cowboy/3118588 + + while true; do + sudo -n true + sleep 60 + kill -0 "$$" || exit + done &> /dev/null & +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +print_in_color() +{ + string=$(echo "$1" | tr -s " ") + + printf "%b" \ + "$(tput setaf "$2" 2> /dev/null)" \ + "$string" \ + "$(tput sgr0 2> /dev/null)" +} + +print_in_red() +{ + print_in_color "$1" 1 +} + +print_in_yellow() +{ + print_in_color "$1" 3 +} + +print_in_green() +{ + print_in_color "$1" 2 +} + +print_in_purple() +{ + print_in_color "$1" 5 +} + +print_title() +{ + print_in_purple "\n • $1\n\n" +} + +print_success() +{ + print_in_green " [✔] $1\n" +} + +print_warning() +{ + print_in_yellow " [!] $1\n" +} + +print_error() +{ + print_in_red " [✖] $1 $2\n" +} + +print_question() +{ + print_in_yellow " [?] $1\n" +} + +print_result() +{ + if [[ "$1" == 0 ]]; then + print_success "$2" + else + print_error "$2" + fi + + return "$1" +} + +print_error_stream() +{ + while read -r line; do + print_error "↳ ERROR: $line" + done +} +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +ask() +{ + print_question "$1" + read -r +} + +get_answer() +{ + printf "%s" "$REPLY" +} + +ask_for_confirmation() +{ + print_question "$1 (y/n) " + read -r -n 1 + printf "\n" +} + +answer_is_yes() +{ + [[ "$REPLY" =~ ^[Yy]$ ]] \ + && return 0 \ + || return 1 +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +show_spinner() +{ + local -r FRAMES='/-\|' + + # shellcheck disable=SC2034 + local -r NUMBER_OF_FRAMES=${#FRAMES} + + local -r CMDS="$2" + local -r MSG="$3" + local -r PID="$1" + + local i=0 + local frame_text="" + + # Provide more space so that the text hopefully doesn't reach the bottom line + # of the terminal window. + # + # This is a workaround for escape sequences not tracking the buffer position + # (accounting for scrolling). + # + # See also: https://unix.stackexchange.com/a/278888 + + printf "\n\n\n" + tput cuu 3 + tput sc + + while kill -0 "$PID" &> /dev/null; do + frame_text=" [${FRAMES:i++%NUMBER_OF_FRAMES:1}] $MSG" + + # Print frame text. + printf "%s" "$frame_text" + + sleep 0.2 + + # Clear frame text. + tput rc + done +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +cmd_exists() +{ + command -v "$1" &> /dev/null +} + +is_git_repository() +{ + git rev-parse &> /dev/null +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +set_trap() +{ + trap -p "$1" | grep "$2" &> /dev/null \ + || trap '$2' "$1" +} + +kill_all_subproccesses() +{ + local i="" + + for i in $(jobs -p); do + kill "$i" + wait "$i" &> /dev/null + done +} + +execute() +{ + local -r CMDS="$1" + local -r MSG="${2:-$1}" + local -r TMP_FILE="$(mktemp /tmp/XXXXX)" + + local exit_code=0 + local cmds_pid="" + + # If the current process is ended, also end all its subproccesses. + set_trap "EXIT" "kill_all_subproccesses" + + # Execute commands in background + # shellcheck disable=SC2261 + eval "$CMDS" \ + &> /dev/null \ + 2> "$TMP_FILE" & + + cmds_pid=$! + + # Show a spinner if the commands require more time to complete. + show_spinner "$cmds_pid" "$CMDS" "$MSG" + + # Wait for the commands to no longer be executing in the background, and then + # get their exit code. + wait "$cmds_pid" &> /dev/null + exit_code=$? + + # Print output based on what happened. + print_result $exit_code "$MSG" + + if [[ $exit -ne 0 ]]; then + print_error_stream < "$TMP_FILE" + fi + + rm -rf "$TMP_FILE" + + return $exit_code +}