Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,15 @@ jobs:

steps:
- name: Bootstrap prerequisites (as root - mirrors a shared host)
# tmux is deliberately NOT pre-installed. It used to be, and that is
# exactly why the no-sudo gap stayed invisible across 15 cells: `has tmux`
# was always true, so the missing installer never mattered while the repo
# still shipped tmux config and cloned tmux plugins. The environment was
# supplying the thing under test. install.sh must provide it now.
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get -yq update
apt-get -yq install apt-utils git curl wget ca-certificates zsh tmux python3
apt-get -yq install apt-utils git curl wget ca-certificates zsh python3

- name: Install sudo binary (forced + nonsudoer)
if: matrix.variant == 'forced' || matrix.variant == 'nonsudoer'
Expand Down Expand Up @@ -345,7 +350,8 @@ jobs:
# --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 \
# tmux deliberately absent here too - see the Ubuntu no-sudo job.
dnf -yq install --allowerasing git curl wget ca-certificates zsh python3 \
tar gzip xz findutils shadow-utils glibc-langpack-en

- name: Install sudo binary (forced + nonsudoer)
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ fallback and the unapplied-pins path - 28 cells total.
list with a per-package retry, keep an optional tool's apt steps non-fatal
(`|| { log_warn ...; return; }`), and let the `_install_*` GitHub fallbacks
cover whatever apt cannot supply.
- **Never let a test environment supply the thing under test.** The no-sudo CI jobs
and `Dockerfile.nosudo` pre-installed tmux as a root prerequisite, so `has tmux`
was true before install.sh ran. A non-sudoer therefore got tmux CONFIG and tmux
PLUGINS but no tmux BINARY, and 15 green cells said nothing about it for months -
found on a real host, not in CI. When adding a prerequisite to a test image, ask
whether the install is supposed to provide it; if it is, leave it out.
- Never construct GitHub release asset URLs manually - use `_gh_release_info` or
`_gh_latest_release` from `lib/utils.sh`; asset names change between releases.
Exception: `releases/latest/download/<name>` for an asset whose name carries no
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.12.0] - 2026-09-19

### Added
- tmux is now installed without sudo, as a static AppImage, with an extract fallback for hosts without FUSE. The repo shipped tmux config and cloned tmux plugins while a non-sudoer could end up with no tmux binary at all.

### Changed
- The no-sudo CI jobs and `Dockerfile.nosudo` no longer pre-install tmux. Doing so made `has tmux` true before install.sh ran, which is why the gap stayed invisible across 15 cells.

## [1.11.9] - 2026-09-18

### Fixed
Expand Down
8 changes: 6 additions & 2 deletions Dockerfile.nosudo
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,20 @@ ARG NOSUDO_INSTALL=
# 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).
# tmux is deliberately NOT here. Pre-installing it made `has tmux` true before
# install.sh ran, which is why a no-sudo host getting tmux CONFIG but no tmux
# BINARY went unnoticed - the image supplied the thing under test. install.sh
# must provide it (_install_tmux in modules/base.sh).
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 && \
apt-utils git curl wget ca-certificates zsh 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 \
git curl wget ca-certificates zsh python3 \
tar gzip xz findutils shadow-utils glibc-langpack-en && \
dnf clean all; \
fi
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.11.9
1.12.0
62 changes: 60 additions & 2 deletions modules/base.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,17 @@ install_base() {
else
log_warn "No sudo - skipping system packages; fetching tools as local binaries"
fi
# git, zsh and python3 have no practical single-binary fallback, so they
# stay a hint. tmux does (see _install_tmux), and this repo ships tmux
# config and clones tmux plugins, so it is fetched rather than mentioned.
local _hint tool; _hint=$(_pkg_install_hint)
for tool in git zsh tmux python3; do
for tool in git zsh python3; do
has "$tool" || log_warn "$tool not found - install it: ${_hint} $tool"
done
_install_ripgrep
_install_fd
_install_jq
_install_tmux
fi

_install_fzf
Expand Down Expand Up @@ -118,13 +122,17 @@ install_base_docker() {
_install_jq
else
log_warn "No apt - skipping system packages; fetching tools as local binaries"
# git, zsh and python3 have no practical single-binary fallback, so they
# stay a hint. tmux does (see _install_tmux), and this repo ships tmux
# config and clones tmux plugins, so it is fetched rather than mentioned.
local _hint tool; _hint=$(_pkg_install_hint)
for tool in git zsh tmux python3; do
for tool in git zsh python3; do
has "$tool" || log_warn "$tool not found - install it: ${_hint} $tool"
done
_install_ripgrep
_install_fd
_install_jq
_install_tmux
fi

_install_fzf
Expand Down Expand Up @@ -453,6 +461,56 @@ _install_ripgrep() {
fi
}

# tmux: only reachable here as a static AppImage.
#
# This repo ships .tmux.conf, .tmux.conf.local and clones tmux plugins, so a host
# that gets the configs but no tmux is incoherent - and that is exactly what a
# non-sudoer on a box without tmux got, measured 2026-09-19 on a fleet host.
# Upstream tmux publishes SOURCE ONLY (checked: the latest release carries just
# tmux-<ver>.tar.gz), and building it needs libevent and ncurses built locally
# too, so a prebuilt static binary is the only one-command option.
#
# The build is THIRD PARTY (nelsonenzo/tmux-appimage), which is why this verifies
# the binary runs before linking it and says plainly where it came from, rather
# than installing it silently. apt is always preferred when it is available.
_install_tmux() {
if has tmux; then
log_ok "tmux already installed - skipping"
return
fi
log_step "tmux (static AppImage - third-party build)"
local arch; arch=$(uname -m)
if [ "$arch" != x86_64 ]; then
log_warn "tmux: the AppImage is x86_64 only, this is $arch - install it with your package manager"
return
fi
local url="https://github.com/nelsonenzo/tmux-appimage/releases/latest/download/tmux.appimage"
local optdir="$HOME/.local/opt"
mkdir -p "$optdir" ~/.local/bin
log_info "tmux: fetching the static AppImage from nelsonenzo/tmux-appimage"
if has curl; then
curl -sfLo "$optdir/tmux.appimage" "$url" || { log_warn "tmux: download failed - skipping"; return; }
else
wget -qO "$optdir/tmux.appimage" "$url" || { log_warn "tmux: download failed - skipping"; return; }
fi
chmod +x "$optdir/tmux.appimage"

# An AppImage mounts itself through FUSE, which many containers and hardened
# hosts do not have - measured on a fleet host, where the direct run failed and
# this fallback is what actually installed it. --appimage-extract needs no FUSE.
if "$optdir/tmux.appimage" -V >/dev/null 2>&1; then
ln -sf "$optdir/tmux.appimage" ~/.local/bin/tmux
log_ok "tmux installed → ~/.local/bin/tmux ($("$optdir/tmux.appimage" -V 2>/dev/null)) [AppImage]"
elif ( cd "$optdir" && "$optdir/tmux.appimage" --appimage-extract >/dev/null 2>&1 ) \
&& [ -x "$optdir/squashfs-root/usr/bin/tmux" ]; then
ln -sf "$optdir/squashfs-root/usr/bin/tmux" ~/.local/bin/tmux
log_ok "tmux installed → ~/.local/bin/tmux ($("$optdir/squashfs-root/usr/bin/tmux" -V 2>/dev/null)) [extracted, no FUSE]"
else
log_warn "tmux: the AppImage neither ran nor extracted - skipping"
return
fi
}

# fd: GitHub tarball - binary is 'fd'
_install_fd() {
if has fd || has fdfind; then
Expand Down
Loading