Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
1705e3a47This file is pinned to the selected release. Download the original to open it in your design tool.
#!/usr/bin/env bash
# Build the managed tmux that `install.sh` downloads on a Mac that has no tmux and no Homebrew —
# the same idea as the managed Node under ~/.harness/runtime: one checksum-verified archive we
# publish, so obtaining tmux never needs a compiler, a package manager or a password on the user's
# computer. Homebrew was how tmux got built before, and Homebrew's installer needs the Apple
# developer tools, which cost a Terminal window, a 2 GB download and a ten-minute wait for a 1 MB
# binary. This runs that build ONCE, in CI, per version.
#
# bash cli/scripts/build-managed-tmux.sh darwin-arm64 [out-dir]
# bash cli/scripts/build-managed-tmux.sh darwin-x64 [out-dir] # cross-built on an arm64 Mac
#
# Produces <out-dir>/tmux-<ver>-<platform>.tar.gz whose root is tmux-<ver>-<platform>/bin/tmux,
# plus the three upstream licences. libevent and ncurses are linked statically; libSystem stays
# dynamic, as every macOS binary's does. Needs the Command Line Tools HERE, and nowhere else.
#
# The three sources are pinned by version AND sha256: a moved tarball fails loudly rather than
# building something else. ncurses's unversioned `ncurses.tar.gz` is deliberately not used.
set -euo pipefail
PLATFORM="${1:-}"
OUT_DIR="${2:-$PWD/dist/tmux}"
case "$PLATFORM" in
darwin-arm64) ARCH=arm64; HOST=aarch64-apple-darwin ;;
darwin-x64) ARCH=x86_64; HOST=x86_64-apple-darwin ;;
*)
echo "usage: $0 darwin-arm64|darwin-x64 [out-dir]" >&2
exit 2
;;
esac
[ "$(uname -s)" = Darwin ] || { echo "error: the managed tmux is built on macOS" >&2; exit 1; }
TMUX_VERSION="${TMUX_VERSION:-3.7c}"
LIBEVENT_VERSION="${LIBEVENT_VERSION:-2.1.12-stable}"
NCURSES_VERSION="${NCURSES_VERSION:-6.5}"
# Pinned sources. Bumping a version means bumping its checksum in the same change.
LIBEVENT_URL="https://github.com/libevent/libevent/releases/download/release-${LIBEVENT_VERSION}/libevent-${LIBEVENT_VERSION}.tar.gz"
LIBEVENT_SHA256="${LIBEVENT_SHA256:-92e6de1be9ec176428fd2367677e61ceffc2ee1cb119035037a27d346b0403bb}"
NCURSES_URL="https://invisible-mirror.net/archives/ncurses/ncurses-${NCURSES_VERSION}.tar.gz"
NCURSES_SHA256="${NCURSES_SHA256:-136d91bc269a9a5785e5f9e980bc76ab57428f604ce3e5a5a90cebc767971cc6}"
TMUX_URL="https://github.com/tmux/tmux/releases/download/${TMUX_VERSION}/tmux-${TMUX_VERSION}.tar.gz"
TMUX_SHA256="${TMUX_SHA256:-7c60cae9a0e25288e2e24750aafc9e8800fc7fd4555e447e1b29ee4201cfb3bf}"
for command in curl tar shasum make cc codesign otool; do
command -v "$command" >/dev/null 2>&1 || { echo "error: $command is required" >&2; exit 1; }
done
/usr/bin/xcrun --find clang >/dev/null 2>&1 || {
echo "error: the Apple developer tools are required to BUILD tmux (never to install it)" >&2
exit 1
}
# `--host` only when actually cross-compiling. Passing it for the native build flips autoconf into
# cross mode, where run-time feature tests are skipped and answered "no" — libevent then built a
# tmux that segfaulted in evutil_make_internal_pipe_ on the very Mac that built it.
NATIVE_ARCH="$(uname -m)"
[ "$NATIVE_ARCH" = arm64 ] && NATIVE_ARCH=arm64 || NATIVE_ARCH=x86_64
if [ "$ARCH" = "$NATIVE_ARCH" ]; then HOST_FLAG=""; else HOST_FLAG="--host=$HOST"; fi
WORK="$(mktemp -d)"
# KEEP_WORK=1 leaves the build tree behind for a post-mortem.
[ -n "${KEEP_WORK:-}" ] && echo ">> work dir: $WORK" || trap 'rm -rf "$WORK"' EXIT
ROOT="tmux-${TMUX_VERSION}-${PLATFORM}"
STAGE="$WORK/stage/$ROOT"
DEPS="$WORK/deps"
mkdir -p "$STAGE/bin" "$DEPS" "$OUT_DIR"
NPROC="$(sysctl -n hw.ncpu 2>/dev/null || echo 2)"
fetch() { # url sha256 name
local archive="$WORK/$3.tar.gz" got
echo ">> $3: downloading $1"
curl -fsSL "$1" -o "$archive"
got="$(shasum -a 256 "$archive" | awk '{print $1}')"
[ "$got" = "$2" ] || { echo "error: $3 checksum mismatch (expected $2, got $got)" >&2; exit 1; }
mkdir -p "$WORK/src/$3"
tar -xzf "$archive" -C "$WORK/src/$3" --strip-components=1
}
fetch "$LIBEVENT_URL" "$LIBEVENT_SHA256" libevent
fetch "$NCURSES_URL" "$NCURSES_SHA256" ncurses
fetch "$TMUX_URL" "$TMUX_SHA256" tmux
# One toolchain line for all three: the target arch (cross-built for x64 on an arm64 Mac) and a
# deployment target old enough for every Mac the app supports.
export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-12.0}"
# The /usr/bin shim resolves the SDK through xcrun; the toolchain's own clang does not, and links
# nothing without an explicit sysroot.
export SDKROOT="$(/usr/bin/xcrun --show-sdk-path)"
export CC=/usr/bin/clang
export CFLAGS="-arch $ARCH -O2 -isysroot $SDKROOT"
export LDFLAGS="-arch $ARCH -isysroot $SDKROOT -L$DEPS/lib"
export CPPFLAGS="-I$DEPS/include -I$DEPS/include/ncursesw"
# LIBDIR, not PATH: PATH is prepended to pkg-config's default search list, which on any Mac with
# Homebrew (every CI runner included) still finds /usr/local and /opt/homebrew — tmux 3.7 then
# linked an Intel Homebrew jemalloc. LIBDIR replaces the list, so only our own .pc files exist.
export PKG_CONFIG_LIBDIR="$DEPS/lib/pkgconfig"
echo ">> libevent $LIBEVENT_VERSION"
(
cd "$WORK/src/libevent"
# A current macOS SDK makes autoconf's link test for pipe2 succeed although macOS has no pipe2;
# the resulting HAVE_PIPE2 sends evutil_make_internal_pipe_ through a null pointer on the first
# new-session. Answered up front, then every Linux-only syscall is re-checked below.
# shellcheck disable=SC2086
ac_cv_func_pipe2=no ./configure $HOST_FLAG --prefix="$DEPS" --disable-shared --enable-static \
--disable-openssl --disable-samples --disable-libevent-regress --disable-debug-mode >/dev/null
if grep -Eq '^#define (EVENT__)?HAVE_(PIPE2|EPOLL|EVENTFD|TIMERFD|ACCEPT4|SIGNALFD|SPLICE)\b' config.h; then
echo "error: libevent's configure detected a Linux-only call on macOS:" >&2
grep -E '^#define (EVENT__)?HAVE_(PIPE2|EPOLL|EVENTFD|TIMERFD|ACCEPT4|SIGNALFD|SPLICE)\b' config.h >&2
exit 1
fi
make -j"$NPROC" >/dev/null 2>&1
make install >/dev/null
)
# ncurses: static, terminfo read from the macOS system database (tmux-256color is there on
# macOS 13+, screen-256color everywhere), so no `tic`, no bundled terminfo, no programs that would
# shadow /usr/bin/clear and friends.
echo ">> ncurses $NCURSES_VERSION"
(
cd "$WORK/src/ncurses"
# shellcheck disable=SC2086
./configure $HOST_FLAG --prefix="$DEPS" \
--with-termlib --without-shared --with-normal --without-debug --without-ada \
--without-progs --without-manpages --without-tests --without-cxx --without-cxx-binding \
--enable-pc-files --with-pkg-config-libdir="$DEPS/lib/pkgconfig" \
--with-default-terminfo-dir=/usr/share/terminfo \
--with-terminfo-dirs=/usr/share/terminfo:/usr/lib/terminfo \
--disable-db-install >/dev/null 2>&1
make -j"$NPROC" >/dev/null 2>&1
make install >/dev/null 2>&1
)
echo ">> tmux $TMUX_VERSION"
(
cd "$WORK/src/tmux"
# Named explicitly rather than left to pkg-config: ncurses 6 installs the wide-character
# `tinfow`, tmux's configure asks pkg-config for `tinfo`, and on a miss it silently links the
# system's ncurses 5.4 against our 6.5 headers — a binary that answers `-V` and then segfaults on
# `new-session`. The .a files above are the only libraries at these paths, so the link is static
# without any macOS-unfriendly -static flag.
LIBEVENT_CORE_CFLAGS="-I$DEPS/include" LIBEVENT_CORE_LIBS="-L$DEPS/lib -levent_core" \
LIBTINFO_CFLAGS="-I$DEPS/include -I$DEPS/include/ncursesw" LIBTINFO_LIBS="-L$DEPS/lib -ltinfow" \
./configure $HOST_FLAG --prefix="$STAGE" --disable-utf8proc --disable-jemalloc >/dev/null
make -j"$NPROC" >/dev/null 2>&1
cp tmux "$STAGE/bin/tmux"
cp COPYING "$STAGE/LICENSE.tmux"
)
cp "$WORK/src/libevent/LICENSE" "$STAGE/LICENSE.libevent"
cp "$WORK/src/ncurses/COPYING" "$STAGE/LICENSE.ncurses"
# Ad-hoc signature (the linker adds one on arm64; make it explicit and identical for x64). Nothing
# here is notarized: curl sets no quarantine flag, so Gatekeeper never sees the binary.
codesign -s - --force "$STAGE/bin/tmux" 2>/dev/null
# The whole point: nothing but the OS may be needed at run time — no package-manager library (a
# Homebrew jemalloc, say), and not the system's ncurses 5.4 either (see the tmux configure note).
if otool -L "$STAGE/bin/tmux" | tail -n +2 | grep -Ev '^\s+/usr/lib/lib(System\.B|resolv\.9|util)\.dylib '; then
echo "error: tmux links a library outside libSystem:" >&2
otool -L "$STAGE/bin/tmux" >&2
exit 1
fi
# Smoke test: version, then a real server on a private socket, under Rosetta for the x64 build.
run() {
if [ "$ARCH" = x86_64 ] && [ "$(uname -m)" = arm64 ]; then arch -x86_64 "$@"; else "$@"; fi
}
run "$STAGE/bin/tmux" -V
SOCKET="$WORK/smoke.sock"
TERM=xterm-256color run "$STAGE/bin/tmux" -S "$SOCKET" -f /dev/null new-session -d -s smoke 'sleep 30'
run "$STAGE/bin/tmux" -S "$SOCKET" list-sessions | grep -q '^smoke:'
run "$STAGE/bin/tmux" -S "$SOCKET" kill-server
ARCHIVE="$OUT_DIR/$ROOT.tar.gz"
tar -czf "$ARCHIVE" -C "$WORK/stage" "$ROOT"
echo ">> built $ARCHIVE ($(wc -c < "$ARCHIVE" | tr -d ' ') bytes, sha256 $(shasum -a 256 "$ARCHIVE" | awk '{print $1}'))"