Skip to main content

Portable Tmux & Neovim

Overview

As of late, I've been attempting to catch up with the flurry of new developer tools that now exists. Many of the TUI tools are golang or rust and therefore have nice portable static versions. This means one single binary that has all the things I need to use the tool. It goes in my ~/.local/bin and that is that (presuming you aren't ricing in catppuccin).

Now tmux and nvim are central to all of this portability and yet they remain relatively non-portable and are themselves written in C and C++ respectively. I'd like to fix this.

Tmux

Tmux is the heart of the terminal and its pane/window/session management. I generally don't acknowledge anything before tmux-3.0 because before that it was a different config format and I usually opted for multiple windows and tabs with my terminal emulator. Since, tmux 3.0, I've been using a pretty simply configuration with some specific colors, pane labels, and the mouse turned on (for pane resizing).

For reasons specifically unknown to me at this time, it appears tmux-3.3 has made some leaps in its capabilities in regards to all the new dev tool hotness on the net. The trouble is that not all of my VMs and installed distributions have a out-of-the-box tmux that is 3.3+. Instead of one off building tmux for these older systems, I want a statically built tmux that I can drop in my ~/.local/bin and be done with it. Ideally I would be able to check this into my .dotfiles repository and stow it into my home directory everywhere.

To facilitate this, I'm using docker to isolate system dependencies from the build. I've tried to use the newest dependencies and I'm using musl as the libc because of its size and its ability to run completely static (unlike vanilla glibc).

Here is the Dockerfile:

FROM debian:12

RUN apt-get update && apt-get install -y \
musl-tools build-essential curl pkg-config yacc

ENV TOP_DIR=/build-static-tmux
ENV PREFIX=$TOP_DIR/local
ENV CC=musl-gcc
ENV CFLAGS="-static -I$PREFIX/include"
ENV LDFLAGS="-static -L$PREFIX/lib64 -L$PREFIX/lib"
ENV CPPFLAGS="-I$PREFIX/include"

WORKDIR $TOP_DIR

RUN curl -LO https://github.com/libevent/libevent/releases/download/release-2.1.12-stable/libevent-2.1.12-stable.tar.gz \
&& curl -LO https://ftp.gnu.org/pub/gnu/ncurses/ncurses-6.5.tar.gz \
&& curl -LO https://github.com/tmux/tmux/releases/download/3.5a/tmux-3.5a.tar.gz \
&& curl -LO https://github.com/openssl/openssl/releases/download/openssl-3.5.0/openssl-3.5.0.tar.gz

RUN tar xf libevent-2.1.12-stable.tar.gz \
&& tar xf ncurses-6.5.tar.gz \
&& tar xf tmux-3.5a.tar.gz \
&& tar xf openssl-3.5.0.tar.gz

WORKDIR $TOP_DIR/openssl-3.5.0
RUN ./Configure no-shared no-dso no-tests no-secure-memory no-afalgeng \
--prefix=$PREFIX linux-x86_64 \
&& make -j$(nproc) && make install_sw

WORKDIR $TOP_DIR/libevent-2.1.12-stable
RUN ./configure --prefix=$PREFIX --disable-shared --enable-static \
--with-pic \
CC=$CC CPPFLAGS="$CPPFLAGS" CFLAGS="$CFLAGS" \
LDFLAGS="$LDFLAGS" LIBS="-lssl -lcrypto" \
&& make -j$(nproc) && make install

WORKDIR $TOP_DIR/ncurses-6.5
RUN ./configure --prefix=$PREFIX --with-normal --with-static --without-shared \
--without-debug --without-ada --enable-widec --datarootdir=/lib \
CC=$CC CFLAGS="$CFLAGS" \
&& make -j$(nproc) && make install

WORKDIR $TOP_DIR/tmux-3.5a
# Do some funky patchwork.
RUN sed -i '/NEED_FORKPTY_TRUE/d' Makefile.in \
&& cp $PREFIX/lib/libncursesw.a $PREFIX/lib/libncurses.a
RUN PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" ./configure --prefix=$PREFIX \
--enable-static CC=$CC CFLAGS="$CFLAGS -DHAVE_FORKPTY" LDFLAGS="$LDFLAGS" \
CPPFLAGS="-I$PREFIX/include -I$PREFIX/include/ncursesw" \
LIBS="-lncursesw -levent" \
&& make -j$(nproc)

The above Dockerfile is run with:

docker build -t tmux-build .

Once the build has completed, extract the tmux binary with:

C=$(docker create tmux-build);docker cp $C:/build-static-tmux/tmux-3.5a/tmux .; docker rm $C

Note: tmux requires terminfo. This has been hardcoded into the build to exist at /lib/terminfo. If you want to change this, update the --datarootdir to the appropriate prefix in the ncurses configure script.

Yay, now we have a static tmux.

Nvim

Unlike tmux, nvim has some dependencies that really don't want to be built statically. We work around this by using AppImage. The caveat with AppImage is that your system will need fuse. If this is not acceptable, you can lug around all the extra files or a tarball. I'm giving AppImage a go for simplicity.

One of the other issues with prebuilt nvim is that its built with newer glibc versions. This means it does not work on older distributions like Ubuntu Focal (20.04). To get around this, we're forced to build our own. Lucily, once you have the script up and running (with docker and AppImage), you can easily add support for older distrobutions by simply rewinding the base image in the Dockerfile to an appropriate generation.

The following is a bit funky because we can not use fuse inside of a docker build context. Therefore we build the neovim package in the Docker environment and then use linuxdeploy to assemble the AppImage. I wanted to keep this simple, so I've combined the Dockerfile content and the follow-on AppImage build into a single script.

#!/bin/bash

docker build -t nvim-build - <<END_OF_DOCKERFILE
FROM ubuntu:focal

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y \
build-essential ninja-build gettext cmake unzip curl

WORKDIR /build
RUN curl -LO https://github.com/neovim/neovim/archive/refs/tags/v0.11.2.tar.gz \
&& tar -xf v0.11.2.tar.gz

WORKDIR /build/neovim-0.11.2
RUN make CMAKE_BUILD_TYPE=Release \
&& make CMAKE_INSTALL_PREFIX=/neovim-0.11.2 install

WORKDIR /
RUN tar -czf neovim-0.11.2.tar.gz neovim-0.11.2
END_OF_DOCKERFILE

C=$(docker create nvim-build);docker cp $C:neovim-0.11.2.tar.gz .;docker rm $C
tar -xf neovim-0.11.2.tar.gz
strip neovim-0.11.2/bin/nvim

curl -LO https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
curl -LO https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage
chmod +x linuxdeploy-*

mkdir Neovim.AppDir
mv neovim-0.11.2 Neovim.AppDir/usr
./linuxdeploy-x86_64.AppImage \
--appdir Neovim.AppDir \
--desktop-file Neovim.AppDir/usr/share/applications/nvim.desktop \
--icon-file Neovim.AppDir/usr/share/icons/hicolor/128x128/apps/nvim.png \
--output appimage

mv Neovim-x86_64.AppImage nvim

Conclusion

Hurray, I now have a straight forward tmux and nvim that can go into my ~/.local/bin and not think to much about it. Like I mentioned before, in the event that something doesn't work, I should be able to rebuild specifically for that distribution in the Docker environment, rerun the linux deploy and we'll have a freshly baked set of tools.

Of course as major versions progress, all things die. :)

Comments