ADR-107 — Pipeline CI/CD linux-air : gate sur notre std, cross-build x86 → exécution native aarch64, et stratégie de cache
Statut : Accepté (2026-07-26, décision BDFL). RFC de direction
(ADR-015).
S’appuie sur ADR-103 (discipline de
build linux-air — tout exécutable Air lié à la std linux-air, jamais gnu),
ADR-088 (std sur PAL safe, sans libc C),
ADR-106 (Air = target_os distinct
non-POSIX ; renommage planifié <arch>-air-none),
ADR-050 (spec de la cible),
ADR-014 (x86_64 et aarch64 dès le jour 1),
ADR-025 (builds reproductibles),
ADR-031 (couverture en root sur runners
self-hosted), ADR-030 (deps test-only exemptées de
la règle des 80 %).
Catégorie : Gouvernance de build + infrastructure CI/CD. N’altère aucune couche scellée ; redéfinit la cible d’exécution des gates, le placement du travail de build/test entre exécuteurs, et la politique de cache.
Contexte
[ADR-103] impose que tout exécutable Air soit lié à la std linux-air (PAL safe,
sans libc C, static-pie, zéro glibc), jamais à la std gnu. [ADR-106] a tranché
qu’Air est un target_os distinct et non-POSIX (cfg(unix) == false). Mais la
CI n’a pas suivi : le pipeline actuel (ci.yml) mesure fmt/clippy/tests/
couverture sur x86_64-unknown-linux-gnu (runner speedy), l’invariant aarch64
étant tenu par un cross-check compile-only (sur x86) plus un job aarch64-native
qui compile ET teste sur le Pi (raspi-srv-2), et seulement sur main/hebdo.
Deux écarts en découlent, l’un doctrinal, l’autre opérationnel :
-
Écart doctrinal — les tests tournent sur la mauvaise std. ~3 685 tests sur ~69 crates s’exécutent aujourd’hui contre la glibc (gnu), pas contre notre std linux-air. On ne prouve donc pas que le code Air fonctionne sur la plateforme qu’on livre. C’est précisément le critère de réussite du chantier de complétion du PAL : la suite de tests doit tourner sur NOTRE std (amendement ADR-103 §2). La vague d’ADR à venir (air-keystore, certs, ML-KEM, air-log, air-sandbox…) va ajouter des crates : les faire naître testés sur gnu creuserait la dette.
-
Écart opérationnel — le Pi compile. Le job
aarch64-nativecompile sur raspi-srv-2, machine modeste (invariant « matériel modeste », Principe 9). Chaque validation aarch64 y relance une construction, lente. Recompiler l’intégralité des composants — a fortioribuild-std(recompilation decore/alloc/stdPAL) — à chaque fois est le goulot que cet ADR élimine.
Directive BDFL (2026-07-26). « On bascule les tests sur linux-air en gate. On sélectionne les crates affectés par PR (le full reste garanti avant merge). On cross- compile aarch64 sur une machine x86 rapide et on n’exécute en natif sur le Pi que pour valider le réel. Et on met en cache pour qu’une PR ne reconstruise pas tout, surtout pas sur le Pi. »
Le levier qui rend tout cela possible est notre propre design : *-linux-air est
C-free, static-pie, sans libc C. Cross-compiler x86_64 → aarch64 ne requiert donc
aucune toolchain C croisée (pas de gcc-aarch64, pas de sysroot C) — seulement
rust-lld, déjà utilisé. Le binaire produit est autonome : il se pose sur le Pi et
s’exécute sans dépendance runtime. La cross-compilation, coûteuse ailleurs, est ici
triviale — c’est un dividende direct de [ADR-088]/[ADR-106].
Décision
1. La cible de gate est linux-air, plus gnu
Les gates de qualité (compilation, clippy, tests, couverture) s’exécutent
contre la std linux-air (*-unknown-linux-air, renommage <arch>-air-none planifié
par [ADR-106]), pas contre gnu. Un test Air doit valider le comportement sur la
plateforme livrée. La std linux-air est construite via -Z build-std (toolchain pinnée,
[ADR-050]). La bascule est progressive mais gatée : un crate est « migré » quand sa
suite passe sur linux-air ; le plancher de couverture ([ADR-031] : 100 % hors exceptions
couches 0/1) est mesuré sur linux-air pour les crates migrés. Un registre de
migration (dans docs/) suit le reste jusqu’à zéro crate sur gnu.
Résidu gnu admis, nommé. Là où un test dépend d’un service hôte hors PAL (p. ex.
interposition glibc, cohabitation toolchain — cf. dette connue), il reste sur gnu,
listé explicitement (même discipline que [ADR-035]/EXCEPTIONS.md), jamais par
défaut silencieux.
2. aarch64 : cross-build sur x86, exécution native sur le Pi
raspi-srv-2 cesse de compiler. Il devient un banc d’exécution natif ARM.
- La compilation aarch64 (
--target aarch64-...-linux-air,build-stdinclus) se fait sur un exécuteur x86 rapide (carbon/speedy). - L’exécution des binaires de test se fait nativement sur le Pi via le mécanisme
cargo
target.<triple>.runner: un script (ci/run-on-target.sh) transfère le binaire de test (autonome, static-pie) vers raspi-srv-2, l’exécute en SSH, et remonte code de sortie +stdout/stderr. Seuls les artefacts (petits) traversent le réseau — jamais les sources ni l’arbre de build. - Bénéfice induit : la dudect aarch64 (mesure cycle-précise, cf. §5) s’exécute nativement sur le Pi — le seul endroit où elle a un sens.
- Vigilance nommée :
cpufeatures(tiré paraes/sha2) touche la libc sur aarch64 — dette C-free connue ([[doctrine-sourcing-crypto]]) ; à traiter à part, susceptible de gêner le link cross deair-cryptoen aarch64, non bloquant pour le reste du workspace.
3. Deux niveaux de gate : PR (affecté) / merge-queue (full)
- Sur une PR (
push) : gates linux-air x86_64 sur les crates affectés par le diff (le jobchanges/git diffexistant, étendu au calcul de l’ensemble des crates impactés + leurs dépendants). Feedback rapide ; le Pi n’est pas sollicité. - En merge-queue (avant
main) : le full — workspace complet linux-air, exécution native aarch64 (§2), couverture 100 % (couches 0/1), barrière complète,repro([ADR-025]). C’est là, et seulement là, que le Pi travaille.
Aucune perte de rigueur : rien n’entre sur main sans le full. La sélection par crates
affectés n’allège que le retour PR, jamais la garantie de merge.
4. Cache : ne pas tout reconstruire
Trois caches cumulables, sûrs parce que nos builds sont reproductibles ([ADR-025]) :
- Cache des artefacts
build-std— le coût dominant d’un build linux-air est la recompilation decore/alloc/stdPAL. La toolchain étant pinnée et le PAL bougeant rarement, ces artefacts sont mis en cache (clé = hash toolchain + sources PAL + spec de cible) et ne se reconstruisent que quand cette clé change. sccachepartagé sur carbon/speedy — cache de compilation par unité ; une PR ne recompile que ce qui a changé et ses dépendants.target/chaud persistant — nos exécuteurs sont self-hosted (état conservé entre runs) : letarget/est réutilisé (incrémental cargo), là où un runner éphémère repartirait de zéro.
L’invalidation du cache build-std est triviale (elle suit le pin de toolchain et le
hash du PAL) ; toute divergence est rattrapée par le repro de merge-queue.
5. dudect : job CI dédié
Les tests constant-time dudect (crates/*/tests/dudect_*, gatés AIR_DUDECT=1,
[[doctrine-sourcing-crypto]]) tournent dans un job CI dédié, séparé des tests
fonctionnels (mesure bruitée en cycles ⇒ ne doit pas polluer le temps/flakiness des
gates fonctionnels). Il s’exécute sur les deux arches (x86 natif ; aarch64 exécuté
nativement sur le Pi via §2). Cadence : merge-queue et planifié ; un verdict |t| ≥ 10
échoue le job.
6. Fuzz : deux étages (smoke PR / profond nightly)
- PR (smoke) : chaque cible cargo-fuzz rejoue son corpus de régression versionné
- un run très court (borné en temps/itérations). Rôle : « aucune régression connue », rapide, non bloquant pour l’itération.
- Nightly (profond) : run long par cible, sur runner dédié ; tout nouveau crash
est minimisé, ajouté au corpus, et ouvre une issue. Le corpus grossit et nourrit le
smoke des PR suivantes (modèle OSS-Fuzz transposé). Les crates de la vague à venir
(décapsulation ML-KEM, compilateur seccomp d’air-sandbox,
air-json, parsing de certificats,known_hosts) exposent des surfaces d’entrée externes ⇒ cibles fuzz obligatoires (Principe 1). La cratefuzz/reste test-only ([ADR-030]).
7. Tests privilégiés : job isolé sur runner privilégié dédié
Les tests exigeant de vrais privilèges (application seccomp/Landlock d’air-sandbox,
service air-keysign, hostbased) s’exécutent tels quels — fidélité maximale à la cible —
dans un job isolé qui est le seul à tourner privilégié. Ce job vit sur une
machine/VM dédiée et jetable : « privilégié » ne doit pas signifier « poste de dev à
risque ». Le reste des jobs tourne non privilégié. (Ce choix prolonge et borne
[ADR-031] : la couverture root reste, mais confinée à un périmètre nommé et à un
exécuteur sacrifiable.) Alternative écartée : userns/rootless — moins d’effort et
meilleur confinement, mais teste un chemin privilégié approximatif (Landlock/seccomp
au comportement subtilement différent, caps simulées pour air-keysign) ⇒ on
validerait autre chose que la prod. La fidélité l’emporte.
Conséquences
Positives.
- La doctrine devient vérifiée, pas seulement déclarée : les tests prouvent le code sur notre std ([ADR-103] enfin gaté), les nouveaux crates de la vague à venir naissent directement conformes.
- Le Pi ne compile plus : les validations aarch64 passent d’une recompilation complète (lente) à une exécution native de binaires déjà bâtis sur x86. Ordre de grandeur sur le temps de cycle, et respect renforcé du Principe 9 (matériel modeste).
- Itération PR rapide sans sacrifier la garantie de merge (sélection par crates affectés + full en merge-queue).
- Cross-compilation triviale exploitée comme un dividende du design C-free
([ADR-088]/[ADR-106]) — aucun sysroot C, aucun
gcc-aarch64. - Cache sûr parce que reproductible ([ADR-025]) : la reproductibilité n’est plus seulement une garantie de release, elle paie en temps de CI.
Négatives / coûts assumés.
- Complexité du pipeline : runner cargo distant (SSH vers le Pi), caches
build-std/sccache, deux niveaux de gate, jobs dédiés dudect/fuzz/privilégié. Coût d’outillage réel, gravé une fois dansci.yml+ci/. - Fenêtre de dette pendant la bascule : tant que des crates restent sur gnu, deux cibles coexistent en CI. Bornée par le registre de migration (§1) jusqu’à zéro.
- Dépendance à la disponibilité du Pi pour la merge-queue (exécution native aarch64). Mitigée : le Pi n’est plus sur le chemin des PR, seulement du merge ; une panne bloque le merge, pas l’itération.
cpufeatures/aarch64 (§2) peut gêner le link cross d’air-cryptoavant résorption de la dette C-free — nommé, non bloquant pour le workspace.- Renommage à venir (
<arch>-air-none, [ADR-106]) :ci.yml,ci/run-on-target.shet les clés de cache référenceront le nouveau triple le moment venu — migration pilotée par grep, sous l’amendement RFC prévu.
Mise en œuvre (référence, hors décision). L’implémentation est un incrément CI dédié
qui : (a) ajoute target.<triple>.runner + ci/run-on-target.sh (transfert + exécution
SSH + remontée) ; (b) bascule test-coverage sur *-linux-air avec build-std ;
(c) remplace le job aarch64-native (compile-sur-Pi) par aarch64-cross (build x86) +
exécution native via le runner ; (d) câble le cache build-std/sccache ; (e) scinde
dudect, fuzz-smoke/fuzz-nightly, et privileged en jobs distincts ; (f) étend le
job changes au calcul des crates affectés. Ces étapes ne sont pas engagées par cet
ADR ; il en fixe les décisions normatives.
Alternatives rejetées
Aucune alternative n’a été consignée lors de l’instruction.