diff --git a/Dockerfile.compat b/Dockerfile.compat index 27a4a3b0f..873c3d394 100644 --- a/Dockerfile.compat +++ b/Dockerfile.compat @@ -1,4 +1,8 @@ -FROM ubuntu:20.04 +# Base image is parameterised so one Dockerfile can produce binaries for several +# glibc floors: docker build --build-arg BASE_IMAGE=ubuntu:18.04 ... +# The default is unchanged, so `./build.sh --linux-compat` behaves exactly as before. +ARG BASE_IMAGE=ubuntu:20.04 +FROM ${BASE_IMAGE} ENV DEBIAN_FRONTEND=noninteractive diff --git a/doc/build-containers.md b/doc/build-containers.md new file mode 100644 index 000000000..1c8e7f70b --- /dev/null +++ b/doc/build-containers.md @@ -0,0 +1,223 @@ +# Building release binaries in containers + +Release binaries must be built in a container based on an **old** Linux distribution. +This document is written to be executed, by a person or an agent, on a machine that +has nothing set up yet. + +--- + +## 1. Why this exists + +glibc compatibility runs one way only. A binary linked against glibc 2.35 demands +symbol versions that glibc 2.31 does not have, and refuses to start. A binary linked +against glibc 2.29 runs on 2.29, 2.31 and 2.35 alike. + +Measured on the actual fleet, 2026-08-25: + +| binary | max GLIBC required | runs on | +|---|---|---| +| what all four 20.04 seeds run today (`v1.0.3-d159e7208`) | `GLIBC_2.29` | 18.04, 20.04, 22.04 | +| anything built on seed 176 today (Ubuntu 22.04) | `GLIBC_2.34` | 22.04 only | + +The second binary will not start on four of our own five seeds. The loader reports: + +``` +/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found +/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.30' not found +``` + +Nothing new is being *called*. glibc 2.34 merged libpthread and libdl into libc and +re-versioned every `pthread_*`, `dlsym` and `dladdr` symbol; 2.33 replaced the old +`__xstat` inlines with real `stat`/`fstat`/`lstat64`. All of those functions exist in +2.31 under older tags. Building against older headers is the entire fix. + +**Do not try to solve this with full static linking.** The daemon calls `getaddrinfo`, +`gethostbyname` and `getnameinfo`, and it must resolve `node1..node5.dragonx.is`, which +are hard-coded and injected into `-addnode` on every node. Under a fully static glibc +binary those go through NSS, which `dlopen`s `libnss_dns.so.2` at run time — it either +fails or silently requires the target to have the same glibc you linked against, which +defeats the purpose. + +--- + +## 2. What already exists in this repo + +Do not write a new build system. Two pieces are already here: + +- **`Dockerfile.compat`** — an Ubuntu base image that installs the toolchain, copies the + tree, **deletes any host-built `depends/` and object files**, runs `./util/build.sh`, + and strips the three binaries. +- **`./build.sh --linux-compat`** — builds that image, creates a throwaway container, + copies `dragonxd`, `dragonx-cli` and `dragonx-tx` out into + `release/dragonx--linux-amd64-ubuntu2004/`, adds `bootstrap-dragonx.sh`, + `asmap.dat` and the two sapling params, fixes ownership, and prints the binary's + maximum required GLIBC version. + +The base image is parameterised via `ARG BASE_IMAGE` (default `ubuntu:20.04`), so the +same Dockerfile can target several glibc floors. + +--- + +## 3. Prerequisites + +Docker (the scripted path uses `docker` specifically; podman works for the manual path +if you alias or substitute it). + +```sh +sudo apt-get update +sudo apt-get install -y docker.io git +sudo usermod -aG docker "$USER" # then log out and back in, or every command needs sudo +``` + +Budget, measured on a 4-core box: + +| resource | needs | +|---|---| +| disk | ~15 GB free (the `depends/` tree alone is ~1.6 GB per target, plus image layers) | +| RAM | 4 GB minimum, 8 GB comfortable — the link step is the peak | +| time | **1–2 hours per base image on first build.** `depends/` builds boost, BDB, wolfssl, libevent, libsodium, libcurl and rust from source. Later builds reuse Docker layer cache unless the tree changed. | + +`depends/` downloads and builds its own rust toolchain, so the host's rust (or absence +of it) is irrelevant. + +--- + +## 4. Build one target + +```sh +git clone https://git.dragonx.is/DragonX/dragonx +cd dragonx +git checkout + +./build.sh --linux-compat +``` + +Output lands in `release/dragonx--linux-amd64-ubuntu2004/` and the script +prints the max GLIBC at the end. `` is read from `configure.ac`, not +hardcoded, so it always matches what the binaries report. + +--- + +## 5. Build several targets + +```sh +for BASE in ubuntu:18.04 ubuntu:20.04 ubuntu:22.04; do + TAG="dragonx-compat-${BASE#ubuntu:}" + TAG="${TAG//./}" + docker build --build-arg "BASE_IMAGE=$BASE" -f Dockerfile.compat -t "$TAG" . + + OUT="release/dragonx-$(grep -oP 'define\(_CLIENT_VERSION_MAJOR, \K[0-9]+' configure.ac).$(grep -oP 'define\(_CLIENT_VERSION_MINOR, \K[0-9]+' configure.ac).$(grep -oP 'define\(_CLIENT_VERSION_REVISION, \K[0-9]+' configure.ac)-linux-amd64-${BASE#ubuntu:}" + mkdir -p "$OUT" + CID=$(docker create "$TAG") + for b in dragonxd dragonx-cli dragonx-tx; do docker cp "$CID:/build/src/$b" "$OUT/$b"; done + docker rm "$CID" >/dev/null + cp util/bootstrap-dragonx.sh contrib/asmap/asmap.dat sapling-output.params sapling-spend.params "$OUT/" 2>/dev/null || true +done +``` + +### Which base to choose + +| base | glibc it provides | default GCC | verdict | +|---|---|---|---| +| `ubuntu:18.04` | 2.27 | 7 | **Verify before relying on it.** The tree is built with `-std=c++17`; GCC 7's C++17 support is incomplete and its cmake (3.10) may be too old for RandomX. Attempt only if you need to reach 18.04 users, and treat a successful build as the proof. | +| `ubuntu:20.04` | 2.31 | 9 | **Recommended floor.** GCC 9 covers C++17 fully. Evidence it works: the binary the fleet runs today requires only `GLIBC_2.29`, i.e. the code touches nothing newer, so a 20.04 build reaches 18.04 machines anyway. | +| `ubuntu:22.04` | 2.35 | 11 | **Do not ship this.** It is what we already have and what excludes four of our own seeds. Useful only for development. | + +Ubuntu 20.04 left standard support in April 2025, which is precisely why it belongs in +a container on a patched host rather than on a build box someone has to maintain. + +--- + +## 6. Verify — this step is not optional + +A build that silently targets the wrong glibc looks completely normal until a user +reports that nothing starts. + +```sh +BIN=release/dragonx--linux-amd64-ubuntu2004/dragonxd + +# The ceiling. Must be <= the glibc of the OLDEST system you intend to support. +objdump -p "$BIN" | grep -oE 'GLIBC_2\.[0-9]+' | sort -t. -k2 -n | tail -1 +objdump -p "$BIN" | grep -oE 'GLIBCXX_3\.4\.[0-9]+' | sort -t. -k3 -n | tail -1 + +# If the ceiling is too high, this names the symbols responsible. +readelf --dyn-syms --wide "$BIN" | grep -E '@GLIBC_2\.(3[2-9])' +``` + +Expected for a 20.04 build: `GLIBC_2.29` or lower, `GLIBCXX_3.4.26` or lower. + +Then actually run it somewhere old. A ceiling check proves the loader will resolve the +symbols; it does not prove the binary works. `./dragonxd --version` on a real 20.04 box +is a ten-second confirmation. + +--- + +## 7. Traps + +Each of these has cost real time. + +**`ETXTBSY` when installing over a running daemon.** `cp` onto the binary fails with +"Text file busy" *even after the process has exited* — `pgrep` returning nothing is not +sufficient, the kernel still holds the text mapping. Stage into the same directory and +`mv` (rename is not blocked), allow ~10 s to settle, and **sha256-verify the installed +file before starting it**. A failed copy that goes unnoticed leaves the old binary +running and looks like a successful deploy. + +**Never touch `configure.ac` in a configured tree.** Even `cp`-ing back a byte-identical +copy updates its mtime, which makes `make` regenerate `aclocal.m4` and `configure` and +then re-run `configure`, which fails with `libdb_cxx headers missing` because the +depends prefix is not on the command line. If it happens: confirm +`git diff --quiet HEAD -- configure.ac`, then restore mtime order oldest-to-newest with +one-second gaps — `configure.ac`/`Makefile.am`, then `aclocal.m4`, then +`configure`/`Makefile.in`, then `config.status`, then `Makefile`. Inside a container +this cannot happen, which is one more reason to build there. + +**Never blind-`touch` a path that might not exist.** `touch src/config/hush-config.h` +silently *creates* an empty stray file; the real header is `bitcoin-config.h`. Check +`git status` after any timestamp surgery. + +**RandomX must be built with `ARCH=default`.** `util/build.sh` already passes it and the +comment there explains why: `ARCH=native` tunes to the build machine, and a build on an +AVX-512 host emitted 746 `zmm` instructions into `librandomx.a`, which `SIGILL`s on the +entire fleet. If you ever invoke cmake by hand, pass `-DARCH=default`. + +**Strip before distributing.** Unstripped is ~220 MB, stripped ~16 MB. `Dockerfile.compat` +already strips inside the container. + +**`util/build-win.sh` discards every argument.** There is no `"$@"` handling in it, so +`-j$(nproc)` and `--disable-tests` are dropped on the floor and the Windows build is +single-threaded. Expect it to be far slower than you planned. + +**Windows also needs `-Wa,-mbig-obj` and `-DARCH=default`.** Both are in +`util/build-win.sh` today. The mingw flag was missing from `dev` for a month; without it +the cross-compile fails at link because boost-heavy translation units exceed the +PE/COFF section limit. Do not lose it on a re-branch. + +**macOS cannot be containerised.** `util/build-mac.sh` is a native-Mac script, and there +is no darwin cross-compile path in `depends/` at all: `hosts/darwin.mk` wants +`native_cctools`, which has no package definition, and there is no SDK in the tree. It +also hardcodes an Intel Homebrew GCC path, so it produces x86_64 only — no arm64, no +universal binary. macOS needs a real Mac. + +**The `contrib/gitian-descriptors/` files are not a build path.** They are unmodified +upstream Bitcoin files (`name: "bitcoin-win-0.11"`, suite `trusty`) with zero DragonX +content. Ignore them. + +--- + +## 8. Handoff checklist + +- [ ] Docker installed, user in the `docker` group, ~15 GB free +- [ ] Correct tag or branch checked out, tree clean (`git status`) +- [ ] Version in `configure.ac` is the one you intend to release +- [ ] `./build.sh --linux-compat` completes +- [ ] GLIBC ceiling is **2.31 or lower** (2.29 expected) +- [ ] GLIBCXX ceiling is **3.4.28 or lower** (3.4.26 expected) +- [ ] `dragonxd --version` runs on a real machine of the oldest supported distro +- [ ] Binaries stripped, `release/` contains the bootstrap script, `asmap.dat` and both sapling params +- [ ] sha256 recorded for each artifact + +One more thing that is not a build step but belongs in the same conversation: the +in-app daemon updater refuses any release without a detached signature +(`kDaemonRequireSignature = true`). Publishing checksums alone means no existing user +can update in place.