32 KiB
Cross-Compiling ASK for Arm64 in Docker Without Full Emulation
Executive summary
The best default for ASK is not QEMU and not an Alpine-first builder. It is a native-host Docker build stage pinned to --platform=$BUILDPLATFORM, using a real arm64 GNU/Linux cross toolchain plus a matching target sysroot and kernel tree. Docker explicitly recommends cross-compilation with multi-stage builds as one of the three multi-platform strategies, and it explicitly warns that QEMU emulation is much slower for compilation-heavy workloads. The key pattern is: keep the build container native to the builder host, and let the compiler target arm64. citeturn10view4turn17search2
For a Linux arm64/glibc target, the cleanest path is a Debian-based builder with crossbuild-essential-arm64, gcc-aarch64-linux-gnu, g++-aarch64-linux-gnu, libc6-dev-arm64-cross, and linux-libc-dev-arm64-cross. Debian’s package metadata makes the intent explicit: crossbuild-essential-arm64 is the cross-build meta-package, gcc-aarch64-linux-gnu is the default arm64 GNU C cross-compiler, and libc6-dev-arm64-cross provides the arm64 glibc development headers and objects for cross-compiling. That stack is the shortest route to reproducible arm64 binaries without emulation. citeturn10view0turn10view1turn10view2
For kernel modules, a matching kernel source tree is not optional. The kernel docs are explicit: external modules need the kernel’s configuration and headers, modules_prepare exists for that purpose, and modules_prepare does not generate Module.symvers when CONFIG_MODVERSIONS is in play. If your module ABI depends on symbol versioning, you need a full kernel build of the matching tree/config to produce Module.symvers. citeturn10view5
For ASK specifically, the practical recommendation is:
- Use a Debian cross-toolchain builder for both userspace and module builds.
- Use a glibc target sysroot if the runtime target is Debian/Ubuntu or another glibc-based rootfs.
- Only use musl-cross or Alpine when the deployment target is actually musl-based.
- Use Clang/LLVM only if ASK and its module path are known to be Clang-clean.
- Treat crosstool-ng and custom toolchain builds as a last-mile optimization for pinned enterprise toolchains, not the day-one setup. citeturn10view8turn14view0turn10view10turn10view11turn10view12
Assumptions and the recommended path
Because some project details remain unspecified, I am making these assumptions instead of inventing facts:
- ASK is a C/C++ project with both userspace code and out-of-tree kernel modules.
- The arm64 target runtime is more likely glibc than musl, because the current build direction and requested Debian-cross path point that way.
- The source enters the build as tarballs, not Git checkouts.
- A matching kernel source tarball is available as
KERNEL_TARwhen module builds are required. - Third-party target libraries are either vendored as source tarballs or staged into a target sysroot before ASK is compiled.
- The build host may be amd64 or arm64, but the goal is to avoid full target emulation either way.
Under those assumptions, the recommended path is:
- Use
docker buildx build --platform=linux/arm64for target metadata and artifact export. - Pin every
FROMthat actually runs build commands to--platform=$BUILDPLATFORM, so those stages run natively on the builder and do not invoke emulation. - Install the Debian arm64 cross toolchain in the builder.
- Stage a target sysroot for arm64 glibc.
- Build vendored target libraries into that sysroot.
- Build ASK userspace with
CC/CXXpointing at the cross compiler and--sysrootor an equivalent staged sysroot layout. - Build ASK modules with
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KDIR=/opt/kernel. - Export artifacts with
--output type=local. citeturn17search1turn17search2turn11view0turn11view1
flowchart LR
A[ASK.tar.gz + dependency tarballs + optional KERNEL_TAR] --> B[buildx build request --platform=linux/arm64]
B --> C[build stages pinned to --platform=$BUILDPLATFORM]
C --> D[install Debian arm64 cross toolchain]
D --> E[stage arm64 glibc sysroot]
E --> F[build vendored target libs into sysroot]
F --> G[extract ASK tarball]
G --> H[build userspace with aarch64-linux-gnu-gcc/g++]
E --> I[extract matching kernel tree]
I --> J[merge config fragments and olddefconfig]
J --> K[modules_prepare or full kernel build]
K --> L[build external modules]
H --> M[collect artifacts]
L --> M
M --> N[scratch artifacts stage]
N --> O[--output type=local]
Toolchain choices without emulation
The main choices are not equal.
Debian cross packages
This is the best default for ASK if the target runtime is arm64 GNU/Linux with glibc. Debian already publishes a coherent arm64 GNU cross stack: crossbuild-essential-arm64, gcc-aarch64-linux-gnu, g++-aarch64-linux-gnu, libc6-dev-arm64-cross, linux-libc-dev-arm64-cross, and the matching arm64 libstdc++ development package. That gives you a native-host compilation environment that directly targets arm64 GNU/Linux without needing emulation or a self-built compiler. citeturn10view0turn10view1turn10view2turn10view3turn15view0turn15view1
Arm GNU Toolchain and Linaro-delivered releases
This is the right fallback when you need a prebuilt, distro-independent GNU cross compiler outside Debian’s cadence, or you want the exact Arm-distributed toolchain family. Linaro’s current downloads page explicitly points users to the Arm Developer site for the official prebuilt GNU cross-toolchain releases for AArch64 and A-profile Arm targets. That makes Arm GNU Toolchain a legitimate alternative to Debian packages, especially when you want one pinned tarball instead of distro package resolution. citeturn10view10turn5search3
Clang and LLVM cross-targeting
Clang is viable when you want one compiler binary that can emit code for many targets, and the official Clang cross-compilation docs explain the exact model: use -target and, where needed, --sysroot, plus explicit include/library paths. For the kernel specifically, the official kernel LLVM docs say arm64 is supported, make LLVM=1 ARCH=arm64 is the standard form, and if you use only LLVM tools then CROSS_COMPILE becomes unnecessary; if you mix GNU binutils with LLVM, you set those utilities explicitly. This is a strong option, but only if ASK’s userspace and module code are already known to behave under Clang. citeturn14view0turn14view1
crosstool-ng
crosstool-ng is a toolchain generator, not a fast path. Its official site describes it as a versatile cross-toolchain generator with a menuconfig-style interface. That is useful when you need a custom-pinned compiler, libc, binutils, and threading model combination, but it is slower to bootstrap and heavier to maintain than using Debian’s built packages. Use it when you need that control, not because you think you are being clever. citeturn10view8
musl-cross and Alpine-based targeting
Use this only when the target is musl-based. Alpine’s own docs say Alpine uses musl, and musl does not implement most glibc locale behavior. Alpine also documents gcompat as a compatibility layer for simpler glibc programs, not as a universal solution. musl’s own site links to musl-cross-make as the automated cross toolchain builder, and the musl-cross-make project describes itself as a simple, relocatable way to produce musl-targeting cross compilers. That is good for a musl target. It is the wrong default for a glibc target. citeturn10view11turn10view12turn11view8turn11view9
Decision table
| Option | Best when | Strengths | Weaknesses | Verdict for ASK |
|---|---|---|---|---|
| Debian cross packages | Target is arm64 GNU/Linux with glibc | Fast setup, distro-integrated sysroot, easiest CI | Tied to Debian package cadence | Best default |
| Arm GNU Toolchain | You want a pinned prebuilt toolchain tarball | Portable, explicit toolchain versioning | You still need a matching sysroot | Strong alternative |
| Clang/LLVM + GNU sysroot | Codebase is Clang-clean and you want one compiler binary | Good cross model, kernel arm64 support | Tool/library path tuning can be fussier | Good if already validated |
| crosstool-ng | You need a custom compiler/libc/binutils combo | Maximum control | Slow bootstrap, more maintenance | Use only if necessary |
| musl-cross / Alpine | Target runtime is musl | Small runtimes, relocatable toolchains possible | glibc mismatch risk | Use only for musl targets |
| QEMU emulation | You need runtime smoke tests or no cross path exists | Easy conceptually | Slow for compilation | Avoid for primary builds |
The table above is grounded in Docker’s documented multi-platform strategies, Debian’s cross-package metadata, Clang’s cross-compilation docs, the kernel’s LLVM docs, crosstool-ng’s own documentation, and Alpine/musl documentation. Docker explicitly states that QEMU is usually much slower for compilation-heavy workloads and recommends cross-compilation or native multi-node builders when possible. citeturn10view4turn10view0turn10view1turn14view0turn14view1turn10view8turn10view11turn10view12
Debian and Alpine comparison for arm64-targeted Linux builds
| Need | Debian / glibc path | Alpine / musl path | Bottom line |
|---|---|---|---|
| Native build meta-package | build-essential |
build-base |
Straight equivalents for native builds |
| Arm64 Linux cross meta-package | crossbuild-essential-arm64 |
no close stable equivalent | Debian is much better here |
| Arm64 Linux GNU C compiler | gcc-aarch64-linux-gnu |
no obvious stable aarch64-linux-gnu-gcc package surfaced |
Debian wins for glibc Linux targets |
| Arm64 Linux GNU C++ compiler | g++-aarch64-linux-gnu |
no obvious stable aarch64-linux-gnu-g++ package surfaced |
Debian wins |
| glibc target sysroot headers/libs | libc6-dev-arm64-cross, linux-libc-dev-arm64-cross, libstdc++-14-dev-arm64-cross |
Alpine is musl-first, not glibc-first | Use Debian for glibc sysroots |
| musl target compiler | extra work on Debian or musl-cross-make |
Alpine naturally targets musl | Alpine or musl-cross only if target is musl |
| glibc compatibility on musl | n/a | gcompat for simpler cases |
Useful only as a runtime compatibility hack |
This comparison comes directly from Debian package pages and Alpine’s own package/wiki materials. Alpine clearly documents build-base as the standard build meta-package and documents musl as the system libc. The Alpine package index examples that do surface clear cross packages in this space are *-none-elf embedded toolchains, which are not the same thing as a glibc/Linux arm64 cross stack. citeturn10view0turn10view1turn10view2turn15view0turn10view13turn11view6turn11view7turn19view0turn10view11
Concrete Docker implementation
The primary design below has two Dockerfiles.
docker/arm64-sysroot.Dockerfilebuilds and caches a reusable arm64 target sysroot.docker/ask.Dockerfileuses that sysroot, the Debian arm64 cross compiler, and an optional matching kernel tree to build ASK userspace and modules without emulation.
The key trick is that both Dockerfiles pin real build stages to FROM --platform=$BUILDPLATFORM .... That means the build steps run natively on the builder host even when the overall build request targets linux/arm64. Docker’s own docs explicitly describe this pattern and the automatic BUILDPLATFORM / TARGET* build args that make it work. citeturn17search1turn17search2turn10view4
Root Makefile
.RECIPEPREFIX := >
DOCKER_BUILDX ?= docker buildx build
TARGET_PLATFORM ?= linux/arm64
DEBIAN_SUITE ?= trixie
ASK_TAR ?= packages/ASK.tar.gz
KERNEL_TAR ?=
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
ASK_OUT ?= out/ask
ASK_IMAGE_NAME ?= local/ask:dev
SYSROOT_IMAGE ?= local/ask-arm64-sysroot:dev
TARGET_ARCH ?= arm64
TARGET_TRIPLE ?= aarch64-linux-gnu
BUILD_TARGET ?= dist
KERNEL_FULL_BUILD ?= 0
SOURCE_DATE_EPOCH ?= 1714521600
COMMON_ARGS = \
> --build-arg DEBIAN_SUITE=$(DEBIAN_SUITE) \
> --build-arg TARGET_TRIPLE=$(TARGET_TRIPLE) \
> --build-arg SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH)
.PHONY: ASK_SYSROOT ASK ASK_IMAGE
ASK_SYSROOT:
> $(DOCKER_BUILDX) \
> --platform $(TARGET_PLATFORM) \
> -f docker/arm64-sysroot.Dockerfile \
> $(COMMON_ARGS) \
> --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) \
> --load \
> -t $(SYSROOT_IMAGE) \
> .
ASK: ASK_SYSROOT
> mkdir -p $(ASK_OUT)
> $(DOCKER_BUILDX) \
> --platform $(TARGET_PLATFORM) \
> -f docker/ask.Dockerfile \
> $(COMMON_ARGS) \
> --build-arg SYSROOT_IMAGE=$(SYSROOT_IMAGE) \
> --build-arg ASK_TAR=$(ASK_TAR) \
> --build-arg KERNEL_TAR=$(KERNEL_TAR) \
> --build-arg BUILD_TARGET=$(BUILD_TARGET) \
> --build-arg KERNEL_FULL_BUILD=$(KERNEL_FULL_BUILD) \
> --target artifacts \
> --output type=local,dest=$(ASK_OUT) \
> .
ASK_IMAGE: ASK_SYSROOT
> $(DOCKER_BUILDX) \
> --platform $(TARGET_PLATFORM) \
> -f docker/ask.Dockerfile \
> $(COMMON_ARGS) \
> --build-arg SYSROOT_IMAGE=$(SYSROOT_IMAGE) \
> --build-arg ASK_TAR=$(ASK_TAR) \
> --build-arg KERNEL_TAR=$(KERNEL_TAR) \
> --build-arg BUILD_TARGET=$(BUILD_TARGET) \
> --build-arg KERNEL_FULL_BUILD=$(KERNEL_FULL_BUILD) \
> --target runtime \
> --load \
> -t $(ASK_IMAGE_NAME) \
> .
Example invocations:
# Userspace-only build
make ASK ASK_TAR=packages/ASK.tar.gz BUILD_TARGET=userspace
# Full build with matching kernel tree and full kernel build to get Module.symvers
make ASK \
ASK_TAR=packages/ASK.tar.gz \
KERNEL_TAR=packages/lf-6.12.49-2.2.0.tar.gz \
BUILD_TARGET=dist \
KERNEL_FULL_BUILD=1
# Minimal artifact image instead of local export
make ASK_IMAGE ASK_TAR=packages/ASK.tar.gz BUILD_TARGET=userspace
Helper sysroot Dockerfile
# syntax=docker/dockerfile:1.7
ARG DEBIAN_SUITE=trixie
FROM --platform=$BUILDPLATFORM debian:${DEBIAN_SUITE}-slim AS sysroot-build
ARG TARGET_TRIPLE=aarch64-linux-gnu
ARG SOURCE_DATE_EPOCH
ARG FMLIB_TAR=
ARG FMC_TAR=
ARG LIBNFNETLINK_TAR=
ARG LIBNFCT_TAR=
ARG LIBCLI_TAR=
ENV DEBIAN_FRONTEND=noninteractive
ENV LC_ALL=C.UTF-8
ENV TZ=UTC
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
build-essential \
crossbuild-essential-arm64 \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
binutils-aarch64-linux-gnu \
libc6-dev-arm64-cross \
linux-libc-dev-arm64-cross \
libstdc++-14-dev-arm64-cross \
autoconf automake libtool pkgconf make patch perl python3 rsync xz-utils bzip2 file \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /work
COPY packages/ /vendor/packages/
# Build a clean target sysroot rooted at /opt/sysroot.
RUN mkdir -p /opt/sysroot/usr && rsync -a /usr/aarch64-linux-gnu/ /opt/sysroot/usr/
# Optional: cross-build vendored target libraries into the sysroot.
RUN if [ -n "${LIBNFNETLINK_TAR}" ] && [ -f "/vendor/${LIBNFNETLINK_TAR}" ]; then \
mkdir -p /tmp/libnfnetlink && \
tar -xf "/vendor/${LIBNFNETLINK_TAR}" --strip-components=1 -C /tmp/libnfnetlink && \
cd /tmp/libnfnetlink && \
./configure --host=${TARGET_TRIPLE} --prefix=/usr --libdir=/usr/lib/aarch64-linux-gnu && \
make -j"$(nproc)" && \
make DESTDIR=/opt/sysroot install; \
fi
RUN if [ -n "${LIBNFCT_TAR}" ] && [ -f "/vendor/${LIBNFCT_TAR}" ]; then \
mkdir -p /tmp/libnfct && \
tar -xf "/vendor/${LIBNFCT_TAR}" --strip-components=1 -C /tmp/libnfct && \
cd /tmp/libnfct && \
PKG_CONFIG_SYSROOT_DIR=/opt/sysroot \
PKG_CONFIG_LIBDIR=/opt/sysroot/usr/lib/aarch64-linux-gnu/pkgconfig:/opt/sysroot/usr/share/pkgconfig \
./configure --host=${TARGET_TRIPLE} --prefix=/usr --libdir=/usr/lib/aarch64-linux-gnu && \
make -j"$(nproc)" && \
make DESTDIR=/opt/sysroot install; \
fi
RUN if [ -n "${LIBCLI_TAR}" ] && [ -f "/vendor/${LIBCLI_TAR}" ]; then \
mkdir -p /tmp/libcli && \
tar -xf "/vendor/${LIBCLI_TAR}" --strip-components=1 -C /tmp/libcli && \
make -C /tmp/libcli \
CC="${TARGET_TRIPLE}-gcc --sysroot=/opt/sysroot" \
AR="${TARGET_TRIPLE}-ar" && \
make -C /tmp/libcli PREFIX=/usr DESTDIR=/opt/sysroot install; \
fi
FROM scratch AS sysroot
COPY --from=sysroot-build /opt/sysroot/ /
Main ASK Dockerfile
# syntax=docker/dockerfile:1.7
ARG DEBIAN_SUITE=trixie
ARG SYSROOT_IMAGE=local/ask-arm64-sysroot:dev
FROM ${SYSROOT_IMAGE} AS sysroot
FROM --platform=$BUILDPLATFORM debian:${DEBIAN_SUITE}-slim AS build
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
ARG TARGET_TRIPLE=aarch64-linux-gnu
ARG ASK_TAR=packages/ASK.tar.gz
ARG KERNEL_TAR=
ARG BUILD_TARGET=dist
ARG KERNEL_FULL_BUILD=0
ARG SOURCE_DATE_EPOCH
ENV DEBIAN_FRONTEND=noninteractive
ENV LC_ALL=C.UTF-8
ENV TZ=UTC
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
ENV KBUILD_BUILD_TIMESTAMP=@${SOURCE_DATE_EPOCH}
ENV KBUILD_BUILD_USER=repro
ENV KBUILD_BUILD_HOST=repro-host
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
build-essential \
crossbuild-essential-arm64 \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
binutils-aarch64-linux-gnu \
libc6-dev-arm64-cross \
linux-libc-dev-arm64-cross \
libstdc++-14-dev-arm64-cross \
bc bison cpio file flex kmod libelf-dev make openssl patch perl pkgconf python3 rsync xz-utils \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /work
COPY --from=sysroot / /opt/sysroot/
COPY packages/ /vendor/packages/
COPY scripts/filter-kconfig-fragment.sh /usr/local/bin/filter-kconfig-fragment.sh
COPY docker/kernel-extra.config /tmp/kernel-extra.config
COPY docker/overrides/ /docker-overrides/
RUN chmod +x /usr/local/bin/filter-kconfig-fragment.sh
# Extract ASK tarball and inject cross-build overrides.
RUN mkdir -p /src/ASK && \
tar -xf "/vendor/${ASK_TAR}" --strip-components=1 -C /src/ASK && \
rm -rf /src/ASK/.git && \
install -m 0644 /docker-overrides/Makefile /src/ASK/Makefile && \
install -m 0644 /docker-overrides/toolchain.mk /src/ASK/build/toolchain.mk
# Optional matching kernel tree for out-of-tree module builds.
RUN if [ -n "${KERNEL_TAR}" ]; then \
mkdir -p /opt/kernel && \
tar -xf "/vendor/${KERNEL_TAR}" --strip-components=1 -C /opt/kernel && \
make -C /opt/kernel ARCH=arm64 CROSS_COMPILE=${TARGET_TRIPLE}- defconfig && \
/usr/local/bin/filter-kconfig-fragment.sh /opt/kernel /tmp/kernel-extra.config > /tmp/kernel-extra.effective.config && \
/opt/kernel/scripts/kconfig/merge_config.sh -m /opt/kernel/.config /tmp/kernel-extra.effective.config && \
KCONFIG_WARN_UNKNOWN_SYMBOLS=1 KCONFIG_WERROR=1 \
make -C /opt/kernel ARCH=arm64 CROSS_COMPILE=${TARGET_TRIPLE}- olddefconfig && \
if [ "${KERNEL_FULL_BUILD}" = "1" ]; then \
make -C /opt/kernel ARCH=arm64 CROSS_COMPILE=${TARGET_TRIPLE}- -j"$(nproc)" Image modules dtbs; \
else \
make -C /opt/kernel ARCH=arm64 CROSS_COMPILE=${TARGET_TRIPLE}- -j"$(nproc)" modules_prepare; \
fi; \
fi
WORKDIR /src/ASK
RUN export SYSROOT=/opt/sysroot; \
export CROSS_COMPILE=${TARGET_TRIPLE}-; \
export ARCH=arm64; \
export CC="${TARGET_TRIPLE}-gcc --sysroot=${SYSROOT}"; \
export CXX="${TARGET_TRIPLE}-g++ --sysroot=${SYSROOT}"; \
export AR="${TARGET_TRIPLE}-ar"; \
export STRIP="${TARGET_TRIPLE}-strip"; \
export PKG_CONFIG=pkg-config; \
export PKG_CONFIG_SYSROOT_DIR="${SYSROOT}"; \
export PKG_CONFIG_LIBDIR="${SYSROOT}/usr/lib/aarch64-linux-gnu/pkgconfig:${SYSROOT}/usr/share/pkgconfig"; \
case "${BUILD_TARGET}" in \
userspace) make userspace ;; \
modules) test -n "${KERNEL_TAR}" && make KDIR=/opt/kernel modules ;; \
dist) test -n "${KERNEL_TAR}" && make KDIR=/opt/kernel dist ;; \
*) echo "unsupported BUILD_TARGET=${BUILD_TARGET}" >&2; exit 2 ;; \
esac && \
mkdir -p /out && cp -a dist/. /out/
FROM scratch AS artifacts
COPY --from=build /out/ /
FROM scratch AS runtime
COPY --from=build /out/ /opt/ask/
Cross-aware Makefile override excerpt
If ASK’s upstream Makefile already has tarball-only source handling from your earlier reproducible-build work, the critical cross additions are these flags and environment variables:
ARCH ?= arm64
TARGET_TRIPLE ?= aarch64-linux-gnu
CROSS_COMPILE ?= $(TARGET_TRIPLE)-
SYSROOT ?= /opt/sysroot
CC ?= $(TARGET_TRIPLE)-gcc --sysroot=$(SYSROOT)
CXX ?= $(TARGET_TRIPLE)-g++ --sysroot=$(SYSROOT)
AR ?= $(TARGET_TRIPLE)-ar
STRIP ?= $(TARGET_TRIPLE)-strip
PKG_CONFIG ?= pkg-config
export PKG_CONFIG_SYSROOT_DIR := $(SYSROOT)
export PKG_CONFIG_LIBDIR := \
$(SYSROOT)/usr/lib/aarch64-linux-gnu/pkgconfig:$(SYSROOT)/usr/share/pkgconfig
KBUILD_ARGS := ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE)
userspace:
$(MAKE) -C cmm CC="$(CC)" CXX="$(CXX)"
$(MAKE) -C dpa_app CC="$(CC)" CXX="$(CXX)"
modules:
test -d "$(KDIR)"
$(MAKE) -C cdx KERNELDIR="$(KDIR)" $(KBUILD_ARGS)
$(MAKE) -C fci KERNEL_SOURCE="$(KDIR)" $(KBUILD_ARGS)
$(MAKE) -C auto_bridge KERNEL_SOURCE="$(KDIR)" $(KBUILD_ARGS)
Optional Arm GNU Toolchain stage
If you prefer a prebuilt Arm-distributed toolchain tarball over Debian packages, swap in a stage like this and keep the rest of the sysroot/kernel logic the same:
FROM --platform=$BUILDPLATFORM debian:trixie-slim AS armgnu
ARG ARM_GNU_TARBALL=packages/arm-gnu-toolchain-aarch64-linux-gnu.tar.xz
COPY packages/ /vendor/packages/
RUN mkdir -p /opt/toolchain && \
tar -xf "/vendor/${ARM_GNU_TARBALL}" --strip-components=1 -C /opt/toolchain
ENV PATH=/opt/toolchain/bin:${PATH}
The Docker pieces above rely on Docker’s documented multi-stage builds, automatic platform args, native-stage --platform=$BUILDPLATFORM pattern, local artifact exporter, and build-arg semantics. For private tarballs or credentials, Docker explicitly says to use secret mounts rather than ARG or ENV. citeturn11view1turn17search1turn17search2turn11view0turn11view2turn11view3
Kernel modules, sysroots, and config handling
The kernel side is where most “cross-compilation” guides turn to mush. The correct model is sharper than that.
If ASK builds only userspace, you need:
- a target compiler,
- a target libc/sysroot,
- target
.pcmetadata or explicit include/library paths.
If ASK builds external kernel modules, you additionally need:
- the exact target kernel source tree or a prepared build tree,
- the exact target
.configafter fragment merging, - generated headers under
include/generated, - and, when
CONFIG_MODVERSIONS=y, a matchingModule.symversfrom a full kernel build, not merelymodules_prepare. citeturn10view5turn11view5
That distinction matters because linux-libc-dev-arm64-cross is for userspace development headers. Debian’s own package metadata says those are Linux kernel headers for cross-compiling development, not a substitute for the actual configured kernel build tree you need for external modules. So: use Debian cross libc/sysroot packages for userspace, and use KERNEL_TAR for modules. citeturn10view2turn10view3
Minimal sysroot extraction patterns
If you build the sysroot from Debian cross packages:
mkdir -p /opt/sysroot/usr
rsync -a /usr/aarch64-linux-gnu/ /opt/sysroot/usr/
If you receive a prebuilt sysroot tarball instead:
mkdir -p /opt/sysroot
tar -xf packages/arm64-glibc-sysroot.tar.xz -C /opt/sysroot
If you receive a matching kernel tree tarball:
mkdir -p /opt/kernel
tar -xf packages/lf-6.12.49-2.2.0.tar.gz --strip-components=1 -C /opt/kernel
Kernel config merge and mismatch handling
Use this sequence every time:
make -C /opt/kernel ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
filter-kconfig-fragment.sh /opt/kernel docker/kernel-extra.config \
> /tmp/kernel-extra.effective.config
/opt/kernel/scripts/kconfig/merge_config.sh -m \
/opt/kernel/.config \
/tmp/kernel-extra.effective.config
KCONFIG_WARN_UNKNOWN_SYMBOLS=1 KCONFIG_WERROR=1 \
make -C /opt/kernel ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfig
Why the filter exists is simple: config fragments drift across kernel lines. merge_config.sh is the right merge tool, but it does not magically make a symbol exist in a tree that no longer defines it. Filtering before merge prevents stale fragment entries from poisoning the build.
The kernel docs explicitly document KCONFIG_WARN_UNKNOWN_SYMBOLS and KCONFIG_WERROR, and the kernel tree ships merge_config.sh explicitly for fragment merging. citeturn16search1turn11view5
Auto-gating missing symbols with scripts/config
For features that are optional or kernel-version-dependent, gate them before olddefconfig:
cd /opt/kernel
if grep -RqsE '^[[:space:]]*(menu)?config[[:space:]]+NETFILTER_XTABLES_LEGACY([[:space:]]|$)' .; then
scripts/config --file .config -e NETFILTER_XTABLES_LEGACY
fi
if grep -RqsE '^[[:space:]]*(menu)?config[[:space:]]+IP_NF_IPTABLES_LEGACY([[:space:]]|$)' .; then
scripts/config --file .config -e IP_NF_IPTABLES_LEGACY
fi
KCONFIG_WARN_UNKNOWN_SYMBOLS=1 KCONFIG_WERROR=1 \
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfig
This is the right cure for errors of the form “fragment expects CONFIG_X=y but the resolved config says <missing>”: first check whether the symbol exists in the target tree, then enable it only when it exists, then let Kconfig resolve dependencies. scripts/config is the official in-tree command-line .config manipulator. citeturn16search0turn16search1turn16search3
When to stop at modules_prepare and when to do a full kernel build
Use modules_prepare when:
- you only need generated headers and basic preparation,
- and
CONFIG_MODVERSIONSis not required for module ABI matching.
Do a full kernel build when:
CONFIG_MODVERSIONS=y,- you need
Module.symvers, - or you want the strongest ABI match signal against the actual shipping board kernel.
A practical decision rule:
If you need only compile-time headers: modules_prepare may be enough.
If you need symbol versioning correctness: build the kernel fully.
That rule is not opinion; it is straight from the kernel’s external-modules documentation. citeturn10view5
Verification, CI, and trade-offs
The verification story should be mechanical, not aspirational.
To verify arm64 userspace binaries:
file out/ask/cmm
readelf -h out/ask/cmm | grep 'Machine:'
readelf -l out/ask/cmm | grep 'Requesting program interpreter'
readelf -d out/ask/cmm | grep NEEDED
You want:
fileto report an AArch64 ELF,readelf -hto reportMachine: AArch64,- the ELF interpreter to match the target runtime’s loader,
- and
NEEDEDentries to resolve against the target sysroot or rootfs, not your host.
To verify kernel modules:
file out/ask/cdx.ko
readelf -h out/ask/cdx.ko | grep 'Machine:'
modinfo -F vermagic out/ask/cdx.ko
readelf -S out/ask/cdx.ko | grep __versions || true
You want:
Machine: AArch64,vermagicmatching the target kernel release/build flags,- and, when
CONFIG_MODVERSIONS=y, version sections consistent with the kernel build products.
For stricter ABI checks, compare the module against the exact Module.symvers and shipping kernel release, not a hand-wavy “same major version” guess.
A light smoke test under QEMU is acceptable after build if you want one, but it should be optional and narrow. The primary build should remain non-emulated. Docker’s own docs explicitly recommend cross-compilation or native multi-node builders over QEMU where possible because QEMU is slower for compute-heavy work. citeturn10view4
CI guidance that actually matters
Use these practices:
- Pin builder base images by digest.
- Keep toolchain and sysroot in separate reusable stages or images.
- Verify
SHA256SUMSfor every vendored tarball before extraction. - Set
SOURCE_DATE_EPOCH,KBUILD_BUILD_TIMESTAMP,KBUILD_BUILD_USER, andKBUILD_BUILD_HOST. - Remove
.gitfrom tarball-extracted sources unless VCS metadata is a deliberate build input. - Use BuildKit cache mounts for
aptand, if applicable, compiler caches. - Use
--output type=localfor artifacts rather than hiding everything inside an image layer.
Those recommendations are directly aligned with Docker’s best-practices guidance and the kernel’s reproducible-build guidance. The kernel docs are explicit that timestamps, user, and host leakage must be overridden for reproducible output, and Docker’s docs explicitly recommend multi-stage builds and local exporters for clean build outputs. citeturn11view1turn11view0turn11view4
Trade-offs
If you want the blunt version:
- Debian cross packages are the best speed-to-value option for ASK.
- Arm GNU Toolchain is best when you want a pinned vendor-distributed compiler tarball.
- Clang/LLVM is attractive if you already know the project and module path build cleanly with it.
- crosstool-ng is for teams that truly need custom toolchains and are willing to own them.
- musl-cross only makes sense when the target runtime is musl.
- QEMU is a fallback or a spot-check tool, not the backbone of a serious CI build.
flowchart TD
A[Need arm64 ASK artifacts in Docker] --> B{Target runtime glibc?}
B -- yes --> C{Need fastest reliable setup?}
C -- yes --> D[Debian cross packages + glibc sysroot]
C -- no --> E{Need custom pinned toolchain?}
E -- yes --> F[Arm GNU Toolchain or crosstool-ng]
E -- no --> D
B -- no --> G{Target runtime musl?}
G -- yes --> H[musl-cross or Alpine/musl sysroot]
G -- no --> I[Clarify runtime first]
D --> J{Kernel modules involved?}
F --> J
H --> J
J -- no --> K[userspace cross build only]
J -- yes --> L[provide KERNEL_TAR + config + headers]
L --> M{CONFIG_MODVERSIONS?}
M -- no --> N[modules_prepare may be enough]
M -- yes --> O[full kernel build to get Module.symvers]
K --> P[verify ELF headers]
N --> Q[verify vermagic and symbols]
O --> Q
Sources and notes
This report prioritizes official or project-authoritative sources. Docker guidance is from the official Docker documentation on multi-platform builds, BUILDPLATFORM/TARGET* build arguments, build secrets, local exporters, and Dockerfile best practices. Debian package metadata is from the official Debian package pages for crossbuild-essential-arm64, gcc-aarch64-linux-gnu, g++-aarch64-linux-gnu, libc6-dev-arm64-cross, linux-libc-dev-arm64-cross, and libstdc++-14-dev-arm64-cross. Kernel guidance is from the official Linux kernel docs on external modules, modules_prepare, Module.symvers, reproducible builds, Kconfig controls, Clang/LLVM kernel builds, and the in-tree merge_config.sh / scripts/config utilities. Alternative cross-toolchain options are grounded in the official crosstool-ng site, the musl site and musl-cross-make, and Linaro’s downloads page pointing to the Arm Developer site for official Arm GNU toolchain releases. Alpine references are from the Alpine wiki and package index for build-base, musl, and gcompat. citeturn10view4turn17search1turn17search2turn11view0turn11view1turn11view2turn11view3turn10view0turn10view1turn10view2turn10view3turn15view0turn15view1turn10view5turn11view4turn11view5turn16search0turn16search1turn14view1turn10view8turn10view10turn11view8turn11view9turn10view11turn10view12turn11view6turn11view7turn19view0turn19view1