Systems Programming · Cross Compilation
Configure Tests That Execute Cannot Survive a Cross Build
Run-time feature checks answer questions about whichever machine runs them, and caching makes the wrong answers outlive the build. Which tests can be converted, and which must be replaced by a declaration.
A configure script decides whether the C library returns the required length from a truncated snprintf by compiling a small program and running it. Under a native build the answer describes the machine the build runs on, which is the machine the program will run on, so the test is sound. Under a cross build the test is a question about the target asked of whatever executes the binary: the host, if the compiler produced a host binary by mistake; an emulator, if one is registered; or nothing, if the binary cannot run at all. The first two produce an answer, and the answer is cached.
Caching is what turns a wrong answer into a durable defect. Autoconf writes every result into config.cache or, by default, into the generated config.status, and a later build in the same tree reuses it without re-running the test. A tree configured once natively and then reconfigured for a cross build carries the host’s answers into the cross build, and nothing in the output distinguishes a value that was tested from a value that was inherited.
Three kinds of test, and which survive
Autoconf’s feature macros fall into three groups by what they need the compiler to produce.
Compile-only tests parse a translation unit and report whether it was accepted. AC_CHECK_DECLS, AC_CHECK_TYPES, and the _Static_assert form of AC_CHECK_SIZEOF belong here. They are answered by the target compiler’s own knowledge and are correct in a cross build as long as the compiler and its headers are the target’s.
Link-only tests build an executable but do not run it. AC_CHECK_FUNCS and AC_SEARCH_LIBS fall here: a function is present if a reference to it resolves against the target’s libraries. These are also correct in a cross build, and they fail informatively when the sysroot is missing a library.
Run tests compile, link, and execute. AC_RUN_IFELSE is the macro, and it takes an action if the program exits zero, an action if it exits non-zero, and an optional action for the cross-compiling case. When that fourth argument is absent and the compiler produces binaries that cannot run, configure stops with cannot run test program while cross compiling. That error is the good outcome: it is the build system telling the truth. The bad outcome is a test that runs and answers.
AC_CHECK_SIZEOF deserves separate mention because it looks like a run test and is not. It compiles a program in which the value is compared against a candidate constant, retries with other candidates until one is accepted, and records that one, so it produces the target’s answer without executing anything. Many size and layout questions can be answered this way, which is why the standard advice for a package with a failing run test is to look for a compile-time formulation first.
How the wrong answer gets in
Four routes produce a build whose configuration describes something other than the target.
The first is omitting --host. A configure script that has no reason to believe it is cross compiling runs every test binary, and if a native compiler was selected — because no prefixed driver was found and CC was left alone — the tests describe the workstation. This is the ordinary misconfiguration, and its symptom is a package that compiles and then misbehaves on the target in ways that correlate with the host’s libc version.
The second is registering an emulator and running the tests through it. Now the answers describe the emulator’s implementation. For questions about return values and structure layouts that is usually the right answer, because the emulator translates the syscall and reports what the host kernel returned. For questions about page size, CPU features, or the kernel version, it is the host’s answer wearing the target’s name. Emulation is a reasonable way to satisfy a build that insists on running tests, but it does not make the run test’s question valid — the boundary is described in user-mode emulation and binfmt_misc.
The third is a cache file reused across configurations. ./configure --cache-file=host.cache followed by ./configure --host=aarch64-linux-gnu --cache-file=host.cache reuses every ac_cv_* value the first run computed, including the ones that were run tests. Autoconf does record whether a result came from a cross-compiling run, but it does not discard values merely because the configuration changed. Any shared cache file has to be inspected rather than trusted.
The fourth is a package that hardcodes its own answer. A configure script that sets cross_compiling=no to make its tests run, or one that assumes a particular sizeof(long) because “every platform is LP64 now”, defeats the mechanism that would have caught the error and produces a build that fails only on the target it was built for.
Declaring answers instead of measuring them
The supported way to configure a cross build is to supply the answers that cannot be measured. Autoconf’s cache variables are a documented interface: setting ac_cv_sizeof_void_p=8, ac_cv_func_mmap_fixed_mapped=yes, or ac_cv_header_stdc=yes in the environment or in a cache file overrides the test, and the value is recorded as if tested.
Packages that care about cross builds publish the list of variables their configure scripts need. The pattern is familiar in the form of a cache file carried alongside a board support package, whose contents state the target’s word size, page size, and the presence of the handful of functions that a run test would otherwise have discovered. The values are only as good as the person who wrote them, which is why a mismatch between the declared value and the actual target appears as a run-time failure with no build-time trace.
The alternative is to change the test. Autoconf’s AC_RUN_IFELSE accepts a cross action that guesses from compile-time evidence: an #if defined(__LP64__) chain instead of a run, or a _Static_assert on a limit from the headers. This is a source change to the package and the only option that keeps the test meaningful, but it moves the guess from a cache file into the package where it can be reviewed.
The CMake equivalents and their defaults
CMake detects a cross build from CMAKE_SYSTEM_NAME being set to something other than the host’s, which is why a toolchain file sets it even when the target’s system name is the same as the host’s.
Its run tests are check_* macros built on try_run, and try_run fails in a cross build unless the result is supplied. Two mechanisms supply it. CMAKE_CROSSCOMPILING_EMULATOR names a program that can execute target binaries, and try_run invokes it when set; this is the direct expression of the emulator strategy, and it carries the same caveat about what the answers describe. Alternatively, the results can be pre-seeded in the cache, which is the declaration strategy.
CMake also offers a coarser tool: CMAKE_TRY_COMPILE_TARGET_TYPE set to STATIC_LIBRARY makes try_compile build a static library rather than an executable. Every check then stops at the compile step and no link is attempted. That is exactly right for a core with no C library available, as in a bare-metal build, and it silently converts link-only checks into compile-only ones, hiding a missing library.
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_SYSROOT /opt/roots/target)
set(CMAKE_FIND_ROOT_PATH /opt/roots/target)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # build tools stay on the host
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
The three FIND_ROOT_PATH_MODE settings are the CMake counterpart of the header and library search rules a sysroot imposes. PROGRAM must stay on the host because programs found by find_program are build-time tools; LIBRARY and INCLUDE must be rooted because everything else is target material.
pkg-config as a source of host paths
pkg-config returns compiler flags, and those flags are absolute paths unless it is told otherwise. Three environment variables change its behaviour, and they are not interchangeable.
PKG_CONFIG_PATH prepends directories to the search path while keeping the default directories. On a host with the library installed natively, the default directories contain a .pc file that describes the host build, and a build that sets only PKG_CONFIG_PATH to the sysroot’s directory can still resolve a dependency from the host if the sysroot’s copy is missing or named differently.
PKG_CONFIG_LIBDIR replaces the default search path entirely. It is the variable that makes a cross lookup hermetic, because it removes the host’s directories rather than adding to them.
PKG_CONFIG_SYSROOT_DIR prefixes the paths that come out. A .pc file containing -I/usr/include/foo and -L/usr/lib is rewritten to -I$SYSROOT/usr/include/foo and -L$SYSROOT/usr/lib, which is the only way those flags become usable in a cross link. Without it, the linker searches the host’s library directory, as described in what a sysroot actually restricts.
Questions that have no compile-time answer
Some properties are genuinely runtime facts and cannot be converted. Whether the kernel implements a particular ioctl, whether a device node exists, what the page size is on the kernel that will boot, and whether a service is reachable are all questions about the running system. A build system that probes them by execution is asking the wrong machine.
The workable treatments are to make the property a build option with a documented default, to read it from the target’s kernel configuration or from a description of the board, or to defer the check to run time in the program itself. What does not work is running the probe under an emulator and recording the host kernel’s answer as the target’s, because the resulting binary encodes an assumption that holds on the build machine and may hold nowhere else.
References
- Autoconf manual: Runtime — GNU Project
- Autoconf manual: Caching Results — GNU Project
- CMake manual: Cross Compiling — Kitware
- pkg-config(1) — manual page — man7.org