36 KiB
Reproducible Docker Build System for a Tarball-Supplied Upstream Project
Executive summary
The right way to containerize a tarball-supplied upstream build is to make the build closed over the build context: put the upstream tarball and every third-party source archive in packages/, pass only paths as build arguments, verify checksums inside the build, extract archives explicitly, and remove every build-time git clone, wget, or other network fetch from the Dockerfile and the upstream Makefile. For the final output, use a multi-stage build and export only artifacts, not the entire toolchain image. Docker’s official docs support exactly this pattern: build arguments to parameterize builds, multi-stage builds to shrink outputs, .dockerignore to keep the context tight, and the local exporter to write build artifacts directly to the host filesystem. citeturn9view6turn9view10turn15view0turn10view1turn9view12
For the supplied ASK archive specifically, direct inspection of the uploaded tarballs shows a C/C++ and kernel-module build for entity["company","NXP","semiconductor company"] Layerscape hardware, a Debian-oriented cross-build setup, hardcoded network fetches for fmlib, fmc, libnfnetlink, and libnetfilter_conntrack, and a separate kernel tree requirement through KDIR. The ASK tarball also contains a .git/ directory, so version generation must be normalized if you want reproducible tarball-based results. The supplied kernel archive expands as linux-lf-6.12.49-2.2.0/. Those facts drive the concrete implementation below.
The Alpine conversion is practical, but only if you treat musl seriously. Alpine uses musl, not glibc, and Alpine’s own docs warn that musl does not implement most glibc locale behavior. The musl docs also call out common failure modes: glibc-specific #ifdefs, GNU getopt expectations, iconv assumptions, small default thread stacks, and different dynamic-loader behavior. For simple glibc-runtime gaps, Alpine recommends gcompat; for harder runtime compatibility issues, Alpine explicitly points you to containers or chroots running a glibc distribution instead of pretending musl is a drop-in replacement. citeturn18view0turn18view1turn18view5turn18view7turn18view8
What the supplied tarballs imply
Direct inspection of the supplied ASK tarball shows all of the following.
The top-level Makefile includes build/toolchain.mk and build/sources.mk, sets HOST := aarch64-linux-gnu, defaults KDIR to $(HOME)/Mono/linux, and fetches upstream dependencies in the build itself. fmlib and fmc are cloned at tag lf-6.12.49-2.2.0; libnfnetlink-1.0.2 and libnetfilter_conntrack-1.1.0 are downloaded as tarballs and then patched. That is reproducibility-hostile because the build is not closed over the supplied source archive.
The provided ASK source already contains one musl-aware clue: cmm/src/cmm.c gates execinfo.h, backtrace(), and backtrace_symbols() behind __GLIBC__ checks. That means a pure musl build is plausible, but it does not prove the whole userspace is glibc-independent.
The ASK build is not just userspace. It also builds out-of-tree kernel modules (cdx, fci, auto_bridge), and those require a matching kernel source tree. So the build system needs a second tarball input for full module builds. If that kernel tarball is absent, the build should degrade cleanly to BUILD_TARGET=userspace.
There are no Dockerfiles in the supplied archive, so the Alpine conversion below is not a line-by-line rewrite of existing container files. It is a concrete replacement build design based on the actual source tree that was provided.
Assumptions where the upstream is unspecified
The concrete code below is tailored to the supplied ASK archive. Where the user’s request is broader than the archive, these are the assumptions:
- The upstream source enters the build as
packages/ASK.tar.gz. - Every third-party upstream source the build needs is also vendored into
packages/. - Kernel module builds require a matching kernel source tarball, exposed as
KERNEL_TAR. - If the project were not ASK but some other tarball-based upstream, the same pattern would still apply: replace in-build network fetches with tarball extraction; keep a closed build context; use a builder stage plus a minimal final stage.
- If the language/build system were unspecified, a reasonable default is:
- C/C++/make/cmake/autotools: Alpine builder with
build-baseand the needed-devpackages. - Go: Alpine builder with Go toolchain, then copy the compiled binary into a minimal runtime stage.
- Python: Alpine builder with Python and build dependencies, then wheel install into a slim runtime stage.
- Node: Alpine builder with Node toolchain, then copy built assets or production-only install into runtime.
- C/C++/make/cmake/autotools: Alpine builder with
- No CPU/arch was specified in the prompt, but the supplied ASK sources are arm64-oriented, so the examples default to
linux/arm64.
That base-image strategy follows Docker’s guidance to use trusted minimal bases and multi-stage builds; the specific image family for Go, Python, or Node is a practical recommendation layered on top of that principle. citeturn15view3turn9view10
Recommended design
The design should be blunt and boring.
Put ASK.tar.gz, the kernel tarball, and every dependency tarball under packages/. Add a root .dockerignore that excludes everything else. Replace the upstream Makefile’s git clone and wget logic with extraction from paths passed as build args. Verify SHA256SUMS in the builder stage. Remove .git after extraction or override the version-generation path so the build does not depend on VCS metadata. Set SOURCE_DATE_EPOCH, LC_ALL=C, and TZ=UTC so timestamps and locale-sensitive outputs stop drifting. Pin the base image by digest in CI, because Docker’s docs are explicit that image tags are mutable; if you still permit BuildKit-managed remote source resolution, Docker also documents EXPERIMENTAL_BUILDKIT_SOURCE_POLICY for reproducible builds with pinned dependencies. citeturn15view0turn15view3turn10view4turn14search1turn10view5
For ASK, the best Alpine port is native Alpine build per target platform using docker buildx build --platform=linux/arm64, not a Debian-style crossbuild-essential-arm64 clone. Debian has an official cross meta-package for that workflow; Alpine stable does not give you the same one-shot cross package story, so Buildx plus platform selection is the cleaner solution here. Docker’s buildx docs explicitly layer platform selection onto the whole Dockerfile, and Debian’s own package page makes clear what crossbuild-essential-arm64 actually is: a convenience list of cross-build essentials for Debian, not a universal pattern you must reproduce on Alpine. citeturn23view0turn23view1turn6view4
flowchart TD
A[packages/ASK.tar.gz] --> B[builder stage]
A2[packages/linux.tar.gz] --> B
A3[vendored dependency tarballs] --> B
B --> C[verify SHA256SUMS]
C --> D[extract ASK tarball]
D --> E[replace upstream fetch logic with tarball extraction]
E --> F[build patched third-party deps]
F --> G[build ASK userspace]
G --> H{kernel tarball present?}
H -->|yes| I[build kernel modules]
H -->|no| J[skip modules and build userspace only]
I --> K[stage dist artifacts]
J --> K
K --> L[scratch artifacts stage]
L --> M[buildx local exporter writes out/ask]
Concrete implementation
Root .dockerignore
**
!Makefile
!docker/**
!packages/**
!scripts/**
Host vendoring script
scripts/vendor-sources.sh
#!/usr/bin/env sh
set -eu
PACKAGES_DIR="${1:-packages}"
ASK_SRC="${ASK_SRC:-/absolute/path/to/ASK.tar.gz}"
KERNEL_SRC="${KERNEL_SRC:-/absolute/path/to/lf-6.12.49-2.2.0.tar.gz}"
NXP_TAG="lf-6.12.49-2.2.0"
mkdir -p "${PACKAGES_DIR}"
install -m 0644 "${ASK_SRC}" "${PACKAGES_DIR}/ASK.tar.gz"
install -m 0644 "${KERNEL_SRC}" "${PACKAGES_DIR}/linux.tar.gz"
curl -L --fail -o "${PACKAGES_DIR}/fmlib-${NXP_TAG}.tar.gz" \
"https://github.com/nxp-qoriq/fmlib/archive/refs/tags/${NXP_TAG}.tar.gz"
curl -L --fail -o "${PACKAGES_DIR}/fmc-${NXP_TAG}.tar.gz" \
"https://github.com/nxp-qoriq/fmc/archive/refs/tags/${NXP_TAG}.tar.gz"
curl -L --fail -o "${PACKAGES_DIR}/libnfnetlink-1.0.2.tar.bz2" \
"https://www.netfilter.org/projects/libnfnetlink/files/libnfnetlink-1.0.2.tar.bz2"
curl -L --fail -o "${PACKAGES_DIR}/libnetfilter_conntrack-1.1.0.tar.xz" \
"https://www.netfilter.org/projects/libnetfilter_conntrack/files/libnetfilter_conntrack-1.1.0.tar.xz"
curl -L --fail -o "${PACKAGES_DIR}/libcli-1.10.7.tar.gz" \
"https://github.com/dparrish/libcli/archive/refs/tags/V1.10.7.tar.gz"
(
cd "${PACKAGES_DIR}"
find . -maxdepth 1 -type f \
\( -name '*.tar.gz' -o -name '*.tar.xz' -o -name '*.tar.bz2' \) \
-print0 | sort -z | xargs -0 sha256sum > SHA256SUMS
)
Host-side extraction and normalization snippet
mkdir -p packages _inspect/ASK
cp /path/to/ASK.tar.gz packages/ASK.tar.gz
cp /path/to/lf-6.12.49-2.2.0.tar.gz packages/linux.tar.gz
tar -xf packages/ASK.tar.gz --strip-components=1 -C _inspect/ASK
export SOURCE_DATE_EPOCH=1704067200
tar --sort=name \
--mtime="@${SOURCE_DATE_EPOCH}" \
--owner=0 --group=0 --numeric-owner \
-czf packages/ASK.normalized.tar.gz \
-C _inspect ASK
Root Makefile
Makefile
.RECIPEPREFIX := >
DOCKER_PLATFORM ?= linux/arm64
ALPINE_VERSION ?= 3.22
BUILD_TARGET ?= dist
OUT_DIR ?= out/ask
IMAGE ?= ask-build:local
ASK_TAR ?= packages/ASK.tar.gz
KERNEL_TAR ?= packages/linux.tar.gz
FMLIB_TAR ?= packages/fmlib-lf-6.12.49-2.2.0.tar.gz
FMC_TAR ?= packages/fmc-lf-6.12.49-2.2.0.tar.gz
LIBNFNETLINK_TAR ?= packages/libnfnetlink-1.0.2.tar.bz2
LIBNFCT_TAR ?= packages/libnetfilter_conntrack-1.1.0.tar.xz
LIBCLI_TAR ?= packages/libcli-1.10.7.tar.gz
SOURCE_DATE_EPOCH ?= 1704067200
JOBS ?= 0
USERSPACE_CFLAGS ?=
USERSPACE_LDFLAGS ?=
.PHONY: ASK ASK_IMAGE
ASK:
> docker buildx build \
> --platform="$(DOCKER_PLATFORM)" \
> --file docker/ask.Dockerfile \
> --build-arg "ALPINE_VERSION=$(ALPINE_VERSION)" \
> --build-arg "ASK_TAR=$(ASK_TAR)" \
> --build-arg "KERNEL_TAR=$(KERNEL_TAR)" \
> --build-arg "FMLIB_TAR=$(FMLIB_TAR)" \
> --build-arg "FMC_TAR=$(FMC_TAR)" \
> --build-arg "LIBNFNETLINK_TAR=$(LIBNFNETLINK_TAR)" \
> --build-arg "LIBNFCT_TAR=$(LIBNFCT_TAR)" \
> --build-arg "LIBCLI_TAR=$(LIBCLI_TAR)" \
> --build-arg "BUILD_TARGET=$(BUILD_TARGET)" \
> --build-arg "SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH)" \
> --build-arg "JOBS=$(JOBS)" \
> --build-arg "USERSPACE_CFLAGS=$(USERSPACE_CFLAGS)" \
> --build-arg "USERSPACE_LDFLAGS=$(USERSPACE_LDFLAGS)" \
> --target artifacts \
> --output "type=local,dest=$(OUT_DIR)" \
> .
ASK_IMAGE:
> docker buildx build \
> --platform="$(DOCKER_PLATFORM)" \
> --file docker/ask.Dockerfile \
> --build-arg "ALPINE_VERSION=$(ALPINE_VERSION)" \
> --build-arg "ASK_TAR=$(ASK_TAR)" \
> --build-arg "KERNEL_TAR=$(KERNEL_TAR)" \
> --build-arg "FMLIB_TAR=$(FMLIB_TAR)" \
> --build-arg "FMC_TAR=$(FMC_TAR)" \
> --build-arg "LIBNFNETLINK_TAR=$(LIBNFNETLINK_TAR)" \
> --build-arg "LIBNFCT_TAR=$(LIBNFCT_TAR)" \
> --build-arg "LIBCLI_TAR=$(LIBCLI_TAR)" \
> --build-arg "BUILD_TARGET=$(BUILD_TARGET)" \
> --build-arg "SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH)" \
> --build-arg "JOBS=$(JOBS)" \
> --build-arg "USERSPACE_CFLAGS=$(USERSPACE_CFLAGS)" \
> --build-arg "USERSPACE_LDFLAGS=$(USERSPACE_LDFLAGS)" \
> --load \
> --tag "$(IMAGE)" \
> .
# Examples:
# make ASK
# make ASK BUILD_TARGET=userspace
# make ASK DOCKER_PLATFORM=linux/amd64 BUILD_TARGET=userspace
# make ASK USERSPACE_LDFLAGS="-static-libgcc -static-libstdc++"
# make ASK ASK_TAR=packages/ASK.tar.gz KERNEL_TAR=packages/linux.tar.gz
The prompt’s example command is syntactically incomplete. The correct buildx form needs a Dockerfile specified with --file, one --build-arg KEY=VALUE flag per argument, and a final positional build context such as .. For artifact builds, --output type=local,dest=... is the right default; --load is only for a single-platform image result. citeturn9view6turn23view0turn10view0turn10view1
Upstream override files
docker/overrides/toolchain.mk
CROSS_COMPILE ?=
ARCH ?= arm64
PLATFORM ?= LS1043A
CC ?= $(if $(CROSS_COMPILE),$(CROSS_COMPILE)gcc,gcc)
CXX ?= $(if $(CROSS_COMPILE),$(CROSS_COMPILE)g++,g++)
AR ?= $(if $(CROSS_COMPILE),$(CROSS_COMPILE)ar,ar)
STRIP ?= $(if $(CROSS_COMPILE),$(CROSS_COMPILE)strip,strip)
KDIR ?= /opt/kernel
docker/overrides/Makefile
.RECIPEPREFIX := >
include build/toolchain.mk
include build/sources.mk
DEFCONFIG := $(CURDIR)/config/kernel/defconfig
DIST := $(CURDIR)/dist
SRCDIR := $(CURDIR)/sources
PATCHES := $(CURDIR)/patches
HOST ?= $(shell $(CC) -dumpmachine 2>/dev/null || echo aarch64-alpine-linux-musl)
FMLIB_DIR := $(SRCDIR)/fmlib
FMC_DIR := $(SRCDIR)/fmc/source
LIBFCI_DIR := $(CURDIR)/fci/lib
SYSROOT := $(SRCDIR)/sysroot
ABM_DIR := $(CURDIR)/auto_bridge
KBUILD_ARGS := CROSS_COMPILE=$(CROSS_COMPILE) ARCH=$(ARCH)
CDX_ARGS := $(KBUILD_ARGS) KERNELDIR=$(KDIR) PLATFORM=$(PLATFORM)
FCI_ARGS := $(KBUILD_ARGS) KERNEL_SOURCE=$(KDIR) BOARD_ARCH=$(ARCH) \
KBUILD_EXTRA_SYMBOLS=$(CURDIR)/cdx/Module.symvers
ABM_ARGS := $(KBUILD_ARGS) KERNEL_SOURCE=$(KDIR) PLATFORM=$(PLATFORM)
ASK_TAR ?=
FMLIB_TAR ?= /vendor/packages/fmlib-$(NXP_TAG).tar.gz
FMC_TAR ?= /vendor/packages/fmc-$(NXP_TAG).tar.gz
LIBNFNETLINK_TAR ?= /vendor/packages/libnfnetlink-$(LIBNFNETLINK_VER).tar.bz2
LIBNFCT_TAR ?= /vendor/packages/libnetfilter_conntrack-$(LIBNFCT_VER).tar.xz
S := $(SRCDIR)/.stamps
$(shell mkdir -p $(S))
JOBS ?= 1
USERSPACE_CFLAGS ?=
USERSPACE_LDFLAGS ?=
.PHONY: all setup sources modules userspace kernel dist clean clean-all help \
cdx fci auto_bridge fmc cmm dpa_app
all: modules userspace
setup:
> @echo "Container build: setup target intentionally disabled."
sources: $(S)/fmlib $(S)/fmc $(S)/libfci $(S)/libnfnetlink $(S)/libnfct
$(S)/fmlib:
> @echo "==> fmlib: extract + patch + build"
> rm -rf $(FMLIB_DIR)
> mkdir -p $(FMLIB_DIR)
> tar -xf "$(FMLIB_TAR)" --strip-components=1 -C $(FMLIB_DIR)
> cd $(FMLIB_DIR) && patch -p1 -i "$(PATCHES)/fmlib/01-mono-ask-extensions.patch"
> $(MAKE) -C $(FMLIB_DIR) CROSS_COMPILE=$(CROSS_COMPILE) KERNEL_SRC=$(KDIR) libfm-arm.a
> ln -sf libfm-arm.a $(FMLIB_DIR)/libfm.a
> touch $@
$(S)/fmc: $(S)/fmlib
> @echo "==> fmc: extract + patch + build"
> rm -rf $(SRCDIR)/fmc
> mkdir -p $(SRCDIR)/fmc
> tar -xf "$(FMC_TAR)" --strip-components=1 -C $(SRCDIR)/fmc
> cd $(SRCDIR)/fmc && patch -p1 -i "$(PATCHES)/fmc/01-mono-ask-extensions.patch"
> $(MAKE) -C $(FMC_DIR) \
> CC=$(CC) CXX=$(CXX) AR=$(AR) \
> MACHINE=ls1046 \
> FMD_USPACE_HEADER_PATH=$(FMLIB_DIR)/include/fmd \
> FMD_USPACE_LIB_PATH=$(FMLIB_DIR) \
> LIBXML2_HEADER_PATH=/usr/include/libxml2 \
> TCLAP_HEADER_PATH=/usr/include
> touch $@
$(S)/libfci:
> @echo "==> libfci: build"
> $(MAKE) -C $(LIBFCI_DIR) CC=$(CC) AR=$(AR)
> touch $@
$(S)/libnfnetlink:
> @echo "==> libnfnetlink: extract + patch + build"
> rm -rf $(SRCDIR)/libnfnetlink-$(LIBNFNETLINK_VER)
> mkdir -p $(SRCDIR) $(SYSROOT)
> tar -xf "$(LIBNFNETLINK_TAR)" -C $(SRCDIR)
> cd $(SRCDIR)/libnfnetlink-$(LIBNFNETLINK_VER) && \
> patch -p1 -i "$(PATCHES)/libnfnetlink/01-nxp-ask-nonblocking-heap-buffer.patch" && \
> ./configure --host=$(HOST) --prefix=$(SYSROOT) --enable-static --disable-shared && \
> $(MAKE) -j$(JOBS) && \
> $(MAKE) install
> touch $@
$(S)/libnfct: $(S)/libnfnetlink
> @echo "==> libnetfilter_conntrack: extract + patch + build"
> rm -rf $(SRCDIR)/libnetfilter_conntrack-$(LIBNFCT_VER)
> mkdir -p $(SRCDIR) $(SYSROOT)
> tar -xf "$(LIBNFCT_TAR)" -C $(SRCDIR)
> cd $(SRCDIR)/libnetfilter_conntrack-$(LIBNFCT_VER) && \
> patch -p1 -i "$(PATCHES)/libnetfilter-conntrack/01-nxp-ask-comcerto-fp-extensions.patch" && \
> PKG_CONFIG_PATH=$(SYSROOT)/lib/pkgconfig \
> ./configure --host=$(HOST) --prefix=$(SYSROOT) --enable-static --disable-shared \
> CFLAGS="-I$(SYSROOT)/include" LDFLAGS="-L$(SYSROOT)/lib" && \
> $(MAKE) -j$(JOBS) && \
> $(MAKE) install
> touch $@
modules: cdx fci auto_bridge
cdx:
> $(MAKE) -C cdx $(CDX_ARGS) modules
fci: cdx
> $(MAKE) -C fci $(FCI_ARGS) modules
auto_bridge:
> $(MAKE) -C auto_bridge $(ABM_ARGS)
userspace: fmc cmm dpa_app
fmc: $(S)/fmc
> @true
cmm: $(S)/libfci $(S)/libnfct
> $(MAKE) -C cmm CC=$(CC) \
> LIBFCI_DIR=$(LIBFCI_DIR) \
> ABM_DIR=$(ABM_DIR) \
> SYSROOT=$(SYSROOT) \
> CFLAGS="$(USERSPACE_CFLAGS) -I/usr/local/include" \
> LDFLAGS="$(USERSPACE_LDFLAGS) -L/usr/local/lib"
dpa_app: $(S)/fmc
> $(MAKE) -C dpa_app CC=$(CC) \
> CFLAGS="-DDPAA_DEBUG_ENABLE -DNCSW_LINUX $(USERSPACE_CFLAGS) \
> -I/usr/local/include \
> -I$(FMC_DIR) -I$(CURDIR)/cdx \
> -I$(FMLIB_DIR)/include/fmd \
> -I$(FMLIB_DIR)/include/fmd/Peripherals \
> -I$(FMLIB_DIR)/include/fmd/integrations" \
> LDFLAGS="-L/usr/local/lib -lpthread -lcli \
> -L$(FMC_DIR) -lfmc \
> -L$(FMLIB_DIR) -lfm \
> -lstdc++ -lxml2 -lm $(USERSPACE_LDFLAGS)"
kernel:
> cp $(DEFCONFIG) $(KDIR)/.config
> $(MAKE) -C $(KDIR) $(KBUILD_ARGS) olddefconfig
> $(MAKE) -C $(KDIR) $(KBUILD_ARGS) -j$(JOBS) Image modules
dist: all
> mkdir -p $(DIST)
> cp cdx/cdx.ko $(DIST)/
> cp fci/fci.ko $(DIST)/
> cp auto_bridge/auto_bridge.ko $(DIST)/
> cp $(FMC_DIR)/fmc $(DIST)/
> cp cmm/src/cmm $(DIST)/
> cp dpa_app/dpa_app $(DIST)/
> @echo "Artifacts staged in $(DIST)/"
clean:
> -$(MAKE) -C cdx $(CDX_ARGS) clean
> -$(MAKE) -C fci $(FCI_ARGS) clean
> -$(MAKE) -C auto_bridge $(ABM_ARGS) clean
> -$(MAKE) -C $(LIBFCI_DIR) clean
> -$(MAKE) -C cmm clean
> -$(MAKE) -C dpa_app clean
> rm -f $(S)/*
> rm -rf $(DIST)
clean-all: clean
> rm -rf $(SRCDIR)
help:
> @echo "make - build everything from vendored tarballs"
> @echo "make userspace - build userspace only"
> @echo "make modules - build out-of-tree kernel modules"
> @echo "make kernel - build kernel Image + in-tree modules"
> @echo "make dist - stage artifacts into dist/"
> @echo "make clean - clean local build artifacts"
> @echo "make clean-all - clean everything including extracted sources"
Alpine multi-stage Dockerfile
docker/ask.Dockerfile
# syntax=docker/dockerfile:1.7
#
# In CI, prefer pinning the base image by digest, for example:
# FROM alpine:3.22@sha256:<digest> AS base-build
ARG ALPINE_VERSION=3.22
FROM alpine:${ALPINE_VERSION} AS base-build
ARG ALPINE_VERSION
WORKDIR /work
RUN set -eux; \
printf '%s\n' \
"https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION}/main" \
"https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION}/community" \
> /etc/apk/repositories; \
apk add --no-cache \
bash \
bc \
bison \
build-base \
bzip2 \
coreutils \
file \
findutils \
flex \
gawk \
libmnl-dev \
libpcap-dev \
libxml2-dev \
linux-headers \
openssl-dev \
patch \
perl \
pkgconf \
python3 \
tar \
tclap-dev \
xz \
zlib-dev
COPY packages/ /vendor/packages/
COPY docker/overrides/ /docker-overrides/
RUN set -eux; \
if [ -f /vendor/packages/SHA256SUMS ]; then \
cd /vendor/packages && sha256sum -c SHA256SUMS; \
fi
FROM base-build AS libcli-builder
ARG LIBCLI_TAR=packages/libcli-1.10.7.tar.gz
RUN set -eux; \
mkdir -p /tmp/libcli; \
tar -xf "/vendor/${LIBCLI_TAR}" --strip-components=1 -C /tmp/libcli; \
make -C /tmp/libcli; \
make -C /tmp/libcli install; \
rm -rf /tmp/libcli
FROM base-build AS builder
ARG ASK_TAR=packages/ASK.tar.gz
ARG KERNEL_TAR=packages/linux.tar.gz
ARG FMLIB_TAR=packages/fmlib-lf-6.12.49-2.2.0.tar.gz
ARG FMC_TAR=packages/fmc-lf-6.12.49-2.2.0.tar.gz
ARG LIBNFNETLINK_TAR=packages/libnfnetlink-1.0.2.tar.bz2
ARG LIBNFCT_TAR=packages/libnetfilter_conntrack-1.1.0.tar.xz
ARG BUILD_TARGET=dist
ARG SOURCE_DATE_EPOCH=1704067200
ARG JOBS=0
ARG USERSPACE_CFLAGS=
ARG USERSPACE_LDFLAGS=
COPY --from=libcli-builder /usr/local/ /usr/local/
ENV LC_ALL=C
ENV TZ=UTC
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
RUN set -eux; \
test -f "/vendor/${ASK_TAR}"; \
mkdir -p /work/src; \
tar -xf "/vendor/${ASK_TAR}" -C /work/src; \
test -d /work/src/ASK; \
rm -rf /work/src/ASK/.git; \
install -m 0644 /docker-overrides/Makefile /work/src/ASK/Makefile; \
install -m 0644 /docker-overrides/toolchain.mk /work/src/ASK/build/toolchain.mk; \
if [ ! -f /work/src/ASK/cmm/src/version.h ]; then \
printf '/* Auto-generated */\n#ifndef VERSION_H\n#define VERSION_H\n#define CMM_VERSION "%s"\n#endif\n' \
"tarball" > /work/src/ASK/cmm/src/version.h; \
fi
RUN set -eux; \
case "${BUILD_TARGET}" in \
all|modules|kernel|dist) \
test -f "/vendor/${KERNEL_TAR}" || { echo "KERNEL_TAR is required for BUILD_TARGET=${BUILD_TARGET}"; exit 2; }; \
mkdir -p /opt/kernel; \
tar -xf "/vendor/${KERNEL_TAR}" --strip-components=1 -C /opt/kernel; \
;; \
*) : ;; \
esac
RUN set -eux; \
if [ "${JOBS}" = "0" ]; then JOBS="$(getconf _NPROCESSORS_ONLN)"; fi; \
make -C /work/src/ASK \
FMLIB_TAR="/vendor/${FMLIB_TAR}" \
FMC_TAR="/vendor/${FMC_TAR}" \
LIBNFNETLINK_TAR="/vendor/${LIBNFNETLINK_TAR}" \
LIBNFCT_TAR="/vendor/${LIBNFCT_TAR}" \
KDIR=/opt/kernel \
JOBS="${JOBS}" \
USERSPACE_CFLAGS="${USERSPACE_CFLAGS}" \
USERSPACE_LDFLAGS="${USERSPACE_LDFLAGS}" \
"${BUILD_TARGET}"
RUN set -eux; \
mkdir -p /out; \
if [ -d /work/src/ASK/dist ]; then \
cp -a /work/src/ASK/dist/. /out/; \
else \
[ -f /work/src/ASK/cmm/src/cmm ] && cp /work/src/ASK/cmm/src/cmm /out/ || true; \
[ -f /work/src/ASK/dpa_app/dpa_app ] && cp /work/src/ASK/dpa_app/dpa_app /out/ || true; \
[ -f /work/src/ASK/sources/fmc/source/fmc ] && cp /work/src/ASK/sources/fmc/source/fmc /out/ || true; \
[ -f /work/src/ASK/cdx/cdx.ko ] && cp /work/src/ASK/cdx/cdx.ko /out/ || true; \
[ -f /work/src/ASK/fci/fci.ko ] && cp /work/src/ASK/fci/fci.ko /out/ || true; \
[ -f /work/src/ASK/auto_bridge/auto_bridge.ko ] && cp /work/src/ASK/auto_bridge/auto_bridge.ko /out/ || true; \
fi
FROM scratch AS artifacts
COPY --from=builder /out/ /
Example invocations
# Full ASK build, including modules, exporting files into out/ask/
make ASK
# Userspace-only build when the kernel tarball is unavailable
make ASK BUILD_TARGET=userspace
# Single-platform image load instead of local artifact export
make ASK_IMAGE BUILD_TARGET=userspace IMAGE=ask-build:dev
# Alpine userspace with selective static GCC/C++ runtime linkage
make ASK BUILD_TARGET=userspace \
USERSPACE_LDFLAGS="-static-libgcc -static-libstdc++"
Sources and notes for the implementation above: .dockerignore behavior, Dockerfile-specific ignore precedence, build-context minimization, multi-stage builds, buildx build arguments, --output type=local, --load, --platform, and ADD/COPY semantics all come from Docker’s official docs. The recommendation to use COPY for ordinary context files and reserve ADD for special cases is also straight from Docker’s best-practices page. The Alpine repository split between main, community, and testing is official Alpine guidance, which matters here because tclap-dev lives in community. The libcli helper stage follows the upstream libcli README, which documents make and make install into /usr/local/lib. citeturn15view0turn15view1turn9view10turn10view0turn10view1turn10view3turn23view0turn23view1turn24view2turn24view1turn18view11turn1search1turn21search0
Debian-to-Alpine conversion guide
The important conversion is not just apt to apk. It is glibc-centric Debian cross-build assumptions to musl-centric Alpine native-per-target builds.
On Debian Trixie, build-essential and crossbuild-essential-arm64 are official convenience packages. build-essential pulls in the default GCC, G++, libc development headers, and make. crossbuild-essential-arm64 is Debian’s official one-shot cross-build convenience package for arm64. Alpine’s nearest equivalent for native builds is build-base, which explicitly depends on gcc, g++, make, and libc-dev; but Alpine does not provide an equivalent stable one-package story matching Debian’s arm64 cross meta-package. That is why the report recommends docker buildx build --platform=... rather than rebuilding Debian’s cross-toolchain pattern inside Alpine. citeturn4view0turn6view4turn2view0turn23view1
Alpine repository selection matters. Alpine’s official repositories are main, community, and testing; stable branches normally use main and community, while testing is edge-only and unsupported as a stable dependency source. That is directly relevant here because tclap-dev is in Alpine community, while the visible libcli package in Alpine’s official package index is in edge/testing rather than a normal stable -dev package flow. For a reproducible stable build, vendoring libcli as a tarball is cleaner than dragging edge/testing into a stable builder image. citeturn18view11turn1search1turn3view3
Debian and Alpine package mapping table
| Capability | Debian Trixie install command | Alpine 3.22 install command | Practical note |
|---|---|---|---|
| meta build toolchain | apt-get update && apt-get install -y build-essential |
apk add --no-cache build-base |
Rough native-build equivalents |
| arm64 cross meta-package | apt-get install -y crossbuild-essential-arm64 |
no stable one-package equivalent | Prefer buildx --platform=linux/arm64 for Alpine |
| GCC C compiler | apt-get install -y gcc |
apk add --no-cache gcc |
Alpine build-base already brings it in |
| GCC C++ compiler | apt-get install -y g++ |
apk add --no-cache g++ |
Alpine build-base already brings it in |
| make | apt-get install -y make |
apk add --no-cache make |
Alpine build-base already brings it in |
| CMake | apt-get install -y cmake |
apk add --no-cache cmake |
Same user-facing package name |
| pkg-config tooling | apt-get install -y pkgconf |
apk add --no-cache pkgconf |
pkgconf is the practical package on both sides |
| OpenSSL headers/libs | apt-get install -y libssl-dev |
apk add --no-cache openssl-dev |
Direct development-package equivalents |
| zlib headers/libs | apt-get install -y zlib1g-dev |
apk add --no-cache zlib-dev |
Direct development-package equivalents |
| C++ runtime | apt-get install -y libstdc++6 |
apk add --no-cache libstdc++ |
Runtime package names differ |
| libc development headers | apt-get install -y libc6-dev |
apk add --no-cache musl-dev |
Alpine uses musl, not glibc |
| TCLAP headers | apt-get install -y libtclap-dev |
apk add --no-cache tclap-dev |
Alpine package is in community |
| libcli development files | apt-get install -y libcli-dev |
vendor the libcli tarball and build it | Alpine stable does not give a clean libcli-dev replacement path |
Sources and notes for the table: Debian package naming and dependency roles come from the official Debian package pages for build-essential, crossbuild-essential-arm64, pkgconf, libssl-dev, zlib1g-dev, libc6-dev, libcli-dev, g++, and the Trixie package index entry showing libtclap-dev. Alpine naming and repository placement come from the official Alpine package database pages for build-base, gcc, g++, make, cmake, pkgconf, openssl-dev, zlib-dev, libstdc++, musl-dev, tclap-dev, libpcap-dev, libmnl-dev, libxml2-dev, and the official libcli package page in edge/testing. citeturn4view0turn6view4turn4view1turn6view1turn6view2turn6view3turn6view0turn19view2turn5search0turn2view0turn1search17turn1search5turn2view1turn2view2turn2view3turn3view0turn3view1turn3view2turn1search13turn1search1turn6view6turn6view7turn6view8turn3view3
Passing tarballs safely and reproducibly
There are two different problems here, and they should not be confused.
If the tarball is not secret, the correct pattern is to pass a path selector as a build arg and copy the tarball into the build context. Docker’s docs explicitly show --build-arg as the right mechanism for build-time parameterization, and the Dockerfile reference explicitly documents that local tar archives added with ADD are decompressed and extracted automatically. For normal reproducible CI, the cleaner pattern is still COPY packages/ ... plus explicit tar -xf, because it gives you better validation and less magic. citeturn9view1turn9view6turn24view2turn9view4turn9view5
If the tarball is secret or confidential, do not rely on ARG. Docker’s docs warn that build args and environment variables are inappropriate for secrets and point you to secret mounts instead. That is the secure answer. citeturn9view0turn9view3turn16view1turn16view2
Non-secret build-arg pattern
ARG ASK_TAR=packages/ASK.tar.gz
COPY packages/ /vendor/packages/
RUN test -f "/vendor/${ASK_TAR}" && mkdir -p /src && tar -xf "/vendor/${ASK_TAR}" -C /src
ASK:
> docker buildx build \
> --file docker/ask.Dockerfile \
> --build-arg "ASK_TAR=$(ASK_TAR)" \
> .
Short ADD pattern for a local tar archive
ARG ASK_TAR=packages/ASK.tar.gz
ADD ${ASK_TAR} /src/
Secret-mount pattern for confidential tarballs
# syntax=docker/dockerfile:1.7
RUN --mount=type=secret,id=ask,target=/run/secrets/ASK.tar.gz \
mkdir -p /src && \
tar -xf /run/secrets/ASK.tar.gz -C /src
docker buildx build \
--secret id=ask,src=packages/ASK.tar.gz \
--file docker/ask.Dockerfile \
.
Reproducibility controls worth enabling
Treat these as the minimum serious set:
- Pin the base image by digest in CI.
- Verify
SHA256SUMSfor every vendored tarball. - Set
SOURCE_DATE_EPOCH. - Set
LC_ALL=CandTZ=UTC. - Remove
.gitafter extraction or replace VCS-derived version generation. - If any remote BuildKit source resolution remains, consider Docker’s documented source policy feature.
The reason is simple: Docker tags are mutable, and SOURCE_DATE_EPOCH was created precisely so build systems can share a stable timestamp rather than leaking wall-clock time into artifacts. citeturn10view4turn15view3turn14search0turn14search1turn10view5
Musl troubleshooting and decision rules
Most Alpine ports do not fail because of package names. They fail because the code assumes glibc semantics.
Alpine’s own musl page says Alpine uses musl as its C standard library and that musl does not implement most locale features that glibc implements. The musl FAQ then gets more specific: common breakage comes from glibc-specific assumptions or wrong #ifdefs, GNU getopt behavior, iconv BOM and UCS2 assumptions, off_t width assumptions, and expectations that pthread_create gives you big glibc-sized stacks by default. That is not theory. Those are the actual usual failure modes. citeturn18view0turn18view1turn18view5turn18view6
For this ASK source tree, one obvious example is already handled: cmm.c only includes execinfo.h and uses backtrace() on glibc. That is the right pattern. If other source files still assume glibc-only headers or symbols, patch them the same way: guard them with #if defined(__GLIBC__), provide a musl-safe fallback, or compile out the diagnostic-only path.
Threading and loader behavior are the next big traps. The musl docs say the default thread stack is much smaller than glibc’s and can be increased explicitly with pthread_attr_setstacksize, or, since newer musl, via -Wl,-z,stack-size=N. The musl functional-differences page also says dlclose() semantics differ materially: under musl, constructors run only once and destructors run on exit, not on each unload/reload cycle as many glibc-focused programs implicitly assume. If the upstream relies on library unloading or reinitialization as a runtime feature, that is not an Alpine packaging bug; it is a portability bug in the application. citeturn18view3turn18view2
Some glibc gaps are small enough for compatibility shims. Alpine’s official software-management page says that for simpler binaries you can install gcompat, which provides glibc-compatible APIs on musl systems. On aarch64, Alpine’s package contents show that gcompat provides the glibc-style loader name and a libc.so.6 compatibility path. That makes gcompat a legitimate option when the problem is a narrow runtime ABI expectation rather than deep glibc dependence. citeturn18view7turn18view8turn13search6
When gcompat is not enough, stop wasting time. Alpine’s own docs explicitly recommend containers or chroots for running glibc programs. If the upstream depends on glibc-only locale behavior, NSS/plugins, loader behavior, or opaque vendor binaries built for glibc, the pragmatic answer is a glibc builder or runtime stage for that component, even if the rest of your pipeline uses Alpine. citeturn18view8
Alpine-specific shell and utility differences also matter. Alpine is built around musl and BusyBox, and Alpine’s docs warn that BusyBox tools tend to implement only standard options and often lack GNU-specific extensions. If the upstream scripts assume GNU sed, GNU find, bash, or other non-POSIX behavior, install the needed packages explicitly or patch the scripts. The Dockerfile above does exactly that by installing bash, coreutils, findutils, gawk, and related tools. citeturn18view10
One more gotcha: musl does not implement utmp; Alpine’s docs say those functions are stubbed. If the upstream or its tests use wall, who, w, or similar libc-backed session accounting behavior, expect that to differ on Alpine. citeturn18view9
Practical fix list
Use these fixes in this order:
-
Wrong
#ifdefs or missing glibc headers
Patch to feature checks or__GLIBC__guards. ASK already does this forexecinfo.h/backtrace()incmm.c. -
Tiny musl thread stacks
Patch the app to callpthread_attr_setstacksize, or pass a linker stack-size hint with-Wl,-z,stack-size=Nwhen appropriate. citeturn18view3 -
Userspace portability while keeping dynamic linking
Prefer normal musl dynamic linking first. -
GCC runtime portability only
Try-static-libgcc -static-libstdc++first. This is often enough when the program is otherwise musl-clean but you want to reduce deployment friction around the GCC runtimes. -
Simple glibc ABI gaps at runtime
Tryapk add gcompat. citeturn18view7turn18view8turn13search6 -
Deep glibc dependencies
Use a glibc runtime image or Alpine-documented container/chroot strategy. citeturn18view8 -
Full static linking
Use only if every dependency is available static and you have checked licensing and target runtime needs. Do not default to-staticblindly.
flowchart TD
A[Need Alpine runtime?] -->|No| B[Use Alpine builder only and export artifacts]
A -->|Yes| C{Does the binary run correctly on musl?}
C -->|Yes| D[Use Alpine runtime stage]
C -->|Minor glibc ABI loader gap| E[Try gcompat]
C -->|glibc-specific behavior or binary blob| F[Use glibc runtime image or chroot/container]
Open questions and limitations
The supplied archive did not contain any original Dockerfiles, so the Alpine design above is a concrete replacement architecture, not a textual translation of preexisting container files.
The full module build depends on a matching kernel source tarball. The uploaded kernel archive makes that possible here, but in a generic tarball-only scenario you must state that dependency explicitly rather than silently assuming a host kernel tree exists.
The final deployment environment for ASK userspace binaries was not specified. That matters. If the target root filesystem is glibc-based and you want drop-in userspace binaries, musl-built artifacts may not be the right end state even if the build itself succeeds.
The implementation above is intentionally strict: no in-build git clone, no wget, no unpinned hidden source fetches, no dependence on host package managers inside the source tree, and no ambiguity about whether the tarball or the network is the source of truth. That is the reproducible answer.