master
Three chained jobs per arch connected by a duckdb-binary-<arch> artifact (unstripped binary + wireduck test pcap). Isolates stage failures and allows re-running a single stage (e.g. re-verify) without a full rebuild. arm64 build stage still exceeds the runner's 3h default deadline under QEMU — needs a runner-side timeout raise or native cross-compile.
duckdb-static-build
Build a fully-static DuckDB v1.5.5 CLI for Linux (glibc/Ubuntu 24.04) with 50 statically-linked extensions, including a patched WireDuck extension (lazy-load + silent output). Builds run in Docker on both amd64 (native) and arm64 (via QEMU), triggered from Gitea Actions.
Layout
| Path | Purpose |
|---|---|
docker/Dockerfile.build |
Ubuntu 24.04 + toolchain + pinned vcpkg builder image (multi-arch) |
scripts/build.sh |
Fetch pinned duckdb/wireduck sources, apply patches, make extension_configuration + static release build, stage out/duckdb |
scripts/verify.sh |
Post-build verification of out/duckdb (staticness, version, 50 static extensions, output cleanliness, GGUF inference smoke test) |
scripts/package.sh |
Split debug symbols, strip, checksum, produce out/dist/*.tar.zst |
extensions_static.cmake |
Extension config selecting the 50 statically-linked extensions |
patches/wireduck/ |
WireDuck patches (*.patch) + wireduck_override.cmake |
.gitea/workflows/build.yml |
CI: dual-arch build, artifact upload, tag-triggered release |
build/ |
Reference checkout of duckdb v1.5.5 (ground-truth docs; gitignored, not used by the build itself) |
Local build quickstart
docker build -f docker/Dockerfile.build -t duckdb-static-builder:local .
# build (sources are cloned into ./build/, binary staged at ./out/duckdb)
docker run --rm -v "$PWD":/work duckdb-static-builder:local
# verify
docker run --rm -v "$PWD":/work --entrypoint /bin/bash \
duckdb-static-builder:local /opt/scripts/verify.sh
# package (writes ./out/dist/*.tar.zst)
docker run --rm -v "$PWD":/work --entrypoint /bin/bash \
duckdb-static-builder:local /opt/scripts/package.sh
Useful env overrides for build.sh (pass with docker run -e): DUCKDB_TAG,
DUCKDB_COMMIT, JOBS, EXTENSION_CONFIGS, EXTRA_CMAKE_VARIABLES.
CI
- Trigger: push a tag matching
v*, or manualworkflow_dispatch(inputarch:both(default),amd64,arm64). - Pipeline: three staged jobs per arch —
build→verify→package— chained by aduckdb-binary-<arch>artifact (unstrippedout/duckdb+ wireduck test pcap). Stage isolation means a failed stage can be re-run alone (e.g. re-verify without a full rebuild). The builder image is rebuilt per stage (runner cache is not shared). - Matrix:
amd64builds natively;arm64builds the same image and runs the container withdocker --platform linux/arm64(requires QEMU/binfmt on the runner — see comments in the workflow file). Note the arm64buildstage alone takes ~5h under QEMU, past this Gitea instance's 3h default job deadline. - Artifacts: per-arch uploads (
duckdb-static-<arch>) from the package stage, and on tag pushes a Gitea Release is created with all dist files attached.
Artifacts
| File | Contents |
|---|---|
duckdb-static-linux-<arch>.tar.zst |
stripped duckdb binary + SHA256SUMS (covers both archives) |
duckdb-static-linux-<arch>-debug.tar.zst |
duckdb.debug split debug symbols |
<arch> is amd64 or arm64.
Verification summary (scripts/verify.sh)
filereports a statically linked ELF (no "dynamically");lddreportsnot a dynamic executableduckdb --versioncompletes in under 2 seconds and reportsv1.5.5SELECT count(*) FROM duckdb_extensions() WHERE install_mode='STATICALLY_LINKED'returns exactly 50duckdb --csv -c "SELECT 42"first stdout line is exactly42; no[WireDuck]lines in--versionor query output- no
initializing glossarynoise in--versionoutput - https fetch smoke test (TLS + DNS via c-ares) returns 200 (skipped offline)
read_pcapfunctional smoke test over the wireduck test pcap (needs tshark)- laduck GGUF inference smoke test:
llm_load_model+llm_complete+llm_unload_modelwith ggml-org's 260K-param stories model (~1.3 MB, downloaded intobuild/models/; skipped offline)
Notes
- DuckDB is built with the v1.5.5 static-extension mechanism
(
make extension_configurationmerged-vcpkg workflow +USE_MERGED_VCPKG_MANIFEST=1,EXTENSION_CONFIGSpointing atextensions_static.cmakeand the wireduck override). The Makefile target ismake release(there is nomake buildtarget in v1.5.5); full static linking is forced via-DCMAKE_EXE_LINKER_FLAGS=-static. - Sources are pinned by commit: duckdb
d8cdaa33fda8df955cc76ef58a280f68f4cd43fa(tagv1.5.5), wireduck19c4018cc8ebad08547d621bcdc25df86294ca2b. - laduck pins
hamidr/laduck@330c946with llama.cpp (third_party/llama.cpp@69c28f1) built as a static subproject — local GGUF inference (llm_complete/llm_embed/llm_classify) runs in-process, no server/subprocess. Upstream is dormant; the pin is effectively owned by this repo.
Known quirks of this binary
- Process exit skips global teardown on purpose
(
scripts/static_cli_runtime_overrides.c, linked into theshelltarget): a fully-static binary bundling OpenSSL + libmariadbclient + two Rust staticlibs + 50 extensions has cross-library atexit handlers that race and double-free after all real work is done.exit()flushes stdio and calls_exit();getenv()force-returnsQUERY_FARM_TELEMETRY_OPT_OUT=1so query-farm extensions never spawn their startup telemetry threads (their concurrent first-use OpenSSL init races in this build). Side effect: memory is not freed at exit (invisible for a CLI). - fts:
PRAGMA create_fts_index('main.t', 'x', 'txt')— qualify the table name (or quote it); the bare unqualified form misparses in this fts pin against duckdb v1.5.5. Query withfts_main_<table>.match_bm25(...). Note upstream fts (v1.5.5 pin) also drops some tokens during indexing — verified identical on a reference v1.5.5-dev23 build, not a static-build artifact. - Rust extensions (prql, lindel): the two Rust archives embed duplicate
copies of three
stdsymbols (EMPTY_PANIC,ARGV_INIT_ARRAY,rust_eh_personality); the link is permitted via-Wl,--allow-multiple-definition(verified: exactly these three duplicates exist and they are bit-identical). - h3 bridge: h3's vendored C-API header is an older API table (2856 B)
than duckdb v1.5.5's (4368 B); the global table symbol is defined once with
the v1.5.5 type in
src/h3_static_api_init.cpp(seepatches/h3/). Without that, the API-table copy overflows into adjacent.bssand corrupts unrelated globals (OpenSSL'sdefault_context_intamong others). - curl is built against c-ares: static glibc cannot dlopen NSS modules,
which crashed hostname resolution (SIGFPE inside getaddrinfo). The merged
vcpkg manifest patches curl's
resolvefeature to c-ares.
Languages
Shell
59.8%
CMake
27.9%
C
12.3%