🔝 Retour au Sommaire
La majorité des runners CI — hébergés ou auto-hébergés — fonctionnent sur des machines x86_64. Pourtant, les projets C++ modernes doivent de plus en plus produire des binaires pour d'autres architectures : ARM64 (aarch64) pour les serveurs Graviton d'AWS et les SBC type Raspberry Pi, ARMv7 pour l'embarqué et l'IoT, et RISC-V pour l'écosystème émergent de processeurs ouverts. Compiler nativement sur chaque architecture cible nécessiterait des runners dédiés pour chacune — un luxe que peu d'organisations peuvent se permettre.
La cross-compilation résout ce problème : un compilateur tournant sur x86_64 produit du code machine pour une architecture différente. Le binaire résultant ne peut pas s'exécuter sur la machine de build, mais il est prêt à être déployé sur la cible. En CI, cela signifie qu'un seul pool de runners x86_64 peut produire des binaires pour ARM64, ARMv7, RISC-V et toute autre architecture supportée par le compilateur.
Cette section couvre la mise en place de la cross-compilation dans un pipeline CI/CD, depuis l'installation des toolchains jusqu'à l'exécution des tests sur l'architecture cible via émulation.
La cross-compilation introduit une terminologie spécifique qu'il faut maîtriser pour comprendre les fichiers de configuration :
Host — La machine qui exécute le compilateur. En CI, c'est le runner, typiquement x86_64.
Target — L'architecture pour laquelle le code est généré. C'est l'architecture du binaire produit (ARM64, ARMv7, RISC-V).
Triplet — Une chaîne qui identifie complètement une cible de compilation sous la forme architecture-vendor-os-abi. Les triplets les plus courants :
| Triplet | Architecture | Usage typique |
|---|---|---|
x86_64-linux-gnu |
x86_64 | Compilation native classique |
aarch64-linux-gnu |
ARM 64-bit | Serveurs ARM, Raspberry Pi 4+, Apple Silicon (Linux) |
arm-linux-gnueabihf |
ARM 32-bit (hard float) | Raspberry Pi 2/3, embarqué ARM |
riscv64-linux-gnu |
RISC-V 64-bit | SBC RISC-V, simulateurs |
Sysroot — Un répertoire contenant les headers et les librairies de la plateforme cible. Le cross-compilateur y cherche les fichiers d'inclusion (<stdio.h>, <vector>) et les librairies de linkage (libc.so, libstdc++.so). Sans sysroot correct, le compilateur ne peut pas résoudre les dépendances de la cible.
En compilation native, le compilateur, les headers et les librairies appartiennent tous au même système. En cross-compilation, trois éléments doivent être coordonnés :
- Le cross-compilateur — Un compilateur qui tourne sur x86_64 mais génère du code pour la cible (par exemple
aarch64-linux-gnu-g++). - Les headers et librairies de la cible — Les fichiers d'en-tête de la libc, de libstdc++ et des librairies système de la cible.
- Les dépendances tierces — Les librairies du projet (installées via Conan, vcpkg ou le système) doivent être compilées pour l'architecture cible, pas pour x86_64.
Le point 3 est souvent le plus problématique : un apt-get install libssl-dev installe la version x86_64 de OpenSSL, inutilisable par un cross-compilateur ARM64. Il faut installer la version ARM64 de la librairie, soit via les paquets multiarch de Debian/Ubuntu, soit via une compilation croisée de la dépendance.
Ubuntu fournit des cross-compilateurs GCC prépackagés pour les architectures les plus courantes :
# ARM64 (aarch64)
sudo apt-get install -y \
g++-14-aarch64-linux-gnu \
gcc-14-aarch64-linux-gnu \
binutils-aarch64-linux-gnu
# ARM 32-bit (hard float)
sudo apt-get install -y \
g++-14-arm-linux-gnueabihf \
gcc-14-arm-linux-gnueabihf \
binutils-arm-linux-gnueabihf
# RISC-V 64-bit
sudo apt-get install -y \
g++-14-riscv64-linux-gnu \
gcc-14-riscv64-linux-gnu \
binutils-riscv64-linux-gnuAprès installation, les compilateurs sont disponibles sous leur nom complet avec le triplet en préfixe :
$ aarch64-linux-gnu-g++-14 --version
aarch64-linux-gnu-g++-14 (Ubuntu 14.2.0-1ubuntu1) 14.2.0
$ aarch64-linux-gnu-g++-14 -dumpmachine
aarch64-linux-gnuCes paquets incluent automatiquement le sysroot correspondant : les headers et librairies ARM64 de la libc et de libstdc++ sont installés sous /usr/aarch64-linux-gnu/.
⚠️ Versions disponibles. Les versions des cross-compilateurs dans les dépôts Ubuntu peuvent être en retrait par rapport au compilateur natif. Si votre projet nécessite GCC 15 en cross-compilation et que le paquet n'est pas encore disponible, vous devrez soit utiliser un PPA, soit construire le cross-compilateur depuis les sources, soit utiliser une approche Docker (section suivante).
Clang a une approche fondamentalement différente de GCC pour la cross-compilation : un seul binaire clang++ peut cibler n'importe quelle architecture supportée. Il n'y a pas de aarch64-linux-gnu-clang++ — on utilise le même clang++ avec l'option --target :
# Clang cible ARM64
clang++-20 --target=aarch64-linux-gnu \
--sysroot=/usr/aarch64-linux-gnu \
-o hello hello.cpp
# Clang cible RISC-V 64
clang++-20 --target=riscv64-linux-gnu \
--sysroot=/usr/riscv64-linux-gnu \
-o hello hello.cppCette architecture unifiée simplifie la gestion des toolchains en CI : une seule installation de Clang suffit pour toutes les cibles. Il faut cependant installer les sysroots séparément (les headers et librairies de chaque architecture cible) :
# Installer les sysroots sans le compilateur GCC complet
sudo apt-get install -y \
libc6-dev-arm64-cross \
libstdc++-14-dev-arm64-cross \
libc6-dev-riscv64-cross \
libstdc++-14-dev-riscv64-crossClang a également besoin du linker de la cible. Par défaut, il tente d'utiliser ld, qui est le linker x86_64. Deux options :
# Option 1 : utiliser le linker GNU de la cible
clang++-20 --target=aarch64-linux-gnu \
-fuse-ld=/usr/bin/aarch64-linux-gnu-ld \
...
# Option 2 : utiliser LLD (linker LLVM, multi-cible comme Clang)
clang++-20 --target=aarch64-linux-gnu \
-fuse-ld=lld \
...LLD (le linker LLVM) est recommandé pour la cross-compilation avec Clang car, comme Clang lui-même, un seul binaire supporte toutes les cibles. C'est la combinaison la plus propre : clang++ --target=<triplet> -fuse-ld=lld suffit pour cibler n'importe quelle architecture.
Pour un pipeline CI reproductible, une image Docker dédiée à la cross-compilation est l'approche la plus fiable :
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# Activer le support multiarch
RUN dpkg --add-architecture arm64 && \
dpkg --add-architecture armhf
RUN apt-get update && apt-get install -y --no-install-recommends \
# Toolchain native
g++-15 cmake ninja-build ccache \
# Cross-compilateurs GCC
g++-14-aarch64-linux-gnu \
g++-14-arm-linux-gnueabihf \
g++-14-riscv64-linux-gnu \
# Clang (cross-compilation unifiée)
clang-20 lld-20 \
# Sysroots
libc6-dev-arm64-cross libstdc++-14-dev-arm64-cross \
libc6-dev-armhf-cross libstdc++-14-dev-armhf-cross \
libc6-dev-riscv64-cross libstdc++-14-dev-riscv64-cross \
# Émulation pour les tests
qemu-user-static binfmt-support \
&& rm -rf /var/lib/apt/lists/*Cette image contient tout le nécessaire pour cross-compiler vers ARM64, ARMv7 et RISC-V, avec le choix entre GCC et Clang, et l'émulation QEMU pour exécuter les tests (détaillée plus loin).
CMake gère la cross-compilation via des fichiers toolchain — des fichiers CMake qui décrivent la cible et les outils de compilation. Le fichier toolchain est passé à CMake via -DCMAKE_TOOLCHAIN_FILE=<chemin>.
# cmake/toolchains/aarch64-linux-gnu-gcc.cmake
# Système cible
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
# Triplet (réutilisable dans les chemins)
set(CROSS_TRIPLET "aarch64-linux-gnu")
# Compilateurs
set(CMAKE_C_COMPILER ${CROSS_TRIPLET}-gcc-14)
set(CMAKE_CXX_COMPILER ${CROSS_TRIPLET}-g++-14)
# Sysroot (headers et librairies de la cible)
set(CMAKE_SYSROOT /usr/${CROSS_TRIPLET})
# Où chercher les librairies et headers de la cible
set(CMAKE_FIND_ROOT_PATH /usr/${CROSS_TRIPLET})
# Ne pas chercher les programmes (comme protoc) dans le sysroot —
# ils doivent tourner sur l'hôte, pas sur la cible
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# Chercher les librairies et headers uniquement dans le sysroot
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) # cmake/toolchains/aarch64-linux-gnu-clang.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CROSS_TRIPLET "aarch64-linux-gnu")
# Clang : même binaire, option --target
set(CMAKE_C_COMPILER clang-20)
set(CMAKE_CXX_COMPILER clang++-20)
set(CMAKE_C_COMPILER_TARGET ${CROSS_TRIPLET})
set(CMAKE_CXX_COMPILER_TARGET ${CROSS_TRIPLET})
# Utiliser LLD comme linker (multi-cible)
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld-20")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld-20")
set(CMAKE_SYSROOT /usr/${CROSS_TRIPLET})
set(CMAKE_FIND_ROOT_PATH /usr/${CROSS_TRIPLET})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) # cmake/toolchains/riscv64-linux-gnu-gcc.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR riscv64)
set(CROSS_TRIPLET "riscv64-linux-gnu")
set(CMAKE_C_COMPILER ${CROSS_TRIPLET}-gcc-14)
set(CMAKE_CXX_COMPILER ${CROSS_TRIPLET}-g++-14)
set(CMAKE_SYSROOT /usr/${CROSS_TRIPLET})
set(CMAKE_FIND_ROOT_PATH /usr/${CROSS_TRIPLET})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) Les fichiers toolchain sont versionnés dans le dépôt, typiquement sous cmake/toolchains/ :
cmake/
└── toolchains/
├── aarch64-linux-gnu-gcc.cmake
├── aarch64-linux-gnu-clang.cmake
├── arm-linux-gnueabihf-gcc.cmake
├── riscv64-linux-gnu-gcc.cmake
└── native.cmake # Optionnel : compilation native explicite
La commande CMake en cross-compilation devient :
cmake -B build-arm64 \
-G Ninja \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/aarch64-linux-gnu-gcc.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-arm64 --parallel $(nproc)Les trois directives CMAKE_FIND_ROOT_PATH_MODE_* sont les plus importantes et les plus mal comprises :
PROGRAM NEVER — Les programmes (outils de génération de code comme protoc, scripts de build) doivent être exécutables sur l'hôte. Un protoc compilé pour ARM64 ne peut pas tourner sur un runner x86_64. CMake doit donc chercher les programmes en dehors du sysroot, dans les chemins standard de l'hôte.
LIBRARY ONLY et INCLUDE ONLY — Les librairies et les headers doivent provenir exclusivement du sysroot de la cible. Linker un binaire ARM64 avec une librairie x86_64 produirait un binaire corrompu. Le mode ONLY empêche CMake de "tomber" accidentellement sur les librairies x86_64 installées sur l'hôte.
.cross_build:
stage: build
image: registry.exemple.com/cpp-cross-build:latest
script:
- cmake -B ${BUILD_DIR}
-G Ninja
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/${TOOLCHAIN_FILE}
-DCMAKE_BUILD_TYPE=${BUILD_TYPE}
-DCMAKE_CXX_STANDARD=${CPP_STANDARD}
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
- cmake --build ${BUILD_DIR} --parallel $(nproc)
cache:
paths:
- .ccache/
policy: pull-push
artifacts:
paths:
- ${BUILD_DIR}/bin/
- ${BUILD_DIR}/lib/
- ${BUILD_DIR}/tests/
- ${BUILD_DIR}/CTestTestfile.cmake
expire_in: 2 hours
build-arm64-gcc:
extends: .cross_build
variables:
TOOLCHAIN_FILE: "aarch64-linux-gnu-gcc.cmake"
BUILD_TYPE: "Release"
CPP_STANDARD: "20"
BUILD_DIR: "build-arm64"
cache:
key: "ccache-arm64-gcc-${CI_COMMIT_REF_SLUG}"
build-riscv64-gcc:
extends: .cross_build
variables:
TOOLCHAIN_FILE: "riscv64-linux-gnu-gcc.cmake"
BUILD_TYPE: "Release"
CPP_STANDARD: "20"
BUILD_DIR: "build-riscv64"
cache:
key: "ccache-riscv64-gcc-${CI_COMMIT_REF_SLUG}"Le cache ccache est séparé par architecture cible : le cache d'un build ARM64 n'est pas réutilisable par un build RISC-V (les fichiers objets sont évidemment incompatibles).
jobs:
cross-build:
name: "Cross-build — ${{ matrix.target }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: aarch64
toolchain: aarch64-linux-gnu-gcc.cmake
packages: g++-14-aarch64-linux-gnu
qemu_arch: aarch64
- target: armv7
toolchain: arm-linux-gnueabihf-gcc.cmake
packages: g++-14-arm-linux-gnueabihf
qemu_arch: arm
- target: riscv64
toolchain: riscv64-linux-gnu-gcc.cmake
packages: g++-14-riscv64-linux-gnu
qemu_arch: riscv64
steps:
- uses: actions/checkout@v4
- name: Install cross-compilation toolchain
run: |
sudo apt-get update
sudo apt-get install -y \
${{ matrix.packages }} \
ninja-build ccache \
qemu-user-static
- name: Setup ccache
uses: hendrikmuhs/ccache-action@v1
with:
key: cross-${{ matrix.target }}
max-size: 1G
- name: Cross-compile
run: |
cmake -B build-${{ matrix.target }} \
-G Ninja \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/${{ matrix.toolchain }} \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_STANDARD=20 \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
cmake --build build-${{ matrix.target }} --parallel $(nproc)
- name: Verify binary architecture
run: |
file build-${{ matrix.target }}/bin/* | head -5
# Doit afficher "ELF 64-bit LSB ... ARM aarch64" ou similaire
- name: Upload cross-compiled artifacts
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.target }}
path: |
build-${{ matrix.target }}/bin/
build-${{ matrix.target }}/lib/
build-${{ matrix.target }}/tests/
build-${{ matrix.target }}/CTestTestfile.cmakeLe step "Verify binary architecture" est une vérification de sanité importante. La commande file affiche le format du binaire et son architecture cible. Si le fichier toolchain est mal configuré, le binaire produit pourrait être en x86_64 au lieu de ARM64 — ce step détecte immédiatement ce problème :
$ file build-arm64/bin/mon-application
build-arm64/bin/mon-application: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (GNU/Linux), dynamically linked, ...
$ file build-riscv64/bin/mon-application
build-riscv64/bin/mon-application: ELF 64-bit LSB pie executable, UCB RISC-V, RVC, double-float ABI, version 1 (GNU/Linux), dynamically linked, ...Le binaire cross-compilé ne peut pas s'exécuter nativement sur le runner x86_64. Pour exécuter les tests sans matériel ARM ou RISC-V, on utilise QEMU user-mode emulation : QEMU intercepte les instructions de l'architecture cible et les traduit en instructions x86_64 à la volée.
# Installation
sudo apt-get install -y qemu-user-static binfmt-support
# Vérification
qemu-aarch64-static --versionLe paquet binfmt-support enregistre QEMU comme interpréteur pour les binaires ELF d'architectures étrangères. Après installation, le noyau Linux redirige automatiquement l'exécution de tout binaire ARM64 vers qemu-aarch64-static, de manière transparente :
# Ceci fonctionne sur un hôte x86_64 grâce à binfmt_misc + QEMU
$ ./build-arm64/bin/mon-application --version
mon-application v1.2.3Le binaire ARM64 s'exécute comme s'il était natif — sans commande qemu-aarch64-static explicite. Cette transparence est essentielle pour CTest, qui lance les binaires de test sans savoir qu'ils sont cross-compilés.
# GitLab CI
test-arm64:
stage: test
image: registry.exemple.com/cpp-cross-build:latest
needs:
- job: build-arm64-gcc
artifacts: true
variables:
QEMU_LD_PREFIX: "/usr/aarch64-linux-gnu"
script:
- chmod +x build-arm64/tests/* build-arm64/bin/* 2>/dev/null || true
- cd build-arm64
- ctest --parallel $(nproc) --timeout 300 --output-on-failure
allow_failure: true# GitHub Actions
test-cross:
name: "Test — ${{ matrix.target }}"
runs-on: ubuntu-latest
needs: [cross-build]
strategy:
matrix:
include:
- target: aarch64
qemu_ld_prefix: /usr/aarch64-linux-gnu
- target: riscv64
qemu_ld_prefix: /usr/riscv64-linux-gnu
steps:
- uses: actions/checkout@v4
- name: Install QEMU
run: sudo apt-get install -y qemu-user-static binfmt-support
- uses: actions/download-artifact@v4
with:
name: build-${{ matrix.target }}
path: build-${{ matrix.target }}/
- name: Run cross-compiled tests
env:
QEMU_LD_PREFIX: ${{ matrix.qemu_ld_prefix }}
run: |
chmod +x build-${{ matrix.target }}/tests/* || true
cd build-${{ matrix.target }}
ctest --parallel 2 --timeout 300 --output-on-failureQEMU_LD_PREFIX est une variable d'environnement cruciale. Elle indique à QEMU où trouver les librairies partagées de la cible. Un binaire ARM64 linké dynamiquement a besoin de libc.so.6 et libstdc++.so.6 en version ARM64 — ces librairies se trouvent sous /usr/aarch64-linux-gnu/, pas dans les chemins standards /lib/ et /usr/lib/ qui contiennent les versions x86_64. Sans QEMU_LD_PREFIX, QEMU tente de charger les librairies x86_64 dans un processus ARM64, ce qui échoue immédiatement.
Parallélisme réduit (--parallel 2). L'émulation QEMU est significativement plus lente que l'exécution native — un facteur 5x à 20x est courant. Exécuter trop de tests en parallèle sous émulation peut saturer le CPU et la mémoire du runner. Deux tests simultanés sont un point de départ prudent.
allow_failure: true (GitLab CI) est parfois utilisé pour les tests cross-compilés car certains tests sensibles au timing (tests de concurrence, benchmarks) peuvent échouer sous émulation en raison de la lenteur et du comportement non-déterministe de QEMU. C'est un compromis pragmatique — l'objectif est de valider la fonctionnalité, pas les performances.
Timeout augmenté (--timeout 300). Le facteur de ralentissement de QEMU impose un timeout plus généreux que pour les tests natifs.
L'émulation en user-mode couvre la grande majorité des cas mais présente des limites :
| Limitation | Impact | Contournement |
|---|---|---|
| Performance 5-20x plus lente | Tests longs, benchmarks inutilisables | Accepter la lenteur ou tester sur du matériel réel |
| Threads : comportement de scheduling différent | Tests de concurrence potentiellement non représentatifs | Tests de concurrence sur matériel réel ou allow_failure |
| Appels système exotiques | Quelques syscalls non implémentés | Rare en pratique — la majorité des applications fonctionnent |
| Pas d'accès matériel (GPIO, périphériques) | Tests matériels impossibles | Nécessite du matériel réel (hors scope CI standard) |
Pour les projets où l'émulation est insuffisante (embarqué avec accès matériel, tests de performance critiques), un runner CI auto-hébergé sur du matériel ARM est la solution. Un Raspberry Pi 4 ou 5 peut servir de runner GitLab CI ou GitHub Actions pour les tests ARM64 — la compilation reste sur x86_64, seuls les tests s'exécutent sur ARM.
Ubuntu et Debian supportent l'installation de librairies pour plusieurs architectures simultanément via le système multiarch :
# Activer l'architecture ARM64
sudo dpkg --add-architecture arm64
# Installer une librairie pour ARM64
sudo apt-get update
sudo apt-get install -y libssl-dev:arm64 zlib1g-dev:arm64 Le suffixe :arm64 indique au gestionnaire de paquets d'installer la version ARM64 de la librairie. Les fichiers sont installés sous /usr/lib/aarch64-linux-gnu/ et sont trouvés automatiquement par CMake lorsque le CMAKE_FIND_ROOT_PATH pointe vers /usr/aarch64-linux-gnu.
Le multiarch fonctionne bien pour les dépendances système courantes (OpenSSL, zlib, libcurl), mais n'est pas toujours disponible pour les librairies plus spécialisées.
Conan est le gestionnaire de dépendances le mieux adapté à la cross-compilation en C++. Il utilise un système de profils qui décrivent séparément la machine hôte et la machine cible :
# ~/.conan2/profiles/arm64
[settings]
os=Linux
arch=armv8
compiler=gcc
compiler.version=14
compiler.libcxx=libstdc++11
build_type=Release
[conf]
tools.cmake.cmaketoolchain:toolchain_file=cmake/toolchains/aarch64-linux-gnu-gcc.cmake
[buildenv]
CC=aarch64-linux-gnu-gcc-14
CXX=aarch64-linux-gnu-g++-14 L'installation des dépendances cross-compilées :
# --profile:host = cible, --profile:build = machine de build
conan install conanfile.py \
--profile:host=arm64 \
--profile:build=default \
--build=missingLe paramètre --build=missing compile les dépendances qui n'existent pas en version ARM64 dans le cache Conan. La première exécution est lente (chaque dépendance est cross-compilée), mais les résultats sont cachés et réutilisés lors des builds suivants.
vcpkg supporte également la cross-compilation via ses triplets :
# vcpkg/triplets/community/arm64-linux.cmake
set(VCPKG_TARGET_ARCHITECTURE arm64)
set(VCPKG_CRT_LINKAGE dynamic)
set(VCPKG_LIBRARY_LINKAGE static)
set(VCPKG_CMAKE_SYSTEM_NAME Linux)
set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE
"${CMAKE_CURRENT_LIST_DIR}/../../cmake/toolchains/aarch64-linux-gnu-gcc.cmake")vcpkg install openssl zlib --triplet=arm64-linuxPour distribuer des paquets pour plusieurs architectures, le pipeline de release doit produire un artifact par cible :
# GitHub Actions — release multi-architecture
jobs:
release-assets:
strategy:
matrix:
include:
- target: linux-amd64
toolchain: "" # Compilation native
arch_deb: amd64
- target: linux-arm64
toolchain: cmake/toolchains/aarch64-linux-gnu-gcc.cmake
arch_deb: arm64
- target: linux-riscv64
toolchain: cmake/toolchains/riscv64-linux-gnu-gcc.cmake
arch_deb: riscv64
steps:
- uses: actions/checkout@v4
- name: Install toolchain
run: |
sudo apt-get update
sudo apt-get install -y ninja-build ccache
if [ -n "${{ matrix.toolchain }}" ]; then
sudo apt-get install -y \
g++-14-$(echo ${{ matrix.target }} | sed 's/linux-//')-linux-gnu \
|| true
fi
- name: Build
run: |
TOOLCHAIN_ARG=""
if [ -n "${{ matrix.toolchain }}" ]; then
TOOLCHAIN_ARG="-DCMAKE_TOOLCHAIN_FILE=${{ matrix.toolchain }}"
fi
cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
${TOOLCHAIN_ARG} \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
cmake --build build --parallel $(nproc)
strip build/bin/* 2>/dev/null || true
- name: Create archive
run: |
VERSION="${GITHUB_REF_NAME#v}"
ARCHIVE="mon-projet-${VERSION}-${{ matrix.target }}"
mkdir -p ${ARCHIVE}/bin
cp build/bin/* ${ARCHIVE}/bin/
cp LICENSE README.md ${ARCHIVE}/
tar czf "${ARCHIVE}.tar.gz" ${ARCHIVE}/
sha256sum "${ARCHIVE}.tar.gz" > "${ARCHIVE}.tar.gz.sha256"
- uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.target }}
path: |
*.tar.gz
*.sha256La page de release résultante propose des téléchargements pour chaque architecture :
Assets:
mon-projet-1.2.3-linux-amd64.tar.gz (3.2 MB)
mon-projet-1.2.3-linux-amd64.tar.gz.sha256
mon-projet-1.2.3-linux-arm64.tar.gz (2.9 MB)
mon-projet-1.2.3-linux-arm64.tar.gz.sha256
mon-projet-1.2.3-linux-riscv64.tar.gz (3.0 MB)
mon-projet-1.2.3-linux-riscv64.tar.gz.sha256
Docker supporte les images multi-architecture via les manifestes multi-plateforme. Une seule référence d'image (mon-projet:1.2.3) peut contenir des variantes pour x86_64, ARM64, et d'autres architectures. Docker sélectionne automatiquement la bonne variante en fonction de l'architecture de la machine qui exécute docker pull.
L'action docker/build-push-action combinée avec QEMU supporte cette fonctionnalité :
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push multi-arch image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}Avec platforms: linux/amd64,linux/arm64, Docker Buildx exécute le Dockerfile une fois par architecture (en utilisant QEMU pour émuler ARM64) et crée un manifeste unifié. L'utilisateur final n'a aucune manipulation à faire — docker pull récupère automatiquement la bonne variante.
⚠️ Performance. Le build Docker sous émulation QEMU est lent — la compilation C++ à l'intérieur d'un Dockerfile émulé ARM64 peut être 10-20x plus lente qu'en natif. L'approche recommandée est de cross-compiler les binaires en dehors de Docker (avec les toolchains décrites plus haut), puis de ne faire que leCOPYdes binaires pré-compilés dans un Dockerfile minimal, sans compilation intra-Docker.
| Élément | x86_64 (natif) | ARM64 | ARMv7 | RISC-V 64 |
|---|---|---|---|---|
| Cross-compilateur GCC | — | aarch64-linux-gnu-g++-14 |
arm-linux-gnueabihf-g++-14 |
riscv64-linux-gnu-g++-14 |
| Cross-compilateur Clang | — | clang++-20 --target=aarch64-linux-gnu |
clang++-20 --target=arm-linux-gnueabihf |
clang++-20 --target=riscv64-linux-gnu |
| Fichier toolchain CMake | Non requis | aarch64-linux-gnu-gcc.cmake |
arm-linux-gnueabihf-gcc.cmake |
riscv64-linux-gnu-gcc.cmake |
| Tests via QEMU | Natif | qemu-aarch64-static |
qemu-arm-static |
qemu-riscv64-static |
QEMU_LD_PREFIX |
— | /usr/aarch64-linux-gnu |
/usr/arm-linux-gnueabihf |
/usr/riscv64-linux-gnu |
| Paquet DEB arch | amd64 |
arm64 |
armhf |
riscv64 |
| Profil Conan arch | x86_64 |
armv8 |
armv7hf |
riscv64 |
Section suivante : 38.7 Matrix builds : Multi-compilateur, multi-version — Stratégies avancées pour les matrices de build : combinaisons compilateur × standard × architecture × sanitizer, matrices conditionnelles, et optimisation du temps total de pipeline.