A static build is compiled version of a program which has been statically linked against libraries, according to Wikipedia. In practice, it means we can run a binary anywhere in compatible environments without installing dependencies. Although the binary is considerably larger, it greatly simplifies deployments.
Learning by Example - cURL
First, we take a look at how a static binary of cURL can be produced:
| |
This snippet is from Travis Burtrum’s static curl repository. There are several important points:
- Install dependencies and their corresponding static libraries
- Configure the static build process
- Build statically
Info
This process is straight-forward, but environmental variables and configuration parameters vary between different programs.
However, several parameters are mostly likely to be used by programs, and we can refer to the manuals if it doesn’t work out.
Environmental variables:
export CC=clang
export LDFLAGS="-static"
export PKG_CONFIG="pkg-config --static"
Build commands:
apk add clang build-base
Configuration parameters:
./configure --disable-shared --enable-shared=no --enable-static --enable-all-static
The detailed parameters for a program can be found by running
./configure --help. Unknown parameters usually result in warnings.
Examples
tmux
tmux is a terminal multiplexer. It only has two dependencies: libevent and ncurses, both of which have static libraries in the official repository.
After downloading and extracting the tmux source code, we can execute the following commands:
# install libevent and ncurses
apk add libevent-dev libevent-static ncurses-dev ncurses-static
# yacc is required in newer versions when building
apk add bison
# configure the build process
./configure --enable-static
# build
make
screen
screen is another terminal multiplexer. Due to having different key prefix, it can be used inside a tmux session without conflicts. It has two dependencies: ncurses and linux-pam which itself needs meson and linux-headers to be built.
Info
We need to compile linux-pam because alpine doesn’t provide a statically built one .To compile linux-pam statically, we need to configure it with meson:
mkdir build && cd build
meson setup ..
meson configure -Ddefault_library=static
ninja
meson install
We need to change the meson.build to use library instead of shared_library for libpam and libpam_misc. We also need to comment the function getpwnam_r inside modules/pam_canonicalize_user/tst-pam_canonicalize_user-retval.c due to multiple definitions of getpwnam_r.
# install ncurses
apk add ncurses-dev ncurses-static
# configure socket directory
./configure --enable-socket-dir=/dev/shm/screen
# build
make
rsync
rsync is an open source utility that provides fast incremental file transfer. Its build dependencies can be found here.
Info
xxHash, lz4 and libidn2 from the official repository lack XXH64_createState, LZ4_compress_default and idn2_lookup_ul functions that rsync requires respectively. All of them need to be compiled manually.
We can enable several optimizations for AMD64 environments, SIMD-roll, asm-roll and asm-MD5. The previous two require a working C++ compiler.
export CXX=clang++
We can use ACL library from the official repository, but we can also compile it from the source to enable ACL-libacl-at. This requires attr-dev and linux-headers to be installed:
apk add attr-dev attr-static linux-headers
# install ACL, xattr, zstd and OpenSSL
apk add acl-dev acl-static attr-dev attr-static zstd-dev zstd-static openssl-dev openssl-libs-static
# compile xxHash, lz4 and libidn2 from source. omitted for brevity
# no extra parameters required. rsync uses environmental variables
./configure --enable-ipv6 --enable-roll-simd --enable-roll-asm --enable-md5-asm
# build
make
sshfs
sshfs can be used to mount access remote file systems using SFTP. However, it can also be used with directport to bypass SSH. A simple SFTP server written in golang can be used to share files over a WireGuard network:
package main
import (
"flag"
"log"
"net"
"github.com/pkg/sftp"
)
func handleSFTP(c net.Conn) {
log.Printf("accepted connection from %s\n", c.RemoteAddr())
server, _ := sftp.NewServer(c, sftp.WithAllocator())
_ = server.Serve()
_ = c.Close()
}
func main() {
addr := flag.String("l", "127.0.0.1:0", "service address")
flag.Parse()
l, err := net.Listen("tcp", *addr)
if err != nil {
panic(err)
}
log.Printf("listening on %s\n", l.Addr())
for {
var c net.Conn
c, err = l.Accept()
if err != nil {
continue
}
go handleSFTP(c)
}
}
As with linux-pam required by screen, sshfs is also compiled this way. Its meson.build file also has problems - it doesn’t look for static libraries when needed:
use_static = get_option('default_library') == 'static'
sshfs_deps = [ dependency('fuse3', version: '>= 3.1.0', static: use_static),
dependency('glib-2.0', static: use_static),
dependency('gthread-2.0', static: use_static) ]
# install pkg-config fuse3 glib-2.0 and the static library of pcre2-8 (dynamic library of pcre2-8 is installed when installing glib-2.0)
apk add pkgconfig fuse3-dev fuse3-static glib-dev glib-static pcre2-static
# build
mkdir build && cd build
meson setup ..
meson configure -Ddefault_library=static
ninja
meson install
rdb-cli
rdb-cli is part of rdblib that provides cli tools to interact with Redis dump files. To build it we’ll also need git to clone it because that’s needed during building to download dependencies.
The only part that needs modification is the Makefile for the cli itself. It resets LDFLAGS to empty and always produces a dynamic binary. Just remove that line and we can get a static binary.
make
aria2
aria2 is the ultra-fast download utility. It can use different libraries for one of its specific features. We’re using OpenSSL because it’s simpler this way.
Info
We have to compile OpenSSL in aria2 version 1.37.0 because prebuilt OpenSSL doesn’t have the legacy provider. Its configuration parameter is no-shared no-module.
OpenSSL requires perl to be configured. It also requires linux-headers to be compiled.
# install dependencies
apk add libssh2-dev libssh2-static libxml2-dev libxml2-static zlib-dev zlib-static c-ares-dev sqlite-dev sqlite-static
## install xz, or in the final stage linking will fail
apk add xz-dev xz-static
# compile OpenSSL from source. omitted for brevity
# configure the compilation process. note that aria2 uses environmental variables to determine whether it's statically built
ARIA2_STATIC=yes ./configure --with-ca-bundle=/etc/ssl/certs/ca-certificates.crt
# build
make
Note
Though c-ares-static is no longer provided in the official repository, the static library needed is included by c-ares-dev in the form of /usr/lib/libcares.a in the package contents.
Info
Although all these can also be built with GCC, there are some differences.
When compiling rsync, there is no need to compile xxHash and lz4 from source as GCC can find the functions it needs. But to enable additional optimizations, besides setting the C++ compiler, compiler flags for the C++ compiler need to be supplied:
# install xxHash and lz4
apk add xxhash-dev xxhash-static lz4-dev lz4-static
# configure C++ compiler
export CXX=g++
export CXXFLAGS="-mavx2"
However, this binary will not fall back to the baseline implementation if the host CPU doesn’t support the avx2 instruction and will crash if any of the unsupported optimizations is used, unlike the one compiled with Clang.
