Add Docker static-qt build support

ober

6f0467b64d16b118a7d50c26221cc9823fa9cd7a

diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..986c0e0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,15 @@
+.git
+.bcache
+build
+**/build
+**/target
+test-artifacts
+gui-snapshots.tar.gz
+jerboa-browser
+jerboa-browser-static-qt-linux-amd64.tar.gz
+*.boot
+*.so
+*.wpo
+jb-main.c
+jb_*.h
+.DS_Store
diff --git a/.gitignore b/.gitignore
index be35c71..a2b1d39 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ build/
 
 # Native binary + Jerboa build artifacts (see build-binary.ss / Makefile)
 /jerboa-browser
+jerboa-browser-static-qt-linux-amd64.tar.gz
 .bcache/
 *.so
 *.wpo
diff --git a/Dockerfile.qt b/Dockerfile.qt
new file mode 100644
index 0000000..1e1fc56
--- /dev/null
+++ b/Dockerfile.qt
@@ -0,0 +1,67 @@
+# Docker recipe for building the jerboa-browser Linux Qt artifact.
+#
+# This mirrors the jerboa-emacs static-qt workflow: build inside Docker, create a
+# container, and copy the finished artifact from /out. Unlike jemacs-qt, this
+# project uses Qt WebEngine, so the result is not a single fully static Qt
+# executable. The tarball contains the native Jerboa browser binary plus the
+# Qt WebEngine backend shared library and expects a system Qt 6 WebEngine
+# runtime, matching packaging/linux/build-tarball.sh.
+#
+# Usage:
+#   docker build -f Dockerfile.qt -t jerboa-browser-qt-builder .
+#   id=$(docker create jerboa-browser-qt-builder)
+#   docker cp $id:/out/jerboa-browser-static-qt.tar.gz . && docker rm $id
+
+ARG JERBOA_IMAGE=jerboa21/jerboa
+FROM ${JERBOA_IMAGE} AS builder
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    ca-certificates \
+    cmake \
+    file \
+    g++ \
+    libgl1-mesa-dev \
+    make \
+    pkg-config \
+    qt6-base-dev \
+    qt6-webengine-dev \
+    cargo \
+    rustc \
+    && rm -rf /var/lib/apt/lists/*
+
+RUN if ! command -v git >/dev/null 2>&1; then \
+      apt-get update && apt-get install -y --no-install-recommends git && \
+      rm -rf /var/lib/apt/lists/*; \
+    fi && \
+    if ! command -v jerbuild >/dev/null 2>&1; then \
+      git clone --depth 1 https://git.sr.ht/~lisp/jerboa /tmp/jerboa && \
+      cd /tmp/jerboa && \
+      JERBOA_HOME=/tmp/jerboa SCHEME="$(command -v scheme)" sh support/build-jerbuild.sh && \
+      install -m 0755 jerbuild /usr/local/bin/jerbuild; \
+    fi && \
+    command -v scheme && \
+    command -v jerbuild && \
+    jerbuild --jerboa-home >/dev/null
+
+COPY . /src
+WORKDIR /src
+
+RUN SCHEME="$(command -v scheme)" bash packaging/linux/build-tarball.sh && \
+    mkdir -p /out && \
+    cp qt-webengine/build/jerboa-browser-*-linux-*.tar.gz \
+      /out/jerboa-browser-static-qt.tar.gz && \
+    tar tzf /out/jerboa-browser-static-qt.tar.gz | \
+      grep -E '/bin/jerboa-browser$' >/dev/null && \
+    tar tzf /out/jerboa-browser-static-qt.tar.gz | \
+      grep -E '/lib/libjerboa_browser.so$' >/dev/null && \
+    bin_path="$(tar tzf /out/jerboa-browser-static-qt.tar.gz | \
+      grep -E '/bin/jerboa-browser$' | head -n 1)" && \
+    tar xzf /out/jerboa-browser-static-qt.tar.gz -O \
+      "$bin_path" > /tmp/jerboa-browser && \
+    file /tmp/jerboa-browser
+
+FROM ubuntu:24.04
+COPY --from=builder /out/jerboa-browser-static-qt.tar.gz /out/jerboa-browser-static-qt.tar.gz
+CMD ["cat", "/out/jerboa-browser-static-qt.tar.gz"]
diff --git a/Makefile b/Makefile
index 4a83347..a7ba097 100644
--- a/Makefile
+++ b/Makefile
@@ -3,10 +3,15 @@
 # full Qt/WebKit GUI bundle additionally needs the native wrapper.)
 JERBUILD ?= jerbuild
 JH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null)
-ifeq ($(JH),)
-$(error jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild)
-endif
 JEXEC       := $(JERBUILD) exec --libdirs $(CURDIR)/scheme:$(JH)/lib
+JERBUILD_ERROR := "ERROR: jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild."
+
+DOCKER ?= docker
+DOCKER_PLATFORM ?= linux/amd64
+DOCKER_BUILD_FLAGS ?=
+JERBOA_IMAGE ?= jerboa21/jerboa
+STATIC_QT_IMAGE ?= jerboa-browser-qt-builder
+STATIC_QT_TARBALL ?= jerboa-browser-static-qt-linux-amd64.tar.gz
 
 # Modules that use (std crypto native-rust) need libjerboa_native dlopen-able
 # when run in the interpreter (jerbuild exec: tests, repl); the compiled binary
@@ -17,7 +22,7 @@ SOEXT := $(if $(filter Darwin,$(shell uname -s)),dylib,so)
 # `make binary` links the static archive from a temp dir and leaves no shared
 # lib behind, so build the cdylib from jerbuild's bundled crate if it isn't
 # already cached, then symlink it where native-rust probes (./lib, CWD-relative).
-lib/libjerboa_native.$(SOEXT):
+lib/libjerboa_native.$(SOEXT): | check-jerbuild
 	@mkdir -p lib
 	@src="$(JH)/jerboa-native-rs/target/release/libjerboa_native.$(SOEXT)"; \
 	 if [ ! -f "$$src" ] && [ -f "$(JH)/jerboa-native-rs/Cargo.toml" ]; then \
@@ -28,13 +33,20 @@ lib/libjerboa_native.$(SOEXT):
 	 if [ -f "$$src" ]; then ln -sf "$$src" "$@" && echo "staged $@ -> $$src"; \
 	 else echo "note: libjerboa_native.$(SOEXT) unavailable; crypto tests may fail"; fi
 
-.PHONY: all build binary run test test-keymap test-minibuffer test-commands test-keys test-nav test-hint test-pass test-polish test-securestore test-adblock test-gui test-buffers repl clean help
+.PHONY: all build binary run test test-keymap test-minibuffer test-commands test-keys test-nav test-hint test-pass test-polish test-securestore test-adblock test-gui test-buffers repl clean help check-jerbuild check-docker static-qt docker-static-qt
 .DEFAULT_GOAL := help
 
 all: binary
 
+check-jerbuild:
+	@test -n "$(JH)" || { echo $(JERBUILD_ERROR) >&2; exit 1; }
+
+check-docker:
+	@command -v $(DOCKER) >/dev/null 2>&1 || \
+	  { echo "ERROR: Docker executable '$(DOCKER)' not found. Install Docker or set DOCKER=/path/to/docker."; exit 1; }
+
 # Build the self-contained native ./jerboa-browser via .jerbuild.
-binary:
+binary: check-jerbuild
 	$(JERBUILD) build
 	@echo "" && ls -lh jerboa-browser && file jerboa-browser
 
@@ -46,6 +58,7 @@ run: binary
 # The crypto-using interpreter suites load (std crypto native-rust), so stage the
 # Rust crypto lib (rule above) as a prerequisite of each.
 test-securestore test-commands test-keys test-nav test-hint test-pass test-polish test-adblock: lib/libjerboa_native.$(SOEXT)
+test test-keymap test-minibuffer test-gui test-buffers repl: check-jerbuild
 
 # Run the (browser) test suite in the interpreter (no binary needed). Depends on
 # the pure secure-store unit tests so they run as part of `make test`.
@@ -122,8 +135,30 @@ test-buffers:
 repl:
 	$(JEXEC) scheme/browser-repl.ss
 
+# Docker wrapper matching the jerboa-emacs static-qt workflow. Qt WebEngine is
+# not emitted as one fully static executable; the artifact is the existing Linux
+# tarball shape with the native browser binary plus system-Qt WebEngine backend.
+static-qt: docker-static-qt
+
+docker-static-qt: check-docker
+	@echo "=== Building jerboa-browser Linux Qt artifact with Docker ==="
+	$(DOCKER) pull $(JERBOA_IMAGE)
+	$(DOCKER) build $(DOCKER_BUILD_FLAGS) --platform $(DOCKER_PLATFORM) \
+	  --build-arg JERBOA_IMAGE="$(JERBOA_IMAGE)" \
+	  -f Dockerfile.qt -t $(STATIC_QT_IMAGE) .
+	@id=$$($(DOCKER) create $(STATIC_QT_IMAGE)) && \
+	  trap '$(DOCKER) rm $$id >/dev/null 2>&1 || true' EXIT INT TERM && \
+	  $(DOCKER) cp $$id:/out/jerboa-browser-static-qt.tar.gz ./$(STATIC_QT_TARBALL)
+	@echo ""
+	@ls -lh $(STATIC_QT_TARBALL)
+	@tar tzf $(STATIC_QT_TARBALL) | grep -E '/bin/jerboa-browser$$' >/dev/null || \
+	  { echo "ERROR: native binary missing from $(STATIC_QT_TARBALL)" >&2; exit 1; }
+	@tar tzf $(STATIC_QT_TARBALL) | grep -E '/lib/libjerboa_browser.so$$' >/dev/null || \
+	  { echo "ERROR: Qt WebEngine backend missing from $(STATIC_QT_TARBALL)" >&2; exit 1; }
+
 clean:
 	rm -f jerboa-browser jb-main.c jb-main.o jb_*.h jerboa-browser.boot
+	rm -f $(STATIC_QT_TARBALL)
 	rm -rf .bcache test-artifacts
 	find scheme -name '*.so' -delete 2>/dev/null || true
 	find scheme -name '*.wpo' -delete 2>/dev/null || true
@@ -132,6 +167,7 @@ help:
 	@echo "jerboa-browser — programmable Qt WebEngine pane from the Jerboa REPL"
 	@echo ""
 	@echo "  make binary   build the self-contained native ./jerboa-browser"
+	@echo "  make static-qt build the Linux Qt/WebEngine tarball via Docker"
 	@echo "  make test     run the (browser) test suite (interpreter)"
 	@echo "  make test-gui offline Qt GUI/snapshot harness (offscreen, headless)"
 	@echo "  make repl     REPL with (browser) preloaded (interpreter)"
diff --git a/README.md b/README.md
index 92be7a9..dc1d72f 100644
--- a/README.md
+++ b/README.md
@@ -83,11 +83,18 @@ and run `pcscd`.
 
 ```sh
 make binary           # the self-contained native ./jerboa-browser
+make static-qt        # Linux Qt/WebEngine tarball via Docker (linux/amd64)
 make test             # (browser) suite + secure-store unit tests; also test-{keymap,…,polish}
 make test-securestore # just the pure secure-store crypto/format unit tests
 make test-gui         # offline Qt snapshot harness (offscreen, headless)
 ```
 
+`make static-qt` mirrors the jerboa-emacs Docker workflow and copies out
+`jerboa-browser-static-qt-linux-amd64.tar.gz`. Because this backend is Qt
+WebEngine, the artifact is a Linux tarball with the native browser binary plus
+`libjerboa_browser.so`; it still expects a system Qt 6 WebEngine runtime rather
+than a literal one-file static WebEngine executable.
+
 The functional suites run offscreen (`QT_QPA_PLATFORM=offscreen`) and hermetic
 (`JWB_TEST_NO_NETWORK=1`), modeled on the jerboa-emacs Qt test harness.
 
diff --git a/packaging/README.md b/packaging/README.md
index 4b7b758..252d59a 100644
--- a/packaging/README.md
+++ b/packaging/README.md
@@ -96,18 +96,21 @@ Total ≈ 299 MB (dominated by QtWebEngineCore + ICU).
 
 ```sh
 packaging/linux/build-tarball.sh      # → qt-webengine/build/jerboa-browser-<ver>-linux-<arch>.tar.gz
+make static-qt                        # Docker build, copies ./jerboa-browser-static-qt-linux-amd64.tar.gz
 ```
 
-> **Status: not verified on this host.** Development is on macOS, so the Linux
-> script is provided and reviewed but has not been executed end-to-end here. The
-> tarball bundles the backend `libjerboa_browser.so`, the Jerboa `scheme/`
-> sources, and `build-binary.ss` + `Makefile`. When the Jerboa toolchain (Chez +
-> Jerboa stdlib) is present on the build host it also compiles and bundles the
-> self-contained native `bin/jerboa-browser` — a Jerboa program with the Chez
-> kernel, boot image, and the `(browser)` library all embedded (built by
-> `build-binary.ss`, **not** a C/C++ launcher); otherwise unpack and run `make
-> binary` to produce it. Either way it relies on a **system Qt 6 WebEngine**
-> install (documented prerequisite) rather than vendoring Qt.
+The tarball bundles the backend `libjerboa_browser.so`, the Jerboa `scheme/`
+sources, and `build-binary.ss` + `Makefile`. When the Jerboa toolchain (Chez +
+Jerboa stdlib) is present on the build host it also compiles and bundles the
+self-contained native `bin/jerboa-browser` — a Jerboa program with the Chez
+kernel, boot image, and the `(browser)` library all embedded (built by
+`build-binary.ss`, **not** a C/C++ launcher); otherwise unpack and run `make
+binary` to produce it. Either way it relies on a **system Qt 6 WebEngine**
+install (documented prerequisite) rather than vendoring Qt.
+
+`make static-qt` wraps that Linux package path in Docker, using the
+`jerboa21/jerboa` image plus Ubuntu Qt WebEngine packages, and has been verified
+on `linux.cons.io` (`linux/amd64`).
 
 For a fully self-contained Linux artifact, run the bundle through
 [`linuxdeployqt`](https://github.com/probonopd/linuxdeployqt) to produce an
diff --git a/qt-webengine/CMakeLists.txt b/qt-webengine/CMakeLists.txt
index 0c09f11..2d0e2a2 100644
--- a/qt-webengine/CMakeLists.txt
+++ b/qt-webengine/CMakeLists.txt
@@ -40,11 +40,18 @@ target_link_libraries(jerboa_browser
 option(JWB_ADBLOCK "Ad/tracker blocking via vendored adblock-rust (needs cargo)" ON)
 if(JWB_ADBLOCK)
   set(ADBLOCK_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/adblock-rust-ffi)
-  set(ADBLOCK_LIB ${ADBLOCK_DIR}/target/release/libadblock.a)
+  set(ADBLOCK_TARGET_DIR ${CMAKE_CURRENT_BINARY_DIR}/adblock-rust-target)
+  set(ADBLOCK_LIB ${ADBLOCK_TARGET_DIR}/release/libadblock.a)
   find_program(CARGO_EXE cargo REQUIRED)
+  set(ADBLOCK_RANLIB_COMMAND)
+  if(CMAKE_RANLIB)
+    list(APPEND ADBLOCK_RANLIB_COMMAND COMMAND ${CMAKE_RANLIB} ${ADBLOCK_LIB})
+  endif()
   add_custom_command(
     OUTPUT ${ADBLOCK_LIB}
-    COMMAND ${CARGO_EXE} build --release --manifest-path ${ADBLOCK_DIR}/Cargo.toml
+    COMMAND ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${ADBLOCK_TARGET_DIR}
+            ${CARGO_EXE} build --release --manifest-path ${ADBLOCK_DIR}/Cargo.toml
+    ${ADBLOCK_RANLIB_COMMAND}
     COMMENT "Building vendored adblock-rust staticlib (cargo build --release)"
     VERBATIM)
   add_custom_target(adblock_rust DEPENDS ${ADBLOCK_LIB})