Skip to content

Cross-Compilation, CI, and Reproducible Builds

A cross compiler runs on one system and produces artifacts for another. Continuous integration turns the project's declared support matrix into repeated clean builds. Reproducibility controls enough inputs that those builds can be understood and, where required, recreated.

Separate build, host, and target

The build platform runs the build system. The host platform runs any tools produced for use during the build. The target runs the final artifact. In a native build they coincide; in cross builds they may not.

A usable cross toolchain needs:

  • compiler/code generator for the target;
  • assembler and linker compatible with target objects;
  • target headers, libraries, and startup objects;
  • a sysroot or SDK root;
  • target architecture/ABI options;
  • optionally an emulator or remote test runner.

Finding /usr/include from the build machine during a target build is usually a bug.

CMake toolchain files establish the target early

cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_SYSROOT /opt/sysroots/aarch64)

This simplified example belongs in a toolchain file selected on first configure. Real SDKs may require more root-path and linker settings. Do not put project feature options in the toolchain merely because both enter CMake; toolchain files describe the compilation environment.

Configuration checks that compile and execute a probe need special handling when target binaries cannot run. CMake can restrict try-compile behavior or use an emulator. Cached guesses without documentation produce fragile ports.

CI should reproduce local entry points

A useful matrix varies meaningful contracts:

text
Linux: GCC and Clang, C17/C++20, sanitizers
Windows: MSVC, x64, Debug/Release-relevant checks
macOS: Apple Clang, current deployment baseline
Packaging: install plus external consumer

Run configure, build, tests, analysis, and install through repository commands or presets. Avoid CI-only compiler flags developers cannot reproduce.

Caches accelerate but must not define correctness

Package binary caches, compiler caches such as ccache/sccache, and build-system caches reduce work. Keys should include compiler/toolchain identity, target, flags, dependency lock state, and relevant environment. A clean no-cache build should remain possible.

Do not cache an entire mutable build directory across incompatible configurations and then trust success. Generated state can hide missing prerequisites.

Reproducibility has levels

A repeatable dependency resolution is weaker than bit-for-bit identical binaries. Inputs include timestamps, absolute paths, archive member ordering, locale, random seeds, compiler version, SDK, environment, and build IDs.

Define the need:

  • developers can recreate a compatible build;
  • release inputs and toolchain are auditable;
  • package contents are deterministic;
  • independent parties can reproduce identical bits.

Each stronger level requires more controls. Record provenance and generate a software bill of materials where distribution or policy requires it.

Release artifacts need verification

Test the packaged archive/installer, not merely the build tree. Verify architecture, signatures where applicable, dynamic dependencies, licenses, symbols/debug archives, and a clean-machine smoke test. Cross-compilation succeeding does not prove the artifact starts on the target.

Optional prompts

Explain: Why must a CMake toolchain file be selected on initial configuration?

Answer: Compiler identity, platform tests, ABI information, and discovery paths are cached early. Retrofitting the target into a native build tree produces inconsistent state.

Explain: Why should CI occasionally build without caches?

Answer: Caches can hide incomplete dependency declarations or stale generated state. Clean builds verify that versioned inputs are sufficient.

Further reference