Cross-compiling

felspar-test runs each test as a post-build step, and also registers it with CTest, so that a successful build means the tests passed. When you cross-compile, the test executables are built for the target platform and can't be run directly on the build host. You have two options: run them on the host through an emulator, or leave the test targets out of the cross build and run them from a native build instead (see Building without the tests).

To run them on the host, the test runner invokes each test through CMake's standard CMAKE_CROSSCOMPILING_EMULATOR:

add_custom_command(TARGET ${name} POST_BUILD
    COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $<TARGET_FILE:${name}>)

CMAKE_CROSSCOMPILING_EMULATOR is empty for a native build, so the test runs directly. For a cross build you set it — usually in your toolchain file — to a command that can run a target binary on the host. CMake applies the same variable to the add_test() registration automatically, so ctest behaves the same way.

Providing an emulator

The emulator is a property of your cross build, not of felspar-test. Set it in the toolchain file you pass via -DCMAKE_TOOLCHAIN_FILE=....

For a foreign-architecture Linux target a user-mode emulator is often enough on its own:

set(CMAKE_CROSSCOMPILING_EMULATOR qemu-aarch64)

When running a target binary needs more than a single command — shared libraries on a search path, environment variables, argument translation — point it at a small wrapper script instead:

set(CMAKE_CROSSCOMPILING_EMULATOR "${CMAKE_CURRENT_LIST_DIR}/run-target")

The wrapper receives the target executable followed by its arguments, sets up whatever the environment needs and execs the real emulator. For example, running mingw-w64 Windows executables under wine from a Linux host:

#!/usr/bin/env bash
set -euo pipefail

# Let wine find the mingw runtime DLLs, as Windows-style ';'-separated Z: paths
# (resolved from the compiler so the threading variant matches what was linked).
target=x86_64-w64-mingw32
stdcxx_dir=$(dirname "$(clang --target=$target -print-file-name=libstdc++-6.dll)")
winpthread_dir=$(dirname "$(clang --target=$target -print-file-name=libwinpthread-1.dll)")
export WINEPATH="Z:${stdcxx_dir};Z:${winpthread_dir}"
export WINEDEBUG=-all

exe=$1
shift
# Rewrite absolute Unix path arguments to Z: paths (wine maps the root to Z:).
args=()
for a in "$@"; do
    if [[ $a == /* && -e $a ]]; then args+=("Z:$a"); else args+=("$a"); fi
done
exec wine "$exe" "${args[@]}"

Building without the tests

Running the tests on the host is optional. The test executables are created EXCLUDE_FROM_ALL and only attached to your test target, so they — and the emulator — only come into play when you build that target.

If you would rather run the tests only from a native build, arrange for the cross build not to create them at all. Turn off FELSPAR_CHECK_TESTS (which stops felspar-test building and registering its own tests, and is a natural flag to gate your own add_test_run calls on), or guard the test target and its tests directly:

if(NOT CMAKE_CROSSCOMPILING)
    add_custom_target(check)
    add_test_run(check your-library TESTS checks1.cpp checks2.cpp)
endif()

The cross build then produces only the shipping artifacts, with no emulator involved.

Why the wrapper is not part of this library

A wrapper like the one above is specific to a toolchain (here mingw + wine) and often to a project — it may also need to select a headless graphics or audio backend, locate the program's data, and so on. felspar-test stays free of all of that by depending only on the generic CMAKE_CROSSCOMPILING_EMULATOR hook. Keep the wrapper next to your toolchain file, where those target- and project-specific choices already live, so the test library remains platform-agnostic.