From 4c9038c33d3635bd951e16490ec25dd0a3514646 Mon Sep 17 00:00:00 2001 From: David Tomaschik Date: Tue, 10 Feb 2026 10:50:54 -0800 Subject: [PATCH] Refactor --- bin/install_ansible.sh | 137 ++++++++++++++++++ dotfiles/aliases | 2 +- .../startup/05-venv-manager.py | 57 ++++++++ install.sh | 91 +++++------- 4 files changed, 234 insertions(+), 53 deletions(-) create mode 100755 bin/install_ansible.sh create mode 100644 dotfiles/ipython/profile_default/startup/05-venv-manager.py diff --git a/bin/install_ansible.sh b/bin/install_ansible.sh new file mode 100755 index 0000000..e207bfb --- /dev/null +++ b/bin/install_ansible.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# +# Installs Ansible, trying user-space methods first before falling back to sudo. +# This script is designed to be idempotent and safe to run multiple times. + +set -e # Exit immediately if a command exits with a non-zero status. + +# --- Helper Functions --- +info() { echo "[INFO] $1"; } +warn() { echo "[WARN] $1"; } +error() { echo "[ERROR] $1" >&2; exit 1; } + +# --- Main Logic --- + +# 1. Check if Ansible is already installed +if command -v ansible >/dev/null 2>&1; then + info "Ansible is already installed at $(command -v ansible)." + exit 0 +fi +info "Ansible not found. Attempting installation..." + +# 2. Try user-space installation (no sudo) +info "--- Attempting user-space installation (no sudo required) ---" + +# Try pipx first, as it's the cleanest user-space method +if command -v pipx >/dev/null 2>&1; then + info "Found pipx. Trying to install Ansible with it..." + if pipx install ansible; + then + # pipx requires adding ~/.local/bin to PATH, which might not be sourced yet. + # Check the executable directly. + if [[ -x "${HOME}/.local/bin/ansible" ]]; then + info "Ansible installed successfully with pipx." + info "Please ensure '${HOME}/.local/bin' is in your PATH." + info "You may need to restart your shell or run: export PATH=\"$HOME/.local/bin:$PATH\"" + exit 0 + else + warn "pipx install seemed to succeed, but ansible executable not found where expected." + fi + else + warn "pipx install ansible failed." + fi +fi + +# Try Python's venv module if pipx failed or wasn't present +VENV_PATH="${HOME}/.local/share/ansible_venv" +# Create a temp path to avoid clobbering a failed install +VENV_TEST_PATH="/tmp/ansible_venv_test_$$" +if python3 -m venv "${VENV_TEST_PATH}" >/dev/null 2>&1; then + rm -rf "${VENV_TEST_PATH}" # Clean up test + info "Python's venv module is available. Creating a virtual environment at ${VENV_PATH}..." + python3 -m venv "${VENV_PATH}" + if "${VENV_PATH}/bin/pip" install --quiet ansible; + then + info "Ansible installed successfully into a virtual environment." + info "To use it, run: '${VENV_PATH}/bin/ansible'" + info "To make it available everywhere, add its bin directory to your PATH:" + info " echo 'export PATH="${VENV_PATH}/bin:$PATH"' >> ~/.profile" + info "(You may need to source ~/.profile or restart your shell)." + exit 0 + else + warn "Failed to install ansible into the virtual environment." + rm -rf "${VENV_PATH}" # Clean up failed attempt + fi +else + info "Python's venv module not available or failed to create a test environment." +fi + +# 3. Fallback to sudo installation +info "--- User-space installation failed. Falling back to system-wide installation (sudo required) ---" + +if ! command -v sudo >/dev/null 2>&1; then + error "sudo command not found. Cannot attempt system-wide installation. Aborting." +fi + +# Prompt for sudo password upfront so it doesn't happen in the middle of the script +info "Sudo privileges are required. You may be prompted for your password." +if ! sudo -v; then + error "Failed to acquire sudo privileges. Aborting." +fi + +# Detect package manager and install +if command -v apt-get >/dev/null 2>&1; then + info "Detected Debian-based system (apt)." + sudo apt-get update -y + info "Attempting to install 'ansible' package..." + if sudo apt-get install -y ansible; + then + info "System package 'ansible' installed successfully." + else + warn "Failed to install 'ansible' package directly. Trying to install prerequisites for user-space install..." + if sudo apt-get install -y pipx; + then + info "Installed pipx. Attempting to install Ansible with it..." + if pipx install ansible; + then + info "Ansible installed successfully with pipx." + info "Please ensure '${HOME}/.local/bin' is in your PATH." + exit 0 + fi + else + error "Failed to install 'ansible' or 'pipx' via apt. Aborting." + fi + fi +elif command -v dnf >/dev/null 2>&1; then + info "Detected Red Hat-based system (dnf)." + info "Attempting to install 'ansible-core' package..." + if ! sudo dnf install -y ansible-core; + then + error "Failed to install ansible-core via dnf. Aborting." + fi +elif command -v pacman >/dev/null 2>&1; then + info "Detected Arch-based system (pacman)." + info "Attempting to install 'ansible' package..." + if ! sudo pacman -Syu --noconfirm ansible; + then + error "Failed to install ansible via pacman. Aborting." + fi +elif command -v brew >/dev/null 2>&1; then + info "Detected macOS (brew)." + info "Attempting to install 'ansible' package..." + if ! brew install ansible; + then + error "Failed to install ansible via brew. Aborting." + fi +else + error "Could not detect a known package manager (apt, dnf, pacman, brew). Aborting." +fi + +# 4. Final verification +info "--- Verifying final installation ---" +if command -v ansible >/dev/null 2>&1; then + info "Ansible successfully installed at $(command -v ansible)." + exit 0 +else + error "Installation attempted but the 'ansible' command is still not available. Please check the output for errors." +fi diff --git a/dotfiles/aliases b/dotfiles/aliases index 6fbceb9..bd6bb7e 100755 --- a/dotfiles/aliases +++ b/dotfiles/aliases @@ -47,7 +47,7 @@ alias cdgr='cd $(git rev-parse --show-toplevel || echo .)' alias sshanon="ssh -oUserKnownHostsFile=/dev/null -oStrictHostKeyChecking=no" # Straight to ipython -alias ipy="ipython3" +alias ipy="ipython3 --no-banner" # Skip the header on bc alias bc="command bc -q" diff --git a/dotfiles/ipython/profile_default/startup/05-venv-manager.py b/dotfiles/ipython/profile_default/startup/05-venv-manager.py new file mode 100644 index 0000000..317b94b --- /dev/null +++ b/dotfiles/ipython/profile_default/startup/05-venv-manager.py @@ -0,0 +1,57 @@ +import os +import sys +from IPython import prompts + +def _setup_environment(): + ip = get_ipython() + if not ip: + return + + # 1. SEARCH LOGIC: Walk up the tree for a venv + active_venv_name = None + curr = os.getcwd() + + while True: + for venv_name in ['.venv', 'env', 'venv']: + venv_path = os.path.join(curr, venv_name) + if os.path.isdir(venv_path): + # Platform-specific site-packages path + py_ver = f"python{sys.version_info.major}.{sys.version_info.minor}" + site_pkgs = os.path.join(venv_path, 'lib', py_ver, 'site-packages') + + if os.path.exists(site_pkgs): + if site_pkgs not in sys.path: + sys.path.insert(0, site_pkgs) + sys.prefix = venv_path + active_venv_name = os.path.basename(venv_path) + break + + if active_venv_name or os.path.exists(os.path.join(curr, '.git')): + break + + parent = os.path.dirname(curr) + if parent == curr: + break + curr = parent + + # 2. PROMPT LOGIC: Inject venv name into the UI + class VenvPrompts(prompts.Prompts): + def in_prompt_tokens(self): + tokens = [] + if active_venv_name: + tokens.append((prompts.Token.Prompt, f'({active_venv_name}) ')) + + tokens.extend([ + (prompts.Token.Prompt, 'In ['), + (prompts.Token.PromptNum, str(self.shell.execution_count)), + (prompts.Token.Prompt, ']: '), + ]) + return tokens + + ip.prompts = VenvPrompts(ip) + +# Execute the setup +_setup_environment() + +# cleanup +del _setup_environment, prompts diff --git a/install.sh b/install.sh index 981ae97..4a83ddc 100755 --- a/install.sh +++ b/install.sh @@ -6,7 +6,6 @@ set -o nounset set -o errexit set -o shwordsplit 2>/dev/null || true # Make zsh behave like bash -USER=${USER:-$(id -un)} HOME=${HOME:-$(cd ~ && pwd)} case $(uname) in @@ -27,11 +26,12 @@ have_command() { } prerequisites() { + local USER=${USER:-$(id -un)} if have_command zsh ; then case $- in *i*) local shell_path - if [ "$(uname)" = "Darwin" ]; then + if [[ "$(uname)" = "Darwin" ]]; then # dscl output is "UserShell: /bin/zsh" shell_path="$(dscl . -read "/Users/${USER}" UserShell | awk '{print $2}')" else @@ -62,7 +62,6 @@ install_dotfile_dir() { done)" # shellcheck disable=SC2086 find "${SRCDIR}" \( -name .git -o \ - -path "${SRCDIR}/private_dotfiles" -o \ -name install.sh -o \ -name README.md -o \ -name .gitignore \ @@ -79,11 +78,11 @@ install_dotfile_dir() { local FULLNAME="${BASEDIR}/${submodule}" local TARGET="${HOME}/.${FULLNAME#"${SRCDIR}"/}" mkdir -p "$(dirname "${TARGET}")" - if test -L "${TARGET}" ; then - if [ "$(readlink "${TARGET}")" != "${FULLNAME}" ] ; then + if [[ -L "${TARGET}" ]] ; then + if [[ "$(readlink "${TARGET}")" != "${FULLNAME}" ]] ; then echo "${TARGET} points to $(readlink "${TARGET}") not ${FULLNAME}!" >/dev/stderr fi - elif test -d "${TARGET}" ; then + elif [[ -d "${TARGET}" ]] ; then echo "rm -rf ${TARGET}" >/dev/stderr else ln -s -f "${FULLNAME}" "${TARGET}" @@ -106,13 +105,13 @@ install_basic_dir() { ssh_key_already_installed() { # Return 1 if the key isn't already installed, 0 if it is local AK="${HOME}/.ssh/authorized_keys" - if [ ! -f "$AK" ] ; then + if [[ ! -f "$AK" ]] ; then return 1 fi # Extract the key data (field 2) from the key file, ignoring comments local key_data key_data=$(awk '/^ssh-/ {print $2}' "$1") - if [ -z "${key_data}" ]; then + if [[ -z "${key_data}" ]]; then # Not a valid key file return 1 fi @@ -127,13 +126,13 @@ install_ssh_keys() { local AK="${HOME}/.ssh/authorized_keys" local key local keydir - if test "${TRUST_ALL_KEYS}" = 1 ; then + if [[ "${TRUST_ALL_KEYS}" = 1 ]] ; then keydir="${BASEDIR}/keys/ssh" else keydir="${BASEDIR}/keys/ssh/trusted" fi for key in "${keydir}"/* ; do - if [ ! -f "${key}" ] ; then + if [[ ! -f "${key}" ]] ; then continue fi if ssh_key_already_installed "${key}" ; then @@ -156,11 +155,11 @@ install_gpg_keys() { install_known_hosts() { verbose 'Installing known hosts...' >&2 - if [ ! -f "${BASEDIR}/keys/known_hosts" ] ; then + if [[ ! -f "${BASEDIR}/keys/known_hosts" ]] ; then return 0 fi mkdir -p "${HOME}/.ssh" - if [ -f "${HOME}/.ssh/known_hosts" ] ; then + if [[ -f "${HOME}/.ssh/known_hosts" ]] ; then local tmpf="$(mktemp)" cat "${BASEDIR}"/keys/known_hosts "${HOME}"/.ssh/known_hosts \ | sort -u > "$tmpf" @@ -180,21 +179,21 @@ setup_git_email() { local gc_local="${HOME}/.gitconfig.local" local current_email="" - if [ -f "${gc_local}" ]; then + if [[ -f "${gc_local}" ]]; then current_email=$(git config -f "${gc_local}" user.email || true) fi - if [ -n "${GIT_EMAIL:-}" ]; then + if [[ -n "${GIT_EMAIL:-}" ]]; then # Use environment variable git config -f "${gc_local}" user.email "${GIT_EMAIL}" - elif [ -n "${current_email}" ]; then + elif [[ -n "${current_email}" ]]; then # Already has an email set GIT_EMAIL="${current_email}" else # Prompt the user echo -n "Enter git email (leave blank to skip): " >&2 read -r GIT_EMAIL || true - if [ -n "${GIT_EMAIL}" ]; then + if [[ -n "${GIT_EMAIL}" ]]; then git config -f "${gc_local}" user.email "${GIT_EMAIL}" fi fi @@ -204,7 +203,7 @@ setup_git_email() { read_saved_prefs() { # Can't use basedir here as we don't have it yet local pref_file="$(dirname "$0")/.installed-prefs" - if [ -f "${pref_file}" ] ; then + if [[ -f "${pref_file}" ]] ; then verbose "Loading saved skel preferences from ${pref_file}" # source is a bashism # shellcheck disable=SC1090 @@ -213,46 +212,38 @@ read_saved_prefs() { } save_prefs() { - test "$SAVE" = 1 || return 0 + [[ "$SAVE" = 1 ]] || return 0 local pref_file=${BASEDIR}/.installed-prefs - (echo_pref BASEDIR - echo_pref MINIMAL - echo_pref INSTALL_KEYS - echo_pref TRUST_ALL_KEYS - echo_pref VERBOSE) > "$pref_file" -} - -echo_pref() { - eval "local val=\${$1}" - # shellcheck disable=SC2154 - echo ": \${$1:=${val}}" + { + echo "BASEDIR=\"${BASEDIR}\"" + echo "MINIMAL=\"${MINIMAL}\"" + echo "INSTALL_KEYS=\"${INSTALL_KEYS}\"" + echo "TRUST_ALL_KEYS=\"${TRUST_ALL_KEYS}\"" + echo "VERBOSE=\"${VERBOSE}\"" + } > "$pref_file" } cleanup() { - if [ -x "${BASEDIR}/bin/prune-broken-symlinks.sh" ]; then + if [[ -x "${BASEDIR}/bin/prune-broken-symlinks.sh" ]]; then "${BASEDIR}/bin/prune-broken-symlinks.sh" -y "${HOME}/.zshrc.d" "${BASEDIR}/bin/prune-broken-symlinks.sh" -y "${HOME}/bin" fi } verbose() { - test "${VERBOSE:-0}" = 1 && echo "$@" >&2 || return 0 + [[ "${VERBOSE:-0}" = 1 ]] && echo "$@" >&2 || return 0 } # Operations install_dotfiles() { install_dotfile_dir "${BASEDIR}/dotfiles" - if test -d "${BASEDIR}/private_dotfiles" && \ - test -d "${BASEDIR}/.git/git-crypt" ; then - install_dotfile_dir "${BASEDIR}/private_dotfiles" - fi - if test -d "${BASEDIR}/local_dotfiles" ; then + if [[ -d "${BASEDIR}/local_dotfiles" ]] ; then install_dotfile_dir "${BASEDIR}/local_dotfiles" fi - if test -d "${BASEDIR}/dotfile_overlays" ; then + if [[ -d "${BASEDIR}/dotfile_overlays" ]] ; then for dotfiledir in "${BASEDIR}/dotfile_overlays/"* ; do - if test -d "${dotfiledir}" ; then + if [[ -d "${dotfiledir}" ]] ; then install_dotfile_dir "${dotfiledir}" fi done @@ -260,21 +251,21 @@ install_dotfiles() { } install_main() { - if test -d "${BASEDIR}/.git" && have_command git ; then - if [ -z "$(git -C "${BASEDIR}" status --porcelain)" ]; then + if [[ -d "${BASEDIR}/.git" && have_command git ]] ; then + if [[ -z "$(git -C "${BASEDIR}" status --porcelain)" ]]; then git -C "${BASEDIR}" pull --ff-only || true - test "$MINIMAL" = 1 || \ + [[ "$MINIMAL" = 1 ]] || \ git -C "${BASEDIR}" submodule update --init --recursive --depth 1 else echo "Skipping self-update: repository has local changes." >&2 fi fi - test "$MINIMAL" = 1 || { + [[ "$MINIMAL" = 1 ]] || { prerequisites # try to update dotfile overlays - if test -d "${BASEDIR}/dotfile_overlays" ; then + if [[ -d "${BASEDIR}/dotfile_overlays" ]] ; then for dotfiledir in "${BASEDIR}/dotfile_overlays/"* ; do - if test -d "${dotfiledir}/.git" ; then + if [[ -d "${dotfiledir}/.git" ]] ; then git -C "${dotfiledir}" pull --ff-only || true git -C "${dotfiledir}" submodule update --init --recursive --depth 1 || true fi @@ -283,7 +274,7 @@ install_main() { } install_dotfiles install_basic_dir "${BASEDIR}/bin" "${HOME}/bin" - test "$INSTALL_KEYS" = 1 && install_keys + [[ "$INSTALL_KEYS" = 1 ]] && install_keys save_prefs setup_git_email cleanup @@ -293,8 +284,8 @@ install_vim_extra() { local DEST="${HOME}/.vim/pack/matir-extra" local REPO="https://github.com/Matir/vim-extra.git" - if test -d "${DEST}" ; then - if test -d "${DEST}/.git" ; then + if [[ -d "${DEST}" ]] ; then + if [[ -d "${DEST}/.git" ]] ; then # do update git -C "${DEST}" pull --ff-only git -C "${DEST}" submodule update --init @@ -321,7 +312,7 @@ read_saved_prefs : ${SAVE:=1} # Check prerequisites -if [ ! -d "$BASEDIR" ] ; then +if [[ ! -d "$BASEDIR" ]] ; then echo "Please install to $BASEDIR!" 1>&2 exit 1 fi @@ -335,10 +326,6 @@ case $OPERATION in dotfiles) install_dotfiles ;; - test) - # Do nothing, just sourcing - set +o errexit - ;; vim-extra) # Install/update extra vim modules install_vim_extra