🔝 Retour au Sommaire
Dans un pipeline C++, le terme "artifact" recouvre deux réalités très différentes. Les artifacts intra-pipeline sont des fichiers transitoires — binaires compilés, rapports de test, compilation database — qui circulent entre les jobs d'un même pipeline et n'ont pas vocation à survivre au-delà de quelques heures. Les artifacts de release sont les livrables du projet : paquets DEB/RPM, images Docker, binaires statiques, archives sources — des fichiers destinés aux utilisateurs finaux et qui doivent être versionnés, reproductibles et conservés à long terme.
La confusion entre ces deux catégories est une source fréquente de problèmes dans les pipelines C++ : des binaires de test traités comme des livrables (non reproductibles, non versionnés), ou des paquets de release stockés comme des artifacts temporaires (expirés après 24 heures, perdus).
Cette section clarifie cette distinction, détaille les formats de livraison pertinents pour un projet C++, et présente les mécanismes de publication sur GitLab et GitHub.
Les artifacts intra-pipeline ont été couverts dans les sections précédentes (38.1.1, 38.1.2, 38.2.1). Récapitulons les bonnes pratiques spécifiques aux projets C++ :
Un répertoire build/ CMake complet peut peser des centaines de mégaoctets : fichiers objets, librairies statiques intermédiaires, fichiers CMake internes, compilation database. Seule une fraction est nécessaire pour les jobs suivants.
# ❌ Trop large — transfère des centaines de Mo inutiles
artifacts:
paths:
- build/
# ✅ Ciblé — seuls les fichiers nécessaires aux tests et au packaging
artifacts:
paths:
- build/bin/ # Exécutables
- build/lib/ # Librairies partagées
- build/tests/ # Binaires de test
- build/CTestTestfile.cmake # Découverte des tests par CTest
- build/tests/CTestTestfile.cmake # Idem pour les sous-répertoires
- build/compile_commands.json # Pour clang-tidy en avalSur un projet de taille moyenne, cette sélection réduit l'artifact de 300-500 Mo à 20-50 Mo, accélérant considérablement le transfert entre jobs.
| Type d'artifact | Durée recommandée | Justification |
|---|---|---|
| Binaires intra-pipeline (build → test) | 1-2 heures | Ne servent qu'au pipeline courant |
| Rapports de test (JUnit XML) | 7 jours | Consultables après coup pour diagnostic |
| Rapports de couverture (HTML) | 7-14 jours | Consultables par l'équipe |
| Paquets DEB/RPM de release | 90 jours à permanent | Livrables distribués aux utilisateurs |
| Binaires de release | 90 jours à permanent | Attachés aux releases |
Sur GitLab CI, la durée se configure via expire_in. Sur GitHub Actions, via retention-days dans actions/upload-artifact.
Un projet C++ peut être distribué sous plusieurs formes, chacune adaptée à un mode de consommation différent.
Un binaire compilé statiquement (-static ou linkage statique de toutes les dépendances) est le format le plus simple à distribuer : un seul fichier exécutable sans dépendance externe. L'utilisateur le télécharge, le rend exécutable, et le lance.
# Build avec linkage statique
- name: Build static binary
run: |
cmake -B build-static \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_EXE_LINKER_FLAGS="-static" \
-DCMAKE_FIND_LIBRARY_SUFFIXES=".a" \
-DBUILD_SHARED_LIBS=OFF
cmake --build build-static --parallel $(nproc)
strip build-static/bin/mon-application # Réduire la taillestrip supprime les symboles de debug du binaire, réduisant sa taille de 50-80%. Un binaire C++ Release de 15 Mo peut descendre à 3-5 Mo après strip. Les symboles de debug peuvent être sauvegardés séparément (via objcopy --only-keep-debug) pour le debugging post-mortem si nécessaire.
Le linkage statique n'est pas toujours possible : certaines librairies (glibc, NSS) ne se linkent pas facilement en statique. L'alternative est de compiler avec musl libc (via une image Alpine ou un cross-compilateur musl) pour obtenir un binaire véritablement autosuffisant :
# Build avec musl pour un binaire 100% statique
FROM alpine:3.20 AS builder
RUN apk add --no-cache g++ cmake ninja-build linux-headers
COPY . /src
WORKDIR /src
RUN cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_EXE_LINKER_FLAGS="-static" \
&& cmake --build build --parallel $(nproc) \
&& strip build/bin/mon-applicationPour les projets qui produisent plusieurs exécutables ou incluent des fichiers de configuration, une archive (tar.gz, zip) est plus adaptée qu'un binaire isolé :
- name: Create release archive
run: |
VERSION="${GITHUB_REF_NAME#v}"
ARCHIVE_NAME="mon-projet-${VERSION}-linux-amd64"
mkdir -p ${ARCHIVE_NAME}/bin ${ARCHIVE_NAME}/etc ${ARCHIVE_NAME}/share
cp build/bin/* ${ARCHIVE_NAME}/bin/
cp config/default.yaml ${ARCHIVE_NAME}/etc/
cp -r share/templates ${ARCHIVE_NAME}/share/
cp LICENSE README.md ${ARCHIVE_NAME}/
# Archive tar.gz (Linux)
tar czf "${ARCHIVE_NAME}.tar.gz" ${ARCHIVE_NAME}/
# Archive zip (cross-platform)
zip -r "${ARCHIVE_NAME}.zip" ${ARCHIVE_NAME}/
# Checksums pour la vérification d'intégrité
sha256sum "${ARCHIVE_NAME}.tar.gz" "${ARCHIVE_NAME}.zip" > checksums-sha256.txtLes checksums SHA256 sont une bonne pratique de distribution : ils permettent aux utilisateurs de vérifier l'intégrité du téléchargement. Les publier dans un fichier séparé (ou directement dans les notes de release) est une convention largement adoptée.
La création de paquets DEB a été introduite en section 38.1.2. Voici une version plus complète intégrant les scripts de maintainer et les dépendances :
- name: Build DEB package
run: |
VERSION="${GITHUB_REF_NAME#v}"
PKG="mon-projet"
ARCH="amd64"
PKG_DIR="pkg/${PKG}_${VERSION}_${ARCH}"
# Structure du paquet
mkdir -p ${PKG_DIR}/DEBIAN
mkdir -p ${PKG_DIR}/usr/bin
mkdir -p ${PKG_DIR}/etc/mon-projet
mkdir -p ${PKG_DIR}/lib/systemd/system
mkdir -p ${PKG_DIR}/usr/share/doc/${PKG}
# Copie des fichiers
cp build/bin/mon-application ${PKG_DIR}/usr/bin/
cp config/default.yaml ${PKG_DIR}/etc/mon-projet/config.yaml
cp deploy/mon-projet.service ${PKG_DIR}/lib/systemd/system/
cp LICENSE ${PKG_DIR}/usr/share/doc/${PKG}/copyright
# Fichier control
cat > ${PKG_DIR}/DEBIAN/control << EOF
Package: ${PKG}
Version: ${VERSION}
Section: utils
Priority: optional
Architecture: ${ARCH}
Depends: libstdc++6 (>= 13), libc6 (>= 2.39)
Maintainer: Equipe Dev <dev@exemple.com>
Description: Mon application C++
Description longue de l'application
sur plusieurs lignes.
EOF
# Script post-installation
cat > ${PKG_DIR}/DEBIAN/postinst << 'POSTINST'
#!/bin/sh
set -e
if [ "$1" = "configure" ]; then
systemctl daemon-reload
systemctl enable mon-projet.service || true
fi
POSTINST
chmod 755 ${PKG_DIR}/DEBIAN/postinst
# Construction
dpkg-deb --build --root-owner-group ${PKG_DIR}
mv pkg/*.deb .
# Vérification
dpkg-deb --info *.deb
lintian *.deb || true # Vérification qualité (non bloquant)Depends: dans le fichier control déclare les dépendances runtime du paquet. Pour un binaire C++ linké dynamiquement, les dépendances minimales sont libstdc++6 (runtime de la Standard Library) et libc6 (runtime C). Les versions minimales correspondent à celles disponibles sur la distribution cible. La commande dpkg-shlibdeps peut calculer automatiquement ces dépendances à partir du binaire :
dpkg-shlibdeps -O build/bin/mon-application--root-owner-group force tous les fichiers du paquet à appartenir à root:root, quelle que soit l'identité de l'utilisateur qui construit le paquet. Sans cette option, les fichiers appartiennent à l'utilisateur du runner CI (souvent root dans Docker, mais pas toujours), ce qui peut causer des avertissements lintian.
lintian est l'outil de vérification de qualité des paquets Debian. Il détecte les problèmes de structure, les permissions incorrectes, les descriptions manquantes, etc. L'exécuter en mode non-bloquant (|| true) permet de voir les avertissements sans bloquer le pipeline.
Pour les distributions basées sur RPM, la construction passe par rpmbuild et un fichier .spec :
- name: Build RPM package
run: |
VERSION="${GITHUB_REF_NAME#v}"
sudo apt-get install -y rpm # rpmbuild sur Ubuntu
mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
# Créer l'archive source attendue par rpmbuild
tar czf ~/rpmbuild/SOURCES/mon-projet-${VERSION}.tar.gz \
--transform "s,^,mon-projet-${VERSION}/," \
build/bin/ config/ deploy/ LICENSE
# Copier le spec file
sed "s/@VERSION@/${VERSION}/g" packaging/mon-projet.spec \
> ~/rpmbuild/SPECS/mon-projet.spec
# Build
rpmbuild -bb ~/rpmbuild/SPECS/mon-projet.spec
cp ~/rpmbuild/RPMS/x86_64/*.rpm .Le fichier .spec est versionné dans le dépôt (dans un répertoire packaging/) avec la version remplacée par un placeholder @VERSION@ :
Name: mon-projet
Version: @VERSION@
Release: 1%{?dist}
Summary: Mon application C++
License: MIT
Source0: %{name}-%{version}.tar.gz
%description
Description longue de l'application.
%install
mkdir -p %{buildroot}/usr/bin
mkdir -p %{buildroot}/etc/mon-projet
cp -a build/bin/mon-application %{buildroot}/usr/bin/
cp -a config/default.yaml %{buildroot}/etc/mon-projet/config.yaml
%files
/usr/bin/mon-application
%config(noreplace) /etc/mon-projet/config.yamlLa création d'images Docker a été abordée en section 38.1.2 (GitLab) et 38.2.2 (GitHub). En contexte de release, l'image doit être taguée avec la version sémantique et des tags additionnels pour faciliter le déploiement :
- name: Tag and push Docker image
run: |
VERSION="${GITHUB_REF_NAME#v}"
MAJOR=$(echo $VERSION | cut -d. -f1)
MINOR=$(echo $VERSION | cut -d. -f1-2)
IMAGE="ghcr.io/${{ github.repository }}"
# Tag avec la version exacte
docker tag local-build ${IMAGE}:${VERSION}
# Tag avec la version mineure (flottant)
docker tag local-build ${IMAGE}:${MINOR}
# Tag avec la version majeure (flottant)
docker tag local-build ${IMAGE}:${MAJOR}
# Tag latest
docker tag local-build ${IMAGE}:latest
docker push ${IMAGE}:${VERSION}
docker push ${IMAGE}:${MINOR}
docker push ${IMAGE}:${MAJOR}
docker push ${IMAGE}:latestCe pattern de multi-tagging est une convention standard dans l'écosystème Docker. L'image mon-projet:1.2.3 est immuable et pointe toujours vers le même contenu. Les tags 1.2, 1 et latest sont des tags flottants qui pointent vers la dernière release de leur gamme. Un utilisateur qui veut la stabilité utilise 1.2.3 ; un utilisateur qui veut les derniers correctifs sans changement d'API utilise 1.2 ; un utilisateur qui veut toujours la dernière version utilise latest.
La gestion de la version est un élément central de la release. Pour un projet C++, la version doit être cohérente entre le binaire, le paquet, l'image Docker et la release.
Le pattern le plus fiable est d'utiliser le tag Git comme source unique de vérité :
# GitHub Actions
- name: Extract version
id: version
run: |
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
VERSION="${GITHUB_REF_NAME#v}" # v1.2.3 → 1.2.3
else
VERSION="0.0.0-dev+$(git rev-parse --short HEAD)"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Version : ${VERSION}"# GitLab CI
variables:
VERSION: "${CI_COMMIT_TAG}"
.extract_version: &extract_version
- |
if [ -n "${CI_COMMIT_TAG}" ]; then
VERSION="${CI_COMMIT_TAG#v}"
else
VERSION="0.0.0-dev+$(git rev-parse --short HEAD)"
fi
echo "VERSION=${VERSION}" >> build.envLa version est ensuite injectée dans le build CMake et dans les métadonnées des paquets :
- name: CMake configure with version
run: |
cmake -B build \
-DPROJECT_VERSION=${{ steps.version.outputs.version }} \
-DCMAKE_BUILD_TYPE=ReleaseDans le CMakeLists.txt, cette version peut alimenter un header généré automatiquement :
project(mon-projet VERSION ${PROJECT_VERSION})
configure_file(
"${CMAKE_SOURCE_DIR}/src/version.hpp.in"
"${CMAKE_BINARY_DIR}/generated/version.hpp"
)// version.hpp.in
#pragma once
#define PROJECT_VERSION "@PROJECT_VERSION@"
#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@Le binaire résultant peut afficher sa version via un flag --version :
if (args.has("--version")) {
std::println("mon-application v{}", PROJECT_VERSION);
return 0;
}Pour les builds intermédiaires (pas sur un tag), git describe fournit une version descriptive automatique :
$ git describe --tags --always
v1.2.3-17-gabcdef0
# Signifie : 17 commits après le tag v1.2.3, au commit abcdef0- name: Dynamic version
id: version
run: |
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "0.0.0-unknown")
VERSION="${VERSION#v}"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"Le suffixe --dirty ajoute -dirty si le working tree contient des modifications non commitées — utile pour détecter les builds non reproductibles.
⚠️ git describenécessite l'historique complet. Sur les runners CI qui effectuent un shallow clone (fetch-depth: 1),git describeéchoue car les tags et l'historique ne sont pas disponibles. Configurezfetch-depth: 0dans le step de checkout pour les jobs qui utilisentgit describe.
Le workflow complet de publication d'une release GitHub combine la création de la release, l'attachement des assets, et la génération automatique des notes :
publish-release:
runs-on: ubuntu-latest
needs: [package-deb, package-rpm, build-static, docker-image]
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
path: release-assets/
merge-multiple: true
- name: Generate checksums
working-directory: release-assets
run: |
sha256sum *.deb *.rpm *.tar.gz 2>/dev/null > SHA256SUMS.txt || true
- name: Determine prerelease status
id: prerelease
run: |
TAG="${GITHUB_REF_NAME}"
if [[ "$TAG" == *"-rc"* ]] || [[ "$TAG" == *"-beta"* ]] || [[ "$TAG" == *"-alpha"* ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: release-assets/*
generate_release_notes: true
draft: false
prerelease: ${{ steps.prerelease.outputs.is_prerelease }}
body: |
## Installation
### Debian/Ubuntu
```bash
sudo dpkg -i mon-projet_${{ github.ref_name }}_amd64.deb
```
### Docker
```bash
docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }}
```
### Binaire statique
```bash
curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mon-projet-linux-amd64.tar.gz
tar xzf mon-projet-linux-amd64.tar.gz
./mon-projet/bin/mon-application --version
```
## Vérification d'intégrité
```bash
sha256sum -c SHA256SUMS.txt
```merge-multiple: true dans download-artifact fusionne tous les artifacts téléchargés dans le même répertoire. Chaque job de packaging a uploadé son artifact sous un nom différent (deb-package, rpm-package, static-binary) — cette option les rassemble.
generate_release_notes: true demande à GitHub de lister automatiquement les pull requests fusionnées depuis le tag précédent. Combiné avec le bloc body: qui fournit les instructions d'installation, la release obtenue est à la fois informative (quoi de neuf) et actionnable (comment installer).
Le statut pre-release est déterminé par le nom du tag : v1.2.3-rc1, v2.0.0-beta.1 ou v3.0.0-alpha.2 sont marqués comme pre-release, tandis que v1.2.3 est une release stable. Les pre-releases ne sont pas affichées comme "Latest release" sur la page du dépôt.
Sur GitLab, la release est créée via la directive native release: du job CI :
create-release:
stage: deploy
image: registry.gitlab.com/gitlab-org/release-cli:latest
needs:
- job: package-deb
artifacts: true
- job: build-static
artifacts: true
script:
- |
VERSION="${CI_COMMIT_TAG#v}"
sha256sum *.deb *.tar.gz > SHA256SUMS.txt 2>/dev/null || true
release:
tag_name: $CI_COMMIT_TAG
name: "Release ${CI_COMMIT_TAG}"
description: |
## Installation
### Debian/Ubuntu
```bash
sudo dpkg -i mon-projet_${CI_COMMIT_TAG#v}_amd64.deb
```
### Docker
```bash
docker pull ${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}
```
## Checksums
Voir le fichier SHA256SUMS.txt dans les assets.
assets:
links:
- name: "Paquet Debian amd64"
url: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/mon-projet/${CI_COMMIT_TAG#v}/mon-projet_${CI_COMMIT_TAG#v}_amd64.deb"
link_type: package
- name: "Archive Linux amd64"
url: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/mon-projet/${CI_COMMIT_TAG#v}/mon-projet-linux-amd64.tar.gz"
link_type: other
- name: "SHA256 Checksums"
url: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/mon-projet/${CI_COMMIT_TAG#v}/SHA256SUMS.txt"
link_type: other
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/'GitLab n'attache pas directement des fichiers aux releases comme GitHub. Les assets sont des liens vers des fichiers stockés dans le Generic Package Registry de GitLab. Un step préalable doit uploader les fichiers vers ce registry :
upload-packages:
stage: package
script:
- |
VERSION="${CI_COMMIT_TAG#v}"
BASE_URL="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/mon-projet/${VERSION}"
# Upload de chaque fichier vers le Package Registry
for file in *.deb *.tar.gz SHA256SUMS.txt; do
[ -f "$file" ] || continue
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--upload-file "$file" \
"${BASE_URL}/${file}"
done
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/'Un livrable de release doit être reproductible : reconstruire exactement le même binaire à partir du même commit et des mêmes dépendances. En C++, cela nécessite de contrôler plusieurs sources de non-déterminisme.
| Source | Problème | Remède |
|---|---|---|
Macros __DATE__, __TIME__ |
Chaque build produit un binaire différent | -Wdate-time (warning) ou CCACHE_SLOPPINESS=time_macros |
| Chemin de build absolu | Encodé dans les infos de debug | -ffile-prefix-map=${PWD}=. |
| Ordre des fichiers objets | Peut varier selon le filesystem | CMake + Ninja gèrent un ordre déterministe |
| Versions des dépendances | apt-get install peut changer de version |
Figer les versions dans le Dockerfile ou utiliser Conan avec lockfiles |
| Timestamp dans les archives | tar enregistre la date de modification |
tar --mtime="2026-01-01" |
| Version du compilateur | Optimisations différentes entre versions | Figer la version dans l'image Docker |
Le flag de compilation -ffile-prefix-map mérite une attention particulière. Il remplace le préfixe du chemin de build dans les informations de debug et les messages d'erreur :
# CMakeLists.txt — pour les builds de release reproductibles
if(CMAKE_BUILD_TYPE STREQUAL "Release")
add_compile_options("-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.")
endif()Sans ce flag, le binaire contient le chemin absolu du répertoire de build (/home/runner/work/projet/projet), ce qui rend chaque build sur un runner différent unique même si le code source est identique.
Les gestionnaires de paquets C++ modernes supportent les lockfiles pour figer les versions exactes des dépendances :
# Conan — générer un lockfile
conan lock create conanfile.py
# Conan — builder à partir du lockfile
conan install conanfile.py --lockfile=conan.lockLe lockfile (conan.lock) est versionné dans le dépôt. Le pipeline de release utilise le lockfile pour installer exactement les mêmes versions de dépendances que celles validées par les tests.
Voici le workflow complet, du développement à la publication, rassemblant tous les éléments de cette section :
Développeur Pipeline CI Utilisateurs
─────────── ─────────── ────────────
git tag v1.2.3 ──────────────▶ Déclenche le workflow
git push --tags de release
│
┌────▼────┐
│ Build │ CMake Release
│ + Test │ Tests complets
└────┬────┘
│
┌─────────┼─────────┐
▼ ▼ ▼
┌─────────┐ ┌──────┐ ┌───────┐
│ DEB │ │ RPM │ │Docker │
│ package │ │ pkg │ │ image │
└────┬────┘ └──┬───┘ └──┬────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────┐
│ Upload vers registries │
│ - GitHub/GitLab Releases │
│ - Container Registry │ ──────▶ docker pull
│ - Package Registry │ ──────▶ apt install
│ + SHA256SUMS.txt │ ──────▶ curl + verify
└──────────────────────────────┘
Le processus est entièrement automatisé : le développeur pousse un tag, et les livrables sont publiés sans intervention humaine. La seule action manuelle est la décision de créer le tag — tout le reste est déterministe et reproductible.
Avant de pousser un tag de version, vérifiez :
- Le CHANGELOG est à jour — les modifications depuis la dernière version sont documentées.
- La version dans
CMakeLists.txtcorrespond au tag (ou est extraite dynamiquement du tag). - Les tests passent sur
main— le dernier pipeline sur la branche principale est vert. - Les dépendances sont figées — le lockfile Conan est commité et à jour.
- Le tag suit le semantic versioning —
v1.2.3pour une release,v1.2.3-rc1pour une release candidate.
Cette checklist peut être partiellement automatisée via un script ou un job CI déclenché par workflow_dispatch qui vérifie ces conditions avant de créer le tag :
# GitHub Actions — vérification pré-release
prepare-release:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate release readiness
run: |
VERSION="${{ github.event.inputs.version }}"
# Vérifier que le tag n'existe pas déjà
if git rev-parse "v${VERSION}" >/dev/null 2>&1; then
echo "ERREUR : Le tag v${VERSION} existe déjà"
exit 1
fi
# Vérifier que le CHANGELOG mentionne cette version
if ! grep -q "## \[${VERSION}\]" CHANGELOG.md; then
echo "ERREUR : Version ${VERSION} absente du CHANGELOG.md"
exit 1
fi
# Vérifier que le lockfile est commité
if [ ! -f conan.lock ]; then
echo "WARNING : Pas de lockfile Conan trouvé"
fi
echo "Prêt pour la release v${VERSION}"Section suivante : 38.6 Cross-compilation : ARM, RISC-V depuis x86_64 — Mise en place de la cross-compilation dans le pipeline CI pour produire des binaires ARM64 et RISC-V depuis des runners x86_64.