27 KiB
Reproducible Container Build System for ASK Tarball Sources
Executive summary
The supplied ASK archive is buildable in containers, but in its current form it is not hermetic or reproducible enough for serious CI use. Inspection of the archive shows a Debian-oriented arm64 build that still fetches sources during the build, assumes a GNU/glibc cross toolchain, and expects a separate kernel tree for module builds. That combination is fine for local hacking and bad for supply-chain control.
The clean fix is straightforward: close the build over a packages/ directory that contains every source archive the build needs, pass only archive paths as build arguments, verify checksums, replace every git clone and wget step with tarball extraction plus patch application, and use a multi-stage Dockerfile that exports only artifacts. Docker’s own docs explicitly support build arguments for parameterizing instructions, recommend multi-stage builds to keep final outputs small, and provide local exporters so you can pull files out of a build without shipping the entire toolchain image. citeturn9view0turn9view3turn9view5turn9view6turn23view0
The bad news is that a pure Alpine conversion is not free. Alpine uses musl, not glibc, and that changes ABI and runtime behavior. If the final ASK userspace binaries must run unchanged on a glibc-based target, Alpine-only is the wrong default unless you deliberately introduce a glibc sysroot, static-link what you can, or use compatibility mechanisms such as gcompat for simple cases. Alpine’s own docs and the musl docs both make that point indirectly but unmistakably. citeturn13view2turn13view3turn14view0turn13view1turn13view0
What the supplied ASK archive implies
Inspection of the supplied ASK tarball shows a C and kernel-module build for entity["company","NXP","semiconductor company"] Layerscape hardware. The root Makefile already pins fmlib and fmc to lf-6.12.49-2.2.0, downloads libnfnetlink-1.0.2 and libnetfilter_conntrack-1.1.0 during the build, and assumes a separate kernel source tree through KDIR. The included build/setup.sh is Debian host bootstrap logic; inside a container, that script should be retired, not executed.
For Alpine, the dependency picture is mixed but manageable. Official Alpine packages exist for libmnl-dev, libpcap-dev, libxml2-dev, and tclap-dev, with tclap-dev living in community. The nuisance dependency is libcli: Alpine’s package index only shows libcli in edge/testing, not as a stable v3.22 package, while upstream libcli’s own README documents a plain make && make install flow into /usr/local/lib. For this project, vendoring libcli as a tarball and building it in a helper stage is the sane choice; enabling edge/testing for one leaf dependency is not. citeturn4view0turn4view1turn14view2turn25search0turn19view0turn24view0turn26search10
There is one more important project-specific constraint: the archive does not include the kernel tree that ASK’s out-of-tree modules need. So a fully reproducible module build requires one more input archive, such as packages/linux.tar.xz, or a deliberate decision to build only userspace.
Recommended build architecture
The right shape is a closed build context. Put the ASK tarball, every required dependency tarball, and optionally the matching kernel source tarball in packages/. Keep the Docker context small with .dockerignore. Verify those archives with SHA256SUMS. Set SOURCE_DATE_EPOCH so timestamps stop drifting. In CI, pin the base image by digest instead of trusting a moving tag. Docker’s best-practices guide is blunt about why: tags move, digests do not, multi-stage builds slim outputs, and .dockerignore matters. The SOURCE_DATE_EPOCH spec exists precisely to keep timestamps from poisoning reproducibility. citeturn23view0turn22search2turn22search4turn22search1
For ASK specifically, the least painful Alpine route is to build natively for linux/arm64 with Buildx rather than trying to recreate Debian’s crossbuild-essential-arm64 model inside Alpine. Debian has a purpose-built GNU/glibc cross meta-package; Alpine’s stable equivalent is not that. Alpine’s native toolchain story is build-base on musl, and Docker’s multi-platform build support is exactly what makes --platform=linux/arm64 practical here. citeturn8view7turn13view4turn14view5turn9view4
flowchart TD
A[packages/*.tar.* in build context] --> B[buildx builder stage]
B --> C[extract ASK tarball]
B --> D[extract vendored dependency tarballs]
D --> E[patch and build fmlib, fmc, libnfnetlink, libnetfilter_conntrack, libcli]
C --> F[build ASK userspace and optionally kernel modules]
E --> F
F --> G[dist or artifact directory]
G --> H[scratch artifacts stage]
H --> I[buildx local exporter to out/ask]
Concrete implementation
The wrapper below fixes the syntax problem in the prompt. docker buildx build needs an explicit build context, repeated --build-arg KEY=VALUE flags, and a --file argument. The local exporter is the right default here because ASK’s real deliverable is a directory of build artifacts, not a fat runtime image. citeturn9view5turn9view6
Recommended .dockerignore
**
!Makefile
!docker/**
!packages/**
Docker recommends using .dockerignore to exclude irrelevant files from the build context, and that matters here because the whole point is to build from a tightly controlled set of source tarballs. citeturn23view0
Host-side vendoring script
#!/usr/bin/env sh
set -eu
PACKAGES_DIR="${1:-packages}"
ASK_SRC="${ASK_SRC:-/absolute/path/to/ASK.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"
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"
# Optional: add a matching kernel source archive for full module builds.
# install -m 0644 /path/to/linux.tar.xz "${PACKAGES_DIR}/linux.tar.xz"
(
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
)
If local repacking is part of your process, normalize ownership, ordering, and mtimes and set SOURCE_DATE_EPOCH; otherwise you are leaking host-local timestamps into your supposedly reproducible input archive. citeturn22search1turn22search4
Root wrapper Makefile
.RECIPEPREFIX := >
PLATFORM ?= linux/arm64
ALPINE_VERSION ?= 3.22
BUILD_TARGET ?= dist
OUT_DIR ?= out/ask
IMAGE ?= ask-build:local
ASK_TAR ?= packages/ASK.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
KERNEL_TAR ?= packages/linux.tar.xz
SOURCE_DATE_EPOCH ?= 1704067200
JOBS ?= 0
USERSPACE_CFLAGS ?=
USERSPACE_LDFLAGS ?=
.PHONY: ASK ASK_IMAGE
ASK:
> docker buildx build \
> --platform="$(PLATFORM)" \
> --file docker/ask.Dockerfile \
> --build-arg "ALPINE_VERSION=$(ALPINE_VERSION)" \
> --build-arg "ASK_TAR=$(ASK_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 "KERNEL_TAR=$(KERNEL_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="$(PLATFORM)" \
> --file docker/ask.Dockerfile \
> --build-arg "ALPINE_VERSION=$(ALPINE_VERSION)" \
> --build-arg "ASK_TAR=$(ASK_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 "KERNEL_TAR=$(KERNEL_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)" \
> .
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)
HOST ?= $(shell $(CC) -dumpmachine 2>/dev/null || echo aarch64-alpine-linux-musl)
KDIR ?= /opt/kernel
docker/overrides/Makefile
This override intentionally removes the upstream host-setup/network-fetch behavior and keeps only the build targets that matter for containerized CI.
.RECIPEPREFIX := >
include build/toolchain.mk
include build/sources.mk
DIST := $(CURDIR)/dist
SRCDIR := $(CURDIR)/sources
PATCHES := $(CURDIR)/patches
VENDOR_DIR ?= /vendor/packages
HOST ?= $(shell $(CC) -dumpmachine 2>/dev/null || echo aarch64-alpine-linux-musl)
JOBS ?= $(shell getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)
USERSPACE_CFLAGS ?=
USERSPACE_LDFLAGS ?=
FMLIB_DIR := $(SRCDIR)/fmlib
FMC_DIR := $(SRCDIR)/fmc/source
LIBFCI_DIR := $(CURDIR)/fci/lib
SYSROOT := $(SRCDIR)/sysroot
ABM_DIR := $(CURDIR)/auto_bridge
FMLIB_TAR ?= $(VENDOR_DIR)/fmlib-$(NXP_TAG).tar.gz
FMC_TAR ?= $(VENDOR_DIR)/fmc-$(NXP_TAG).tar.gz
LIBNFNETLINK_TAR ?= $(VENDOR_DIR)/libnfnetlink-$(LIBNFNETLINK_VER).tar.bz2
LIBNFCT_TAR ?= $(VENDOR_DIR)/libnetfilter_conntrack-$(LIBNFCT_VER).tar.xz
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)
S := $(SRCDIR)/.stamps
$(shell mkdir -p $(S))
define extract_strip1
rm -rf $(2) && mkdir -p $(2) && tar -xf $(1) --strip-components=1 -C $(2)
endef
.PHONY: all sources modules userspace cdx fci auto_bridge fmc cmm dpa_app dist clean clean-all
all: modules userspace
sources: $(S)/fmlib $(S)/fmc $(S)/libfci $(S)/libnfnetlink $(S)/libnfct
$(S)/fmlib:
> @echo "==> fmlib: extract, patch, build"
> test -f $(FMLIB_TAR)
> $(call extract_strip1,$(FMLIB_TAR),$(FMLIB_DIR))
> cd $(FMLIB_DIR) && patch -p1 -i $(PATCHES)/fmlib/01-mono-ask-extensions.patch
> $(MAKE) -j$(JOBS) -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:
> @echo "==> fmc: extract, patch, build"
> test -f $(FMC_TAR)
> rm -rf $(SRCDIR)/fmc
> mkdir -p $(SRCDIR)/fmc
> tar -xf $(FMC_TAR) -C $(SRCDIR)/fmc --strip-components=1
> cd $(FMC_DIR) && patch -p1 -i $(PATCHES)/fmc/01-mono-ask-extensions.patch
> $(MAKE) -j$(JOBS) -C $(FMC_DIR) \
> CC=$(CC) \
> CFLAGS="$(USERSPACE_CFLAGS)" \
> LDFLAGS="$(USERSPACE_LDFLAGS)" \
> TCLAP_HEADER_PATH=/usr/include \
> LIBXML2_HEADER_PATH=/usr/include/libxml2 \
> SYSROOT=$(SYSROOT)
> touch $@
$(S)/libnfnetlink:
> @echo "==> libnfnetlink: extract, patch, install into sysroot"
> test -f $(LIBNFNETLINK_TAR)
> 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/0001-libnfnetlink-fix-for-ARM64-and-clang.patch && \
> ./configure --host=$(HOST) --prefix=$(SYSROOT) --disable-shared --enable-static && \
> $(MAKE) -j$(JOBS) && $(MAKE) install
> touch $@
$(S)/libnfct: $(S)/libnfnetlink
> @echo "==> libnetfilter_conntrack: extract, patch, install into sysroot"
> test -f $(LIBNFCT_TAR)
> 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/0001-libnetfilter_conntrack-fix-for-ARM64-and-clang.patch && \
> ./configure --host=$(HOST) --prefix=$(SYSROOT) --with-libnfnetlink=$(SYSROOT) --disable-shared --enable-static && \
> $(MAKE) -j$(JOBS) && $(MAKE) install
> touch $@
$(S)/libfci: $(S)/fmlib $(S)/libnfnetlink $(S)/libnfct
> @echo "==> libfci"
> $(MAKE) -C fci/lib CC=$(CC) AR=$(AR)
> touch $@
modules: cdx fci auto_bridge
userspace: fmc cmm dpa_app
cdx:
> $(MAKE) -C cdx $(CDX_ARGS)
fci: $(S)/libfci
> $(MAKE) -C fci $(FCI_ARGS)
auto_bridge:
> $(MAKE) -C auto_bridge $(ABM_ARGS)
fmc: $(S)/fmc
cmm: $(S)/libfci $(S)/libnfnetlink $(S)/libnfct
> $(MAKE) -C cmm \
> CC=$(CC) \
> LIBFCI_DIR=$(LIBFCI_DIR) \
> ABM_DIR=$(ABM_DIR) \
> SYSROOT=$(SYSROOT) \
> CFLAGS="$(USERSPACE_CFLAGS)" \
> LDFLAGS="$(USERSPACE_LDFLAGS)"
dpa_app: $(S)/fmc
> $(MAKE) -C dpa_app \
> CC=$(CC) \
> CFLAGS="-DDPAA_DEBUG_ENABLE -DNCSW_LINUX $(USERSPACE_CFLAGS) -I$(FMC_DIR) -I$(FMC_DIR)/inc/integrations/drivers/netcfg" \
> LDFLAGS="-lpthread -lcli -lxml2 -lstdc++ $(USERSPACE_LDFLAGS)"
dist: all
> rm -rf $(DIST)
> mkdir -p $(DIST)
> cp -a $(CURDIR)/cmm/src/cmm $(DIST)/
> cp -a $(CURDIR)/dpa_app/dpa_app $(DIST)/
> cp -a $(CURDIR)/cdx/cdx.ko $(DIST)/
> cp -a $(CURDIR)/fci/fci.ko $(DIST)/
> cp -a $(CURDIR)/auto_bridge/auto_bridge.ko $(DIST)/
> cp -a $(SRCDIR)/fmc/source/fmc $(DIST)/
> cp -a scripts/init/* $(DIST)/
clean:
> $(MAKE) -C auto_bridge clean || true
> $(MAKE) -C cdx clean || true
> $(MAKE) -C cmm clean || true
> $(MAKE) -C dpa_app clean || true
> $(MAKE) -C fci clean || true
> $(MAKE) -C fci/lib clean || true
> rm -rf $(DIST)
clean-all: clean
> rm -rf $(SRCDIR)
docker/ask.Dockerfile
This Dockerfile deliberately enables Alpine community, because Alpine documents that community is not enabled by default in many configurations, and ASK needs tclap-dev, which is in that repository. citeturn26search10turn25search0
# syntax=docker/dockerfile:1.7
ARG ALPINE_VERSION=3.22
FROM alpine:${ALPINE_VERSION} AS builder
ARG ALPINE_VERSION
ARG ASK_TAR=packages/ASK.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 LIBCLI_TAR=packages/libcli-1.10.7.tar.gz
ARG KERNEL_TAR=packages/linux.tar.xz
ARG BUILD_TARGET=dist
ARG SOURCE_DATE_EPOCH=1704067200
ARG JOBS=0
ARG USERSPACE_CFLAGS=
ARG USERSPACE_LDFLAGS=
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 \
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
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; \
test -f "/vendor/${LIBCLI_TAR}"; \
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
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
ENV LC_ALL=C
ENV TZ=UTC
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
RUN set -eux; \
if [ "${JOBS}" = "0" ]; then JOBS="$(getconf _NPROCESSORS_ONLN)"; fi; \
make -C /work/src/ASK \
VENDOR_DIR=/vendor/packages \
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/ /
The Dockerfile above uses COPY packages/ plus explicit tar -xf because that is the less magical pattern once you have multiple optional archives such as KERNEL_TAR, checksum verification, and tarballs whose extracted top-level directory names you do not want to trust blindly. Docker’s docs are clear that ADD can auto-unpack local tar archives, but they are equally clear that COPY is the basic, explicit copy primitive. In practice, explicit wins here. citeturn23view0turn10view0turn10view1turn10view4
The normal invocation is make ASK BUILD_TARGET=dist KERNEL_TAR=packages/linux.tar.xz for a full module build, or make ASK BUILD_TARGET=userspace when only the source/dependency tarballs are available and the kernel tree is not. The artifact-export path is the better default; the image-loading target is only useful if you actually want a local OCI image as an intermediate. citeturn9view5turn9view6
Short ARG + ADD pattern and a secret-safe variant
If all you need is the shortest path-based pattern, Docker supports it:
ARG ASK_TAR=packages/ASK.tar.gz
ADD ${ASK_TAR} /work/src/
That works because local ADD sources are relative to the build context and local tar archives are unpacked automatically. But Docker also warns that build args are not secret channels. So if the archive is confidential, use a BuildKit secret mount instead:
# build command:
# docker buildx build --secret id=ask,src=packages/ASK.tar.gz -f docker/ask.Dockerfile .
RUN --mount=type=secret,id=ask,target=/tmp/ASK.tar.gz \
mkdir -p /work/src && tar -xf /tmp/ASK.tar.gz -C /work/src
That is the secure answer. Build args should carry a path or selector, not secret bytes. citeturn10view0turn10view2turn9view0turn9view1
Debian and Alpine package mapping
The table below is the practical package/command map for the common build tools requested, plus the one ASK-specific exception that matters in real life.
| Need | Debian Trixie | Alpine 3.22 | Practical note |
|---|---|---|---|
| Meta toolchain bundle | apt-get install -y build-essential |
apk add --no-cache build-base |
Rough equivalents for native builds |
| arm64 cross bundle | apt-get install -y crossbuild-essential-arm64 |
no single stable equivalent | On Alpine, use buildx --platform=linux/arm64 or bring your own GNU sysroot |
| C compiler | apt-get install -y gcc |
apk add --no-cache gcc |
build-base already includes it |
| C++ compiler | apt-get install -y g++ |
apk add --no-cache g++ |
build-base already includes it |
make |
apt-get install -y make |
apk add --no-cache make |
build-base already includes it |
| CMake | apt-get install -y cmake |
apk add --no-cache cmake |
Direct name match |
| pkg-config tooling | apt-get install -y pkgconf |
apk add --no-cache pkgconf |
On Debian, pkg-config is now effectively the pkgconf world too |
| 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 | runtime usually via libstdc++6; headers via g++/libstdc++-14-dev |
apk add --no-cache libstdc++ |
Alpine splits runtime from the compiler meta-package |
| libc headers | apt-get install -y libc6-dev |
apk add --no-cache musl-dev |
Alpine exposes libc-dev through musl |
| ASK-specific TCLAP | apt-get install -y libtclap-dev |
apk add --no-cache tclap-dev |
Needs Alpine community |
| ASK-specific libcli | apt-get install -y libcli-dev |
vendor tarball | Stable Alpine v3.22 does not have a normal libcli-dev package |
These mappings are grounded in Debian’s package pages and Alpine’s official package index. The important details are: build-base bundles gcc, g++, make, patch, and libc-dev; musl-dev provides the libc-dev role on Alpine; pkgconf is the package you actually install for pkg-config tooling; and Debian’s crossbuild-essential-arm64 does not have a one-package Alpine twin. citeturn5search0turn8view7turn7view0turn8view0turn8view2turn5search1turn5search9turn8view3turn8view4turn8view5turn8view6turn14view5turn13view4turn4view2turn4view3turn14view1turn14view3turn14view4
For ASK specifically, the two Alpine deltas worth remembering are tclap-dev in community and the absence of stable libcli, which is why the helper-stage libcli build is worth doing. citeturn25search0turn19view0turn24view0turn26search10
Musl compatibility, troubleshooting, and assumptions
This is where most Alpine ports actually fail.
-
Do not trust glibc-specific
#ifdeflogic. The musl FAQ explicitly calls out hardcoded glibc assumptions and wrong__GLIBC__checks as common failure causes. Fix the preprocessor logic first, not last. The same FAQ also calls out GNUgetoptexpectations, iconv/UCS2 assumptions,off_twidth assumptions, and too-small default thread stacks as recurring causes of runtime failures. citeturn13view1 -
Musl locale behavior is not glibc locale behavior. Alpine’s own musl page says musl does not implement most of the locale features that glibc implements. If your code relies on glibc locale internals, stop pretending Alpine is a drop-in replacement. citeturn13view2
-
Plugin unload/reload semantics differ. musl’s dynamic loader keeps libraries loaded for the life of the process and treats
dlcloseas a no-op. If your software depends on glibc-style unload/reload behavior, that is a real portability bug, not a packaging bug. citeturn13view0 -
Static versus dynamic needs a hard-headed choice.
-static-libgccand-static-libstdc++are good pragmatic flags when the problem is just the GCC/C++ runtime. They are not magic. Full static linking only works cleanly when every dependency is available as a static archive and your deployment model actually benefits from it. -
gcompatis for simple runtime cases, not for wishful thinking. Alpine documentsgcompatas a glibc compatibility layer for musl systems, and the package provides the relevantlibc6-compat/loader compatibility pieces. For more complex glibc applications, Alpine’s own docs point you toward a glibc chroot/container instead. citeturn13view3turn14view0 -
ASK-specific practical fix: because ASK’s source already contains at least one musl-friendly guard around
execinfo/backtrace, a native Alpine build is plausible for the build itself. The unresolved question is not “can it compile?” It is “what libc must the resulting userspace binaries target on the final device?”
Where the upstream project is genuinely unspecified, the pattern stays the same and only the builder image family changes: C/C++/Make/CMake/autotools usually start from alpine, Go from golang:*-alpine, Python from python:*-alpine, and Node from node:*-alpine, all with the same multi-stage split. The exception rule is blunt: the moment the project depends on glibc-only binaries, ABI-sensitive vendor libraries, or packaging ecosystems that assume glibc, switch back to a glibc builder instead of burning time fighting musl. Docker’s own guidance is to use trusted minimal images and multi-stage builds; Alpine’s own docs make clear that musl/glibc compatibility is extra engineering work, not something the base image solves for you. citeturn23view0turn9view3turn13view2turn13view3
Open questions / limitations
The supplied ASK tarball did not include the matching kernel source tree, so a fully reproducible module build still needs one more input archive. The deploy target’s libc requirement was also unspecified; that matters a lot, because a musl-linked dpa_app or cmm is not automatically a drop-in replacement for a glibc target userland. Finally, no existing Debian Dockerfiles were present in the supplied archive, so the Alpine conversion above is a concrete replacement design derived from the archive’s real build logic, not a literal line-by-line rewrite of preexisting Dockerfiles.