diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index 6d316f4..3f0c5f6 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -164,3 +164,99 @@ jobs: su -s /bin/bash -c \ 'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; export TERM=xterm-256color; cd ~/dotfiles && bash test.sh nosudo' \ user + + install-nosudo-alma: + name: "nosudo-${{ matrix.variant }} @ AlmaLinux ${{ matrix.alma }}" + runs-on: ubuntu-latest + timeout-minutes: 40 + container: + image: "almalinux:${{ matrix.alma }}" + + strategy: + fail-fast: false + matrix: + alma: ["9", "10"] + variant: + - auto # user has NO sudo binary; detect_sudo() auto-detects CAN_SUDO=false + - forced # user HAS passwordless sudo but NOSUDO=1 overrides it + - nonsudoer # sudo binary present but user NOT in sudoers; auto-detects CAN_SUDO=false + + steps: + - name: Bootstrap prerequisites (as root - mirrors a shared RHEL host) + # --allowerasing: EL9+ images ship curl-minimal, which conflicts with + # the full curl package - allow dnf to swap it out. + run: | + dnf -yq install --allowerasing git curl wget ca-certificates zsh tmux python3 \ + tar gzip xz findutils shadow-utils glibc-langpack-en + + - name: Install sudo binary (forced + nonsudoer) + if: matrix.variant == 'forced' || matrix.variant == 'nonsudoer' + run: dnf -yq install sudo + + - name: Grant passwordless sudo to user (forced only) + # nonsudoer deliberately leaves the user OUT of sudoers so detect_sudo() + # must auto-detect CAN_SUDO=false against a present-but-unauthorised sudo. + if: matrix.variant == 'forced' + run: echo 'user ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + + - name: Create non-root user + run: useradd -m -s /bin/bash user + + - name: Checkout dotfiles + uses: actions/checkout@v4 + + - name: Copy dotfiles to user home + run: | + cp -r . /home/user/dotfiles + chown -R user:user /home/user/dotfiles + + - name: Configure curl auth (avoids GitHub API rate limit) + run: | + printf 'header = "Authorization: Bearer %s"\n' "${{ secrets.GITHUB_TOKEN }}" \ + > /home/user/.curlrc + chown user:user /home/user/.curlrc + chmod 600 /home/user/.curlrc + + - name: Install (${{ matrix.variant }}) + env: + VARIANT: ${{ matrix.variant }} + run: | + if [ "$VARIANT" = "forced" ]; then + install_cmd="NOSUDO=1 bash install.sh minimal" + else + install_cmd="bash install.sh minimal" + fi + su -s /bin/bash -c \ + "HOME=/home/user; export HOME; export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && $install_cmd" \ + user + + - name: Run test suite (nosudo profile) + run: | + su -s /bin/bash -c \ + 'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; export TERM=xterm-256color; cd ~/dotfiles && bash test.sh nosudo' \ + user + + - name: Idempotency - re-run install.sh + env: + VARIANT: ${{ matrix.variant }} + run: | + if [ "$VARIANT" = "forced" ]; then + install_cmd="NOSUDO=1 bash install.sh minimal" + else + install_cmd="bash install.sh minimal" + fi + su -s /bin/bash -c \ + "HOME=/home/user; export HOME; export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && $install_cmd" \ + user + + - name: Run update.sh + run: | + su -s /bin/bash -c \ + 'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; cd ~/dotfiles && bash update.sh' \ + user + + - name: Re-run test suite after update + run: | + su -s /bin/bash -c \ + 'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; export TERM=xterm-256color; cd ~/dotfiles && bash test.sh nosudo' \ + user diff --git a/AGENTS.md b/AGENTS.md index d1998a3..9e92775 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,10 +4,11 @@ This file provides codebase context for AI coding agents (Codex, Copilot, etc.). ## Repository purpose -Personal dotfiles for Ubuntu/Debian: one-command install (`install.sh`), managed -updates (`update.sh`), post-install test suite (`test.sh`), and CI matrix covering -3 Ubuntu versions × 3 install profiles + 3 no-sudo variants (auto / forced / -nonsudoer) - 18 cells total. +Personal dotfiles for Ubuntu/Debian, with RHEL-family (AlmaLinux/Rocky/Fedora) +support via the user-local binary path: one-command install (`install.sh`), +managed updates (`update.sh`), post-install test suite (`test.sh`), and CI +matrix covering 3 Ubuntu versions × 3 install profiles + no-sudo variants +(auto / forced / nonsudoer) on 3 Ubuntu + 2 AlmaLinux versions - 24 cells total. ## Key files @@ -23,6 +24,11 @@ nonsudoer) - 18 cells total. ## Critical rules (never violate) +- Gate every apt code path on `$CAN_APT` (sudo AND apt-get present, set by + `detect_sudo`), never on `$CAN_SUDO` alone - on RHEL-family systems sudo can be + available while apt is not, and a bare `apt-get` call dies under `set -e`. + Applies to the install/update flow; the optional `scripts/` desktop helpers + are Ubuntu-only. - All scripts use `set -euo pipefail`. Use `count=$(( count + 1 ))` - never `(( count++ ))`. Also avoid `[ cond ] && var=true` - when the condition is false, the expression exits with 1 and trips `set -e`. Use `if [ cond ]; then var=true; fi` instead. diff --git a/CHANGELOG.md b/CHANGELOG.md index cb882e3..7d53aef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- RHEL-family support (AlmaLinux, Rocky, CentOS Stream, Fedora), including + no-sudo accounts: a new `CAN_APT` global (sudo AND apt-get present, set by + `detect_sudo`) gates every apt code path in the install/update flow + (`install.sh`, `update.sh`, `modules/`; the optional `scripts/` desktop + helpers remain Ubuntu-only), so non-apt systems take the + user-local binary path (`~/.local/bin`) with or without sudo instead of + dying on a missing `apt-get` under `set -e`. This includes neovim: on + non-apt systems `install.sh` and `update.sh` place it under `~/.local` + even when sudo is available (`/usr/local` remains the destination on apt + systems with sudo). Prerequisite hints are now + package-manager-aware (`_pkg_install_hint`: apt/dnf/yum), `get.sh` bootstraps + git/curl via dnf/yum when run as root on RHEL, and `_check_os`/`get.sh` + recognise RHEL-family IDs instead of warning "not Ubuntu/Debian". +- CI coverage for the RHEL path: `Dockerfile.nosudo` takes a `BASE` image arg + (Ubuntu or AlmaLinux, apt/dnf branch, `--allowerasing` for the EL9+ + curl-minimal conflict), `ci-local.sh` gains `--alma` and AlmaLinux 9/10 + no-sudo cells, and a new `install-nosudo-alma` GitHub Actions job runs all + three variants on AlmaLinux 9 and 10 (18 -> 24 cells). +- `ci-local.sh`: local runs now pass a GitHub token (from `GH_TOKEN` or + `gh auth token`) into builds via BuildKit secret and into runs via env, so + API-based installers no longer trip the unauthenticated 60-req/hr rate limit + (the documented false-failure mode of local no-sudo cells). - `zsh/.zshrc`: prepend `$HOME/go/bin` to `PATH` so `go install`-ed binaries are found (no-op when the directory doesn't exist). diff --git a/Dockerfile b/Dockerfile index bf70977..9e1d8a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ # Test dotfiles install in a clean Ubuntu environment. +# Requires BuildKit (Docker 20.10+ default) for the optional gh_token secret. # # Build & run (installs during build, then drops to shell): # docker build -t dotfiles-test:24.04-docker . @@ -29,7 +30,14 @@ RUN apt-get -yq update && \ WORKDIR /root/dotfiles COPY . . -RUN bash install.sh ${PROFILE} && \ - chsh -s /usr/bin/zsh root +# Optional gh_token secret -> ~/.curlrc auth header for the install only +# (avoids GitHub's unauthenticated API rate limit); removed after install. +RUN --mount=type=secret,id=gh_token,mode=0444,required=false \ + tok="$(cat /run/secrets/gh_token 2>/dev/null || true)"; \ + if [ -n "$tok" ]; then \ + printf 'header = "Authorization: Bearer %s"\n' "$tok" > ~/.curlrc; \ + fi; \ + bash install.sh ${PROFILE} && chsh -s /usr/bin/zsh root; \ + rc=$?; rm -f ~/.curlrc; exit $rc CMD ["zsh"] diff --git a/Dockerfile.nosudo b/Dockerfile.nosudo index 7e9f2cd..24313d3 100644 --- a/Dockerfile.nosudo +++ b/Dockerfile.nosudo @@ -1,9 +1,11 @@ # Dockerfile.nosudo +# Requires BuildKit (Docker 20.10+ default) for the optional gh_token secret. # # Covers three user-local install scenarios (all land binaries in ~/.local/bin): # # nosudo-auto - user has NO sudo; detect_sudo() auto-detects CAN_SUDO=false. -# Ubuntu base images ship without sudo, so no extra setup needed. +# Ubuntu/AlmaLinux base images ship without sudo, so no extra +# setup needed. # Build args: GRANT_SUDO=false (default), NOSUDO_INSTALL="" (default) # # nosudo-forced - user HAS passwordless sudo but NOSUDO=1 overrides it. @@ -17,25 +19,32 @@ # misclassified as sudo_password. # Build args: INSTALL_SUDO=true, GRANT_SUDO=false, NOSUDO_INSTALL="" # +# The BASE arg selects the distro (apt- and dnf-based images both work): +# ubuntu:24.04 (default), ubuntu:22.04, ubuntu:20.04, almalinux:9, almalinux:10 +# # Used automatically by ci-local.sh (runs all variants) or directly: # -# # nosudo-auto (default) -# docker build --build-arg UBUNTU=24.04 \ -# -t dotfiles-test:24.04-nosudo-auto -f Dockerfile.nosudo . +# # nosudo-auto on AlmaLinux 9 +# docker build --build-arg BASE=almalinux:9 \ +# -t dotfiles-test:alma9-nosudo-auto -f Dockerfile.nosudo . # -# # nosudo-forced -# docker build --build-arg UBUNTU=24.04 \ +# # nosudo-forced on Ubuntu 24.04 +# docker build --build-arg BASE=ubuntu:24.04 \ # --build-arg GRANT_SUDO=true --build-arg NOSUDO_INSTALL=1 \ # -t dotfiles-test:24.04-nosudo-forced -f Dockerfile.nosudo . # +# Optional: pass a GitHub token so the ~6 API-based installers don't hit the +# unauthenticated 60-req/hr rate limit (the known local-run failure mode): +# docker build --secret id=gh_token,src=/path/to/tokenfile ... +# # Then run the test suite (ci-local.sh does this automatically): # docker run --rm --user user -e TERM=xterm-256color \ # -e POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true \ # bash -c \ # 'export PATH="$HOME/.local/bin:$PATH"; cd ~/dotfiles && bash test.sh nosudo' -ARG UBUNTU=24.04 -FROM ubuntu:${UBUNTU} +ARG BASE=ubuntu:24.04 +FROM ${BASE} ENV DEBIAN_FRONTEND=noninteractive ENV TERM=xterm-256color @@ -59,10 +68,23 @@ ARG INSTALL_SUDO=false ARG NOSUDO_INSTALL= # ── Step 1-2: Base prerequisites (always installed) ─────────────────────────── -RUN apt-get -yq update && \ - apt-get -yq install --no-install-recommends \ - apt-utils git curl wget ca-certificates zsh tmux python3 && \ - rm -rf /var/lib/apt/lists/* +# Branch on the image's package manager: apt (Ubuntu/Debian) or dnf (AlmaLinux/ +# RHEL family). The dnf list adds what Ubuntu images already carry: tar/gzip/xz +# (binary extraction), findutils (_download_tar_bin uses find), shadow-utils +# (useradd below), glibc-langpack-en (en_US.UTF-8 locale data). +RUN if command -v apt-get >/dev/null 2>&1; then \ + apt-get -yq update && \ + apt-get -yq install --no-install-recommends \ + apt-utils git curl wget ca-certificates zsh tmux python3 && \ + rm -rf /var/lib/apt/lists/*; \ + else \ + # --allowerasing: EL9+ images ship curl-minimal, which conflicts with + # the full curl package - allow dnf to swap it out. + dnf -yq install --allowerasing \ + git curl wget ca-certificates zsh tmux python3 \ + tar gzip xz findutils shadow-utils glibc-langpack-en && \ + dnf clean all; \ + fi # ── Step 2b: Optionally install sudo ────────────────────────────────────────── # nosudo-auto intentionally omits the sudo binary so detect_sudo() auto-detects. @@ -70,9 +92,13 @@ RUN apt-get -yq update && \ # nosudo-nonsudoer installs sudo (INSTALL_SUDO=true) but withholds sudoers # membership below, so detect_sudo() must auto-detect CAN_SUDO=false. RUN if [ "${GRANT_SUDO}" = "true" ] || [ "${INSTALL_SUDO}" = "true" ]; then \ - apt-get -yq update && \ - apt-get -yq install --no-install-recommends sudo && \ - rm -rf /var/lib/apt/lists/*; \ + if command -v apt-get >/dev/null 2>&1; then \ + apt-get -yq update && \ + apt-get -yq install --no-install-recommends sudo && \ + rm -rf /var/lib/apt/lists/*; \ + else \ + dnf -yq install sudo && dnf clean all; \ + fi; \ fi # ── Step 3: Create non-root user; conditionally grant passwordless sudo ─────── @@ -94,6 +120,18 @@ WORKDIR /home/user # NOSUDO="${NOSUDO_INSTALL}" is "" for nosudo-auto (detect_sudo decides) # and "1" for nosudo-forced (overrides working sudo). # 'minimal' profile: zsh, tmux, git config + ~7 GitHub-tarball binaries. -RUN cd dotfiles && NOSUDO="${NOSUDO_INSTALL}" bash install.sh minimal +# The optional gh_token secret becomes a ~/.curlrc Authorization header for the +# install only (curl reads it automatically); removed afterwards so the image +# carries no credentials. +# mode=0444 (not uid=): Ubuntu 24.04 images ship an 'ubuntu' user at uid 1000, +# pushing 'user' to 1001 - a uid-pinned secret is then unreadable and the +# resulting empty Bearer header turns every API call into a 401. +RUN --mount=type=secret,id=gh_token,mode=0444,required=false \ + tok="$(cat /run/secrets/gh_token 2>/dev/null || true)"; \ + if [ -n "$tok" ]; then \ + printf 'header = "Authorization: Bearer %s"\n' "$tok" > ~/.curlrc; \ + fi; \ + cd dotfiles && NOSUDO="${NOSUDO_INSTALL}" bash install.sh minimal; \ + rc=$?; rm -f ~/.curlrc; exit $rc CMD ["bash"] diff --git a/README.md b/README.md index a72ef23..b3e7dc3 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,17 @@ curl -fsSL https://raw.githubusercontent.com/YASoftwareDev/dotfiles/master/get.s NOSUDO=1 ./install.sh workstation ``` +**RHEL family (AlmaLinux / Rocky / CentOS Stream / Fedora)** - supported via the +same user-local path: there is no apt, so tools are always fetched as prebuilt +binaries into `~/.local/bin`, with or without sudo. The `NOSUDO=1` override is +unnecessary (no-apt is auto-detected), and a plain user without sudo works out +of the box. The only requirement is that an administrator has preinstalled the +base prerequisites: + +```bash +sudo dnf install -y git curl zsh tmux python3 tar gzip xz findutils glibc-langpack-en +``` +
Other install methods diff --git a/ci-local.sh b/ci-local.sh index a9ad1ca..a1ae5b9 100755 --- a/ci-local.sh +++ b/ci-local.sh @@ -24,9 +24,13 @@ ALL_PROFILES=(docker minimal workstation) # auto: user has NO sudo binary; detect_sudo() auto-detects CAN_SUDO=false # nonsudoer: sudo binary present but user NOT in sudoers; detect_sudo() must # auto-detect CAN_SUDO=false via `sudo -n -v` (no override set) +# The nosudo matrix also covers the RHEL family (AlmaLinux) - there the +# binary-fetch path is the ONLY install path, sudo or not (no apt). ALL_NOSUDO_UBUNTU=(20.04 22.04 24.04) +ALL_NOSUDO_ALMA=(9 10) ALL_NOSUDO_VARIANTS=(forced auto nonsudoer) FILTER_UBUNTU=() +FILTER_ALMA=() FILTER_PROFILES=() NO_CACHE=false CLEAN=false @@ -47,6 +51,7 @@ Usage: bash ci-local.sh [OPTIONS] Options: --ubuntu VERSION Test only this Ubuntu version (repeatable) + --alma VERSION Test only this AlmaLinux version - nosudo cells only (repeatable) --profile PROFILE Test only this profile (repeatable) --no-cache Pass --no-cache to docker build --clean Remove ALL dotfiles-test:* images and dangling layers after @@ -55,6 +60,7 @@ Options: --help Show this help Valid Ubuntu versions : ${ALL_UBUNTU[*]} +Valid Alma versions : ${ALL_NOSUDO_ALMA[*]} (nosudo variants only - no apt profiles) Valid profiles : ${ALL_PROFILES[*]} Valid nosudo variants : ${ALL_NOSUDO_VARIANTS[*]} (or "nosudo" to run all) @@ -62,13 +68,16 @@ Valid nosudo variants : ${ALL_NOSUDO_VARIANTS[*]} (or "nosudo" to run all) nosudo-auto user has NO sudo binary; detect_sudo() auto-detects nosudo-nonsudoer sudo binary present but user NOT in sudoers; auto-detects +GitHub API rate limit: when GH_TOKEN is set (or \`gh auth token\` works), the +token is passed to builds/runs so API-based installers are not rate-limited. + Examples: bash ci-local.sh bash ci-local.sh --ubuntu 24.04 --profile minimal bash ci-local.sh --ubuntu 22.04 --ubuntu 24.04 --profile docker bash ci-local.sh --ubuntu 24.04 --profile nosudo # all variants + bash ci-local.sh --alma 9 --profile nosudo-auto # AlmaLinux 9, one variant bash ci-local.sh --ubuntu 24.04 --profile nosudo-forced # one variant - bash ci-local.sh --ubuntu 24.04 --profile nosudo-auto # one variant bash ci-local.sh --ubuntu 24.04 --profile nosudo-nonsudoer # one variant bash ci-local.sh --skip-nosudo EOF @@ -90,6 +99,9 @@ while [[ $# -gt 0 ]]; do --ubuntu) _validate_in "$2" "ubuntu version" "${ALL_UBUNTU[@]}" FILTER_UBUNTU+=("$2"); shift 2 ;; + --alma) + _validate_in "$2" "alma version" "${ALL_NOSUDO_ALMA[@]}" + FILTER_ALMA+=("$2"); shift 2 ;; --profile) case "$2" in nosudo) @@ -110,6 +122,11 @@ while [[ $# -gt 0 ]]; do esac done +if $SKIP_NOSUDO && $FILTER_NOSUDO; then + echo "Error: --skip-nosudo contradicts a requested nosudo profile." >&2 + exit 1 +fi + # NOTE: "${arr[@]:-default}" inside double quotes collapses to a single string # element when arr is empty - use explicit length checks instead. if [[ ${#FILTER_UBUNTU[@]} -eq 0 ]]; then @@ -123,6 +140,27 @@ else PROFILE_LIST=("${FILTER_PROFILES[@]}") fi +# Both Dockerfiles use RUN --mount=type=secret, which needs BuildKit +# (default since Docker 20.10; a user-set DOCKER_BUILDKIT=0 would break builds). +export DOCKER_BUILDKIT=1 + +# ── GitHub API auth (optional) ──────────────────────────────────────────────── +# Unauthenticated builds hit GitHub's 60-req/hr API cap: API-based installers +# (rg, fd, jq, zoxide, delta, eza) then fail with "could not find release URL - +# skipping" - a false failure. With a token, builds get a ~/.curlrc via BuildKit +# secret (never stored in image layers) and runs get it via GH_TOKEN env. +GH_TOKEN_VAL="${GH_TOKEN:-$(gh auth token 2>/dev/null || true)}" +GH_TOKEN_FILE="" +if [ -n "$GH_TOKEN_VAL" ]; then + GH_TOKEN_FILE=$(mktemp) + printf '%s' "$GH_TOKEN_VAL" > "$GH_TOKEN_FILE" + chmod 600 "$GH_TOKEN_FILE" + trap 'rm -f "$GH_TOKEN_FILE"' EXIT +fi +# Prepended to in-container commands: writes ~/.curlrc from the GH_TOKEN env +# var (single-quoted here - expands inside the container, not on the host). +_CURLRC_CMD='[ -n "${GH_TOKEN:-}" ] && printf "header = \"Authorization: Bearer %s\"\n" "$GH_TOKEN" > ~/.curlrc || true' + # ── Helpers ─────────────────────────────────────────────────────────────────── _check_docker() { if ! command -v docker &>/dev/null; then @@ -145,13 +183,15 @@ _build() { local ubuntu="$1" profile="$2" tag logfile tag=$(_tag "$ubuntu" "$profile") logfile="${LOG_DIR}/${ubuntu}-${profile}.log" - local -a cache_flag=() + local -a cache_flag=() secret_flag=() $NO_CACHE && cache_flag=(--no-cache) + [ -n "$GH_TOKEN_FILE" ] && secret_flag=(--secret "id=gh_token,src=${GH_TOKEN_FILE}") echo -e " ${BLUE}→${NC} building ${BOLD}${tag}${NC} ..." if docker build \ "${cache_flag[@]}" \ --build-arg "UBUNTU=${ubuntu}" \ --build-arg "PROFILE=${profile}" \ + "${secret_flag[@]}" \ -t "$tag" \ "$DOTFILES_DIR" \ >>"$logfile" 2>&1; then @@ -176,6 +216,7 @@ _run_step() { "${run_user_flag[@]}" \ -e TERM=xterm-256color \ -e POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true \ + -e GH_TOKEN="${GH_TOKEN_VAL:-}" \ "$tag" bash -c "$*" \ >>"$logfile" 2>&1; then echo -e " ${GREEN}✓${NC} ${label}" @@ -205,7 +246,7 @@ run_combination() { # 2. Idempotency - re-run install.sh inside built image _run_step "$tag" "idempotency (re-run install.sh)" "$logfile" \ - "cd /root/dotfiles && bash install.sh ${profile}" || return 1 + "${_CURLRC_CMD}; cd /root/dotfiles && bash install.sh ${profile}" || return 1 # 3. Test suite _run_step "$tag" "test.sh ${profile}" "$logfile" \ @@ -213,7 +254,7 @@ run_combination() { # 4. update.sh _run_step "$tag" "update.sh" "$logfile" \ - "cd /root/dotfiles && bash update.sh" || return 1 + "${_CURLRC_CMD}; cd /root/dotfiles && bash update.sh" || return 1 # 5. Test suite again after update _run_step "$tag" "test.sh after update" "$logfile" \ @@ -223,26 +264,30 @@ run_combination() { } # ── No-sudo runner (uses Dockerfile.nosudo, non-root user) ─────────────────── +# base_label: short tag/log identifier (e.g. "24.04", "alma9") +# base_image: docker image reference (e.g. "ubuntu:24.04", "almalinux:9") # variant: "forced" (has sudo, NOSUDO=1 override) # | "auto" (no sudo binary) # | "nonsudoer" (sudo binary present, user not in sudoers, no override) # Returns: 0=pass, 1=fail _build_nosudo() { - local ubuntu="$1" variant="$2" tag logfile - tag=$(_tag "$ubuntu" "nosudo-${variant}") - logfile="${LOG_DIR}/${ubuntu}-nosudo-${variant}.log" - local -a cache_flag=() extra_args=() + local base_label="$1" base_image="$2" variant="$3" tag logfile + tag=$(_tag "$base_label" "nosudo-${variant}") + logfile="${LOG_DIR}/${base_label}-nosudo-${variant}.log" + local -a cache_flag=() extra_args=() secret_flag=() $NO_CACHE && cache_flag=(--no-cache) + [ -n "$GH_TOKEN_FILE" ] && secret_flag=(--secret "id=gh_token,src=${GH_TOKEN_FILE}") case "$variant" in forced) extra_args=(--build-arg "GRANT_SUDO=true" --build-arg "NOSUDO_INSTALL=1") ;; auto) extra_args=(--build-arg "GRANT_SUDO=false" --build-arg "NOSUDO_INSTALL=") ;; nonsudoer) extra_args=(--build-arg "GRANT_SUDO=false" --build-arg "INSTALL_SUDO=true" --build-arg "NOSUDO_INSTALL=") ;; esac - echo -e " ${BLUE}→${NC} building ${BOLD}${tag}${NC} (Dockerfile.nosudo, variant=${variant}) ..." + echo -e " ${BLUE}→${NC} building ${BOLD}${tag}${NC} (Dockerfile.nosudo, base=${base_image}, variant=${variant}) ..." if docker build \ "${cache_flag[@]}" \ - --build-arg "UBUNTU=${ubuntu}" \ + --build-arg "BASE=${base_image}" \ "${extra_args[@]}" \ + "${secret_flag[@]}" \ -f "${DOTFILES_DIR}/Dockerfile.nosudo" \ -t "$tag" \ "$DOTFILES_DIR" \ @@ -256,10 +301,10 @@ _build_nosudo() { } run_nosudo() { - local ubuntu="$1" variant="$2" + local base_label="$1" base_image="$2" variant="$3" local tag logfile install_cmd - tag=$(_tag "$ubuntu" "nosudo-${variant}") - logfile="${LOG_DIR}/${ubuntu}-nosudo-${variant}.log" + tag=$(_tag "$base_label" "nosudo-${variant}") + logfile="${LOG_DIR}/${base_label}-nosudo-${variant}.log" # For idempotency re-run: nosudo-forced needs NOSUDO=1; nosudo-auto and # nosudo-nonsudoer rely on detect_sudo() reaching CAN_SUDO=false on its own @@ -274,10 +319,10 @@ run_nosudo() { : >"$logfile" # truncate echo "" - echo -e "${BOLD}── Ubuntu ${ubuntu} / nosudo-${variant} ──${NC}" + echo -e "${BOLD}── ${base_image} / nosudo-${variant} ──${NC}" # 1. Build (install.sh runs as non-root user during docker build) - _build_nosudo "$ubuntu" "$variant" || return 1 + _build_nosudo "$base_label" "$base_image" "$variant" || return 1 # 2. Test suite _run_step -u user "$tag" "test.sh nosudo" "$logfile" \ @@ -286,12 +331,12 @@ run_nosudo() { # 3. Idempotency - re-run install.sh (must be a no-op) _run_step -u user "$tag" "idempotency (re-run install.sh minimal)" "$logfile" \ - "export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && ${install_cmd}" \ + "${_CURLRC_CMD}; export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && ${install_cmd}" \ || return 1 # 4. update.sh _run_step -u user "$tag" "update.sh" "$logfile" \ - "export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && bash update.sh" \ + "${_CURLRC_CMD}; export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && bash update.sh" \ || return 1 # 5. Re-validate after update @@ -315,22 +360,42 @@ echo -e "${BOLD}╔════════════════════ echo -e "║ YA Dotfiles - Local Matrix Tests ║" echo -e "╚══════════════════════════════════════════╝${NC}" echo "" -# Determine which no-sudo Ubuntu versions to run -if $FILTER_NOSUDO; then - # User explicitly requested nosudo - use the ubuntu filter (or all) - if [[ ${#FILTER_UBUNTU[@]} -gt 0 ]]; then - NOSUDO_LIST=("${FILTER_UBUNTU[@]}") +# Determine which no-sudo bases (Ubuntu + AlmaLinux) to run. +# A --ubuntu filter without --alma limits nosudo to those Ubuntu versions (and +# vice versa) so a single-cell invocation never pulls in the other distro. +_resolve_nosudo_bases() { + if [[ ${#FILTER_UBUNTU[@]} -gt 0 && ${#FILTER_ALMA[@]} -eq 0 ]]; then + NOSUDO_LIST=("${FILTER_UBUNTU[@]}"); NOSUDO_ALMA_LIST=() + elif [[ ${#FILTER_ALMA[@]} -gt 0 && ${#FILTER_UBUNTU[@]} -eq 0 ]]; then + NOSUDO_LIST=(); NOSUDO_ALMA_LIST=("${FILTER_ALMA[@]}") + elif [[ ${#FILTER_ALMA[@]} -gt 0 && ${#FILTER_UBUNTU[@]} -gt 0 ]]; then + NOSUDO_LIST=("${FILTER_UBUNTU[@]}"); NOSUDO_ALMA_LIST=("${FILTER_ALMA[@]}") else - NOSUDO_LIST=("${ALL_NOSUDO_UBUNTU[@]}") + NOSUDO_LIST=("${ALL_NOSUDO_UBUNTU[@]}"); NOSUDO_ALMA_LIST=("${ALL_NOSUDO_ALMA[@]}") fi +} +if $FILTER_NOSUDO; then + # User explicitly requested nosudo + _resolve_nosudo_bases # Remove regular profiles if user only asked for nosudo if [[ ${#FILTER_PROFILES[@]} -eq 0 ]]; then PROFILE_LIST=() fi elif $SKIP_NOSUDO; then - NOSUDO_LIST=() + NOSUDO_LIST=(); NOSUDO_ALMA_LIST=() else - NOSUDO_LIST=("${ALL_NOSUDO_UBUNTU[@]}") + _resolve_nosudo_bases +fi + +# Regular profiles are apt-only and have no Alma cells: with --alma and no +# --ubuntu, drop them (none requested) or reject the unsatisfiable request. +if [[ ${#FILTER_ALMA[@]} -gt 0 && ${#FILTER_UBUNTU[@]} -eq 0 ]]; then + if [[ ${#FILTER_PROFILES[@]} -gt 0 ]]; then + echo "Error: profiles '${FILTER_PROFILES[*]}' need apt and have no AlmaLinux cells." >&2 + echo "Combine with --ubuntu, or use nosudo profiles with --alma." >&2 + exit 1 + fi + PROFILE_LIST=() fi # Resolve which nosudo variants to run @@ -340,13 +405,20 @@ else NOSUDO_VARIANT_LIST=("${ALL_NOSUDO_VARIANTS[@]}") fi -nosudo_count=$(( ${#NOSUDO_LIST[@]} * ${#NOSUDO_VARIANT_LIST[@]} )) +nosudo_count=$(( (${#NOSUDO_LIST[@]} + ${#NOSUDO_ALMA_LIST[@]}) * ${#NOSUDO_VARIANT_LIST[@]} )) regular_count=$(( ${#UBUNTU_LIST[@]} * ${#PROFILE_LIST[@]} )) +if (( regular_count + nosudo_count == 0 )); then + echo "Error: the given filters select zero cells - nothing to run." >&2 + exit 1 +fi + echo -e " Ubuntu versions : ${UBUNTU_LIST[*]}" echo -e " Profiles : ${PROFILE_LIST[*]:-none}" echo -e " No-sudo Ubuntu : ${NOSUDO_LIST[*]:-none}" +echo -e " No-sudo Alma : ${NOSUDO_ALMA_LIST[*]:-none}" echo -e " No-sudo variants : ${NOSUDO_VARIANT_LIST[*]:-none}" +echo -e " GitHub API auth : $([ -n "$GH_TOKEN_VAL" ] && echo "token found" || echo "none (60 req/hr limit)")" echo -e " Combinations : $((regular_count + nosudo_count))" echo -e " Logs : ${LOG_DIR}/" @@ -366,7 +438,7 @@ done for ubuntu in "${NOSUDO_LIST[@]}"; do for variant in "${NOSUDO_VARIANT_LIST[@]}"; do total=$((total + 1)) - if run_nosudo "$ubuntu" "$variant"; then + if run_nosudo "$ubuntu" "ubuntu:${ubuntu}" "$variant"; then passed=$((passed + 1)) results+=("${GREEN}PASS${NC} Ubuntu ${ubuntu} / nosudo-${variant}") else @@ -376,6 +448,19 @@ for ubuntu in "${NOSUDO_LIST[@]}"; do done done +for alma in "${NOSUDO_ALMA_LIST[@]}"; do + for variant in "${NOSUDO_VARIANT_LIST[@]}"; do + total=$((total + 1)) + if run_nosudo "alma${alma}" "almalinux:${alma}" "$variant"; then + passed=$((passed + 1)) + results+=("${GREEN}PASS${NC} AlmaLinux ${alma} / nosudo-${variant}") + else + failed=$((failed + 1)) + results+=("${RED}FAIL${NC} AlmaLinux ${alma} / nosudo-${variant} → ${LOG_DIR}/alma${alma}-nosudo-${variant}.log") + fi + done +done + # ── Summary table ───────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}── Results ──────────────────────────────────────────${NC}" diff --git a/get.sh b/get.sh index f2bbb5a..0fc23ec 100755 --- a/get.sh +++ b/get.sh @@ -70,27 +70,40 @@ fi echo -e "${BOLD}── Pre-flight checks ──${NC}" _preflight_ok=true -# auto-bootstrap missing tools when running as root on apt-based systems +# package-manager hint for error messages (apt on Debian family, dnf/yum on RHEL family) +if command -v apt-get &>/dev/null; then _PKG_HINT="sudo apt install" +elif command -v dnf &>/dev/null; then _PKG_HINT="sudo dnf install" +elif command -v yum &>/dev/null; then _PKG_HINT="sudo yum install" +else _PKG_HINT="your package manager:" +fi + +# auto-bootstrap missing tools when running as root (apt or dnf/yum systems) _apt_updated=false -_apt_bootstrap() { +_pkg_bootstrap() { local pkg="$1" - if [ "$(id -u)" -ne 0 ] || ! command -v apt-get &>/dev/null; then + [ "$(id -u)" -eq 0 ] || return 1 + if command -v apt-get &>/dev/null; then + if ! $_apt_updated; then + apt-get update -qq >/dev/null + _apt_updated=true + fi + DEBIAN_FRONTEND=noninteractive apt-get install -yq "$pkg" >/dev/null + elif command -v dnf &>/dev/null; then + dnf -yq install "$pkg" >/dev/null 2>&1 + elif command -v yum &>/dev/null; then + yum -yq install "$pkg" >/dev/null 2>&1 + else return 1 fi - if ! $_apt_updated; then - apt-get update -qq >/dev/null - _apt_updated=true - fi - DEBIAN_FRONTEND=noninteractive apt-get install -yq "$pkg" >/dev/null } # git (hard requirement - needed to clone) if command -v git &>/dev/null; then _ok "git $(git --version | awk '{print $3}')" -elif _apt_bootstrap git; then +elif _pkg_bootstrap git; then _ok "git $(git --version | awk '{print $3}') (just installed)" else - echo -e "${RED} ✗${NC} git - not found → sudo apt install git" >&2 + echo -e "${RED} ✗${NC} git - not found → ${_PKG_HINT} git" >&2 _preflight_ok=false fi @@ -99,10 +112,10 @@ if command -v curl &>/dev/null; then _ok "curl $(curl --version | awk 'NR==1{print $2}')" elif command -v wget &>/dev/null; then _ok "wget $(wget --version 2>&1 | awk 'NR==1{print $3}')" -elif _apt_bootstrap curl; then +elif _pkg_bootstrap curl; then _ok "curl $(curl --version | awk 'NR==1{print $2}') (just installed)" else - echo -e "${RED} ✗${NC} curl/wget - neither found → sudo apt install curl" >&2 + echo -e "${RED} ✗${NC} curl/wget - neither found → ${_PKG_HINT} curl" >&2 _preflight_ok=false fi @@ -114,18 +127,22 @@ elif [ "$(id -u)" -eq 0 ]; then elif command -v sudo &>/dev/null && sudo -n true 2>/dev/null; then _ok "sudo (passwordless)" elif command -v sudo &>/dev/null; then - _warn "sudo present but requires a password - you will be prompted when apt runs" + _warn "sudo present but requires a password - you will be prompted when packages install" else - _warn "sudo not found - apt package installs will be skipped" + _warn "sudo not found - system package installs will be skipped" fi -# OS (soft - scripts are written for Ubuntu/Debian) +# OS (soft - Debian family is fully supported; RHEL family installs tools +# as user-local binaries and needs prerequisites preinstalled via dnf) if [ -f /etc/os-release ]; then # shellcheck source=/dev/null . /etc/os-release case "${ID:-}" in ubuntu) _ok "OS: Ubuntu ${VERSION_ID:-}" ;; debian) _warn "OS: Debian ${VERSION_ID:-} - supported but not tested" ;; + almalinux|rocky|rhel|centos|fedora) + _ok "OS: ${PRETTY_NAME:-$ID} (RHEL family)" + _warn "No apt: tools install to ~/.local/bin; prerequisites (git curl zsh tmux python3 tar) need dnf" ;; *) _warn "OS: ${PRETTY_NAME:-unknown} - scripts are written for Ubuntu; proceed with caution" ;; esac else diff --git a/lib/utils.sh b/lib/utils.sh index d739216..ae6184a 100755 --- a/lib/utils.sh +++ b/lib/utils.sh @@ -23,17 +23,22 @@ apt_install() { $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -yq "$@" } -# Check if we can use sudo (sets globals SUDO, CAN_SUDO, SUDO_STATUS). +# Check if we can use sudo (sets globals SUDO, CAN_SUDO, SUDO_STATUS, CAN_APT). # Pure probe - no output; uses only non-interactive `sudo -n` forms so it # never prompts. A non-interactive `sudo -n -v` distinguishes a real sudoer # who needs a password from a user who is not in sudoers at all. # SUDO_STATUS values: root | sudo_passwordless | sudo_password | nosudo +# CAN_APT: privileged AND apt-based OS. The package-install paths are apt-only, +# so non-apt distros (RHEL family: AlmaLinux, Rocky, Fedora, ...) must take the +# user-local binary path even when sudo works - gate them on CAN_APT, never on +# CAN_SUDO alone, or `apt-get` dies under set -e mid-install. detect_sudo() { if [ -n "${NOSUDO:-}" ]; then SUDO="" CAN_SUDO=false SUDO_STATUS=nosudo - export SUDO CAN_SUDO SUDO_STATUS + CAN_APT=false + export SUDO CAN_SUDO SUDO_STATUS CAN_APT return fi if [ "$(id -u)" -eq 0 ]; then @@ -75,7 +80,21 @@ detect_sudo() { CAN_SUDO=false SUDO_STATUS=nosudo fi - export SUDO CAN_SUDO SUDO_STATUS + if $CAN_SUDO && command -v apt-get &>/dev/null; then + CAN_APT=true + else + CAN_APT=false + fi + export SUDO CAN_SUDO SUDO_STATUS CAN_APT +} + +# Native package-manager install prefix for prerequisite hint messages. +_pkg_install_hint() { + if command -v apt-get &>/dev/null; then echo "sudo apt install -y" + elif command -v dnf &>/dev/null; then echo "sudo dnf install -y" + elif command -v yum &>/dev/null; then echo "sudo yum install -y" + else echo "your package manager to install:" + fi } # Safe symlink: creates parent dir and force-overwrites existing link @@ -166,20 +185,24 @@ run_checks() { detect_sudo case "$SUDO_STATUS" in root) - log_ok "Running as root - system packages will be installed directly" ;; + log_ok "Running as root - no sudo needed" ;; sudo_passwordless) - log_ok "sudo available (passwordless) - system packages will be installed via apt" ;; + log_ok "sudo available (passwordless)" ;; sudo_password) - log_info "sudo available - system packages will be installed via apt" - log_warn "sudo requires a password - you will be prompted when apt runs" ;; + log_info "sudo available" + log_warn "sudo requires a password - you will be prompted when it is used" ;; nosudo) if [ -n "${NOSUDO:-}" ]; then log_info "NOSUDO=1 set - running in user-local mode (sudo disabled)" else - log_warn "No sudo - apt skipped; tools will be fetched as local binaries into ~/.local/bin" + log_warn "No sudo - system packages skipped; tools will be fetched as local binaries into ~/.local/bin" fi ;; esac - if $CAN_SUDO && [ -n "${PROFILE:-}" ]; then + if $CAN_SUDO && ! $CAN_APT; then + log_warn "No apt on this system - system packages skipped; tools will be fetched into ~/.local/bin" + log_warn " Prerequisites (if missing): $(_pkg_install_hint) git curl zsh tmux python3 tar findutils" + fi + if $CAN_APT && [ -n "${PROFILE:-}" ]; then case "$PROFILE" in minimal) log_info "sudo will be used for: apt packages, chsh/usermod (default shell)" ;; @@ -188,6 +211,8 @@ run_checks() { docker) log_info "sudo will be used for: apt packages" ;; esac + elif $CAN_SUDO && [ -n "${PROFILE:-}" ] && [ "$PROFILE" != "docker" ]; then + log_info "sudo will be used for: chsh/usermod (default shell)" fi log_ok "All checks passed" } @@ -208,6 +233,10 @@ _check_os() { log_ok "OS: $PRETTY_NAME" fi ;; + almalinux | rocky | rhel | centos | fedora) + log_ok "OS: $PRETTY_NAME" + log_info "RHEL-family OS: no apt - tools are fetched as user-local binaries into ~/.local/bin" + ;; *) log_warn "OS '$ID' is not Ubuntu/Debian. Package installation may fail - proceeding anyway." ;; diff --git a/modules/base.sh b/modules/base.sh index c72e470..e75be61 100755 --- a/modules/base.sh +++ b/modules/base.sh @@ -1,14 +1,15 @@ #!/usr/bin/env bash # Base system packages - requires apt (Ubuntu/Debian) # Idempotent: apt handles "already installed" gracefully -# When CAN_SUDO=false, apt steps are skipped and tools are fetched as -# pre-built binaries into ~/.local/bin instead. +# When CAN_APT=false (no sudo, or a non-apt distro such as AlmaLinux), apt +# steps are skipped and tools are fetched as pre-built binaries into +# ~/.local/bin instead. install_base() { log_step "Base packages" mkdir -p ~/.local/bin - if $CAN_SUDO; then + if $CAN_APT; then local -a _pkgs=( locales git curl wget @@ -37,9 +38,14 @@ install_base() { log_ok "Created fd → fdfind shim in ~/.local/bin" fi else - log_warn "No sudo - skipping apt; fetching available tools as local binaries" + if $CAN_SUDO; then + log_warn "No apt on this system - skipping system packages; fetching tools as local binaries" + else + log_warn "No sudo - skipping system packages; fetching tools as local binaries" + fi + local _hint tool; _hint=$(_pkg_install_hint) for tool in git zsh tmux python3; do - has "$tool" || log_warn "$tool not found - install via your system package manager" + has "$tool" || log_warn "$tool not found - install it: ${_hint} $tool" done _install_ripgrep _install_fd @@ -52,7 +58,7 @@ install_base() { _install_eza _install_yazi - if ! $CAN_SUDO; then + if ! $CAN_APT; then log_warn "Binaries installed to ~/.local/bin - ensure it is on your PATH:" log_warn " export PATH=\"\$HOME/.local/bin:\$PATH\"" fi @@ -62,26 +68,33 @@ install_base() { install_base_docker() { log_step "Base packages (docker mode)" - if ! $CAN_SUDO; then - log_warn "Skipping system packages (no sudo)" - return - fi + mkdir -p ~/.local/bin - local -a _pkgs=( - locales git curl wget - zsh tmux neovim - jq ripgrep fd-find shellcheck - ) - log_info "Installing via apt: ${_pkgs[*]} (versions resolved by apt)" - $SUDO apt-get -yq update - apt_install "${_pkgs[@]}" - log_info "Generating locale: en_US.UTF-8" - $SUDO locale-gen en_US.UTF-8 - $SUDO update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 - - if ! has fd && has fdfind; then - mkdir -p ~/.local/bin - ln -sf "$(command -v fdfind)" ~/.local/bin/fd + if $CAN_APT; then + local -a _pkgs=( + locales git curl wget + zsh tmux neovim + jq ripgrep fd-find shellcheck + ) + log_info "Installing via apt: ${_pkgs[*]} (versions resolved by apt)" + $SUDO apt-get -yq update + apt_install "${_pkgs[@]}" + log_info "Generating locale: en_US.UTF-8" + $SUDO locale-gen en_US.UTF-8 + $SUDO update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 + + if ! has fd && has fdfind; then + ln -sf "$(command -v fdfind)" ~/.local/bin/fd + fi + else + log_warn "No apt - skipping system packages; fetching tools as local binaries" + local _hint tool; _hint=$(_pkg_install_hint) + for tool in git zsh tmux python3; do + has "$tool" || log_warn "$tool not found - install it: ${_hint} $tool" + done + _install_ripgrep + _install_fd + _install_jq fi _install_fzf @@ -122,7 +135,7 @@ _install_zoxide() { local major=0 [ -n "$apt_ver" ] && major=$(echo "$apt_ver" | cut -d. -f1) - if $CAN_SUDO && { [ "$major" -gt 0 ] || [ "$minor" -ge 8 ]; }; then + if $CAN_APT && { [ "$major" -gt 0 ] || [ "$minor" -ge 8 ]; }; then log_info "zoxide: installing via apt (version ≥ 0.8 available) → system" apt_install zoxide return @@ -146,7 +159,7 @@ _install_zoxide() { read -r tag url < <(_gh_release_info "ajeetdsouza/zoxide" "${zoxide_arch}.tar.gz") || true if [ -z "$url" ]; then - if $CAN_SUDO && [ -n "$apt_ver" ]; then + if $CAN_APT && [ -n "$apt_ver" ]; then log_warn "zoxide: could not determine latest version; falling back to apt $apt_ver" log_warn "zoxide: apt version <0.8 - 'zi' alias requires ≥0.8; run update.sh to upgrade" apt_install zoxide @@ -163,7 +176,7 @@ _install_zoxide() { fi # GitHub download failed (e.g. rate-limited in local Docker builds) - if $CAN_SUDO && [ -n "$apt_ver" ]; then + if $CAN_APT && [ -n "$apt_ver" ]; then log_warn "zoxide: GitHub download failed; falling back to apt $apt_ver" log_warn "zoxide: apt version <0.8 - 'zi' alias requires ≥0.8; run update.sh to upgrade" apt_install zoxide @@ -183,12 +196,12 @@ _install_delta() { return fi - if $CAN_SUDO && apt-cache show git-delta &>/dev/null 2>&1; then + if $CAN_APT && apt-cache show git-delta &>/dev/null 2>&1; then apt_install git-delta return fi - if $CAN_SUDO; then + if $CAN_APT; then log_step "git-delta (GitHub .deb)" local arch; arch="$(_deb_arch)" local tag="" url="" @@ -213,7 +226,7 @@ _install_delta() { $SUDO dpkg -i "$tmp/$deb" log_ok "git-delta ${ver} installed → $(command -v delta 2>/dev/null || echo 'system')" else - log_step "git-delta (GitHub binary - no sudo)" + log_step "git-delta (GitHub binary)" local arch; arch=$(uname -m) local delta_arch case "$arch" in @@ -250,13 +263,13 @@ _install_eza() { return fi - if $CAN_SUDO && apt-cache show eza &>/dev/null 2>&1; then + if $CAN_APT && apt-cache show eza &>/dev/null 2>&1; then log_info "eza: installing via apt → system" apt_install eza return fi - if $CAN_SUDO; then + if $CAN_APT; then log_step "eza (official PPA)" log_info "eza: installing latest → system (via PPA)" apt_install gpg # needed for dearmor; may already be present @@ -277,7 +290,7 @@ _install_eza() { apt_install eza log_ok "eza installed via PPA ($(eza --version 2>/dev/null | head -1))" else - log_step "eza (GitHub binary - no sudo)" + log_step "eza (GitHub binary)" local arch; arch=$(uname -m) local eza_arch case "$arch" in diff --git a/modules/neovim.sh b/modules/neovim.sh index a94c250..58e1c34 100755 --- a/modules/neovim.sh +++ b/modules/neovim.sh @@ -89,9 +89,11 @@ install_neovim() { | head -1) local latest="${latest_tag#v}" - # Install prefix: /usr/local with sudo, ~/.local without (needed for disclosure below) + # Install prefix: /usr/local on apt systems with sudo, ~/.local otherwise. + # Keyed on CAN_APT, not CAN_SUDO: on RHEL-family hosts all tools stay + # user-local even when sudo works. local prefix - if $CAN_SUDO; then prefix="/usr/local"; else prefix="$HOME/.local"; fi + if $CAN_APT; then prefix="/usr/local"; else prefix="$HOME/.local"; fi # Skip if already at latest version AND binary actually runs. # A version-matching but glibc-broken binary must not be skipped. @@ -103,7 +105,7 @@ install_neovim() { # Shadow check still needed: `nvim` above may have resolved to a # user-local copy (e.g. ~/.local/bin/nvim) that shadows an existing # /usr/local/bin/nvim at the same version. - if $CAN_SUDO; then _nvim_warn_shadows /usr/local/bin/nvim; fi + if $CAN_APT; then _nvim_warn_shadows /usr/local/bin/nvim; fi return fi log_info "neovim: upgrading $current → $latest (installing to $prefix)" @@ -141,7 +143,7 @@ install_neovim() { return fi - if ! $CAN_SUDO; then mkdir -p "$prefix"; fi + if [ "$prefix" != "/usr/local" ]; then mkdir -p "$prefix"; fi local tmp tmp=$(mktemp -d) @@ -164,7 +166,7 @@ install_neovim() { return fi - if $CAN_SUDO; then + if [ "$prefix" = "/usr/local" ]; then # Refresh credential cache right before use - the download above may have # taken long enough for the 15-minute sudo cache to expire. if ! sudo -n true 2>/dev/null; then @@ -179,7 +181,7 @@ install_neovim() { # Use the full path so the version shown is always the binary we just # installed, not whatever `nvim` resolves to in the install-time bash PATH. log_ok "neovim installed → $prefix ($($prefix/bin/nvim --version 2>/dev/null | head -1))" - if $CAN_SUDO; then _nvim_warn_shadows /usr/local/bin/nvim; fi + if $CAN_APT; then _nvim_warn_shadows /usr/local/bin/nvim; fi } # Download the last neovim release compatible with glibc < 2.32. @@ -210,20 +212,25 @@ _neovim_legacy_binary() { return fi - if $CAN_SUDO; then + if [ "$prefix" = "/usr/local" ]; then [ -n "${SUDO:-}" ] && sudo -v 2>/dev/null || true $SUDO cp -r "$extracted"/. "$prefix/" else + mkdir -p "$prefix" cp -r "$extracted"/. "$prefix/" fi log_ok "neovim legacy $tag installed → $prefix ($($prefix/bin/nvim --version 2>/dev/null | head -1))" } _neovim_apt() { - if ! $CAN_SUDO; then - log_warn "neovim: no sudo - cannot install via apt" - log_warn " Prebuilt GitHub binaries require glibc ≥ 2.32 (Ubuntu 22.04+)" - log_warn " Options: upgrade OS, or build neovim from source" + if ! $CAN_APT; then + if $CAN_SUDO; then + log_warn "neovim: no apt on this system - install it manually: $(_pkg_install_hint) neovim" + else + log_warn "neovim: no sudo - cannot install via a system package manager" + log_warn " Prebuilt GitHub binaries require glibc ≥ 2.32" + log_warn " Options: upgrade OS, or build neovim from source" + fi return fi apt_install neovim diff --git a/update.sh b/update.sh index 8ef355a..31bc675 100755 --- a/update.sh +++ b/update.sh @@ -147,7 +147,7 @@ _do_update_neovim() { if _ver_older_than "$glibc_ver" "2.32"; then log_warn "neovim: system glibc $glibc_ver < 2.32 - pinned to legacy v0.9.5" local nvim_dest - if $CAN_SUDO; then nvim_dest=/usr/local/bin/nvim; else nvim_dest=$HOME/.local/bin/nvim; fi + if $CAN_APT; then nvim_dest=/usr/local/bin/nvim; else nvim_dest=$HOME/.local/bin/nvim; fi if $CHECK_ONLY; then local cur_leg; cur_leg=$(_cmd_version nvim --version) || cur_leg="none" log_info " neovim: $cur_leg (legacy; cannot upgrade on glibc $glibc_ver)" @@ -186,7 +186,7 @@ _do_update_neovim() { || { log_warn "neovim: legacy download failed - skipping"; return; }; fi local leg_extracted; leg_extracted=$(find "$tmp" -maxdepth 1 -type d -name 'nvim-*' | head -1) [ -z "$leg_extracted" ] && { log_warn "neovim: unexpected archive layout - skipping"; return; } - if $CAN_SUDO; then $SUDO cp -r "$leg_extracted"/. /usr/local/; else cp -r "$leg_extracted"/. "$HOME/.local/"; fi + if $CAN_APT; then $SUDO cp -r "$leg_extracted"/. /usr/local/; else cp -r "$leg_extracted"/. "$HOME/.local/"; fi log_ok "neovim restored → $("$nvim_dest" --version 2>/dev/null | head -1)" return fi @@ -201,10 +201,10 @@ _do_update_neovim() { fi if $CHECK_ONLY; then _report_version neovim "${current:-none}" "$latest_tag" - if $CAN_SUDO; then - log_info " → update would install to /usr/local/ (sudo available)" + if $CAN_APT; then + log_info " → update would install to /usr/local/" else - log_info " → update would install to ~/.local/ (no sudo)" + log_info " → update would install to ~/.local/" fi return fi @@ -227,7 +227,7 @@ _do_update_neovim() { local extracted; extracted=$(find "$tmp" -maxdepth 1 -type d -name 'nvim-*' | head -1) if [ -z "$extracted" ]; then log_warn "neovim: unexpected archive layout - skipping"; return; fi local nvim_dest - if $CAN_SUDO; then + if $CAN_APT; then $SUDO cp -r "$extracted"/. /usr/local/ nvim_dest=/usr/local/bin/nvim else @@ -245,14 +245,14 @@ _do_update_xcape() { if $CHECK_ONLY; then _check_git_updates "xcape" "" 2>/dev/null || true log_info "xcape: installed at $(command -v xcape) - source-built from alols/xcape (no version tags)" - if $CAN_SUDO; then + if $CAN_APT; then log_info " → run './update.sh xcape' to rebuild from latest source" else - log_warn " → sudo required to reinstall xcape" + log_warn " → sudo + apt required to rebuild xcape" fi else - if ! $CAN_SUDO; then - log_warn "xcape: sudo required to install build deps and binary - skipping" + if ! $CAN_APT; then + log_warn "xcape: sudo + apt required to install build deps and binary - skipping" else log_info "xcape: rebuilding from source (alols/xcape)" apt_install libxtst-dev libx11-dev pkg-config make gcc @@ -352,16 +352,16 @@ _check_path_shadows() { log_info "Checking sudo access..." detect_sudo case "$SUDO_STATUS" in - root) log_ok "Running as root - apt upgrades will run directly" ;; - sudo_passwordless) log_ok "sudo available - apt upgrades will run via sudo" ;; + root) log_ok "Running as root - system upgrades will run directly" ;; + sudo_passwordless) log_ok "sudo available - system upgrades will run via sudo" ;; sudo_password) - log_ok "sudo available - apt upgrades will run via sudo" - log_warn "sudo requires a password - you will be prompted when apt runs" ;; - nosudo) log_warn "No sudo - apt upgrade skipped" ;; + log_ok "sudo available - system upgrades will run via sudo" + log_warn "sudo requires a password - you will be prompted when it is used" ;; + nosudo) log_warn "No sudo - system package upgrade skipped" ;; esac if _should_run apt; then log_step "System packages (apt)" - if $CAN_SUDO; then + if $CAN_APT; then if $CHECK_ONLY; then apt list --upgradable 2>/dev/null | grep -v '^Listing' || true log_info " → sudo required to apply these updates" @@ -370,6 +370,8 @@ if _should_run apt; then $SUDO env DEBIAN_FRONTEND=noninteractive apt-get -yq upgrade log_ok "System packages updated" fi + elif $CAN_SUDO; then + log_warn "No apt on this system - skipping (upgrade via your native package manager)" else log_warn "No sudo - skipping apt upgrade" fi