ADR-025 — Stratégie de builds reproductibles
Statut : Accepté. Document fondateur de la phase 0.
Catégorie : Méthode (gouvernance technique et infrastructure).
Contexte
Un build reproductible est un build qui, à partir des mêmes sources, produit des artefacts binaires bit-pour-bit identiques indépendamment de la machine, du moment, ou de l’utilisateur qui le déclenche. Cette propriété, longtemps considérée comme un détail d’ingénierie, est devenue un élément structurant de la chaîne de confiance d’un projet logiciel : elle permet à n’importe quel tiers de vérifier que les binaires distribués correspondent exactement aux sources publiées.
Pour Air, la reproductibilité n’est pas un objectif décoratif. Elle découle directement de plusieurs engagements antérieurs :
- Charte principe 3 (Confiance par défaut) : la confiance ne se déclare pas, elle se vérifie. Tant qu’un utilisateur ne peut pas vérifier qu’un binaire d’Air correspond aux sources auditables, la confiance reste un acte de foi.
- Charte principe 5 (Indépendance et durabilité institutionnelle) : la reproductibilité protège le projet contre la compromission d’une infrastructure de build unique.
- ADR-013 (Distribution Modèle C → B) : pendant la phase d’incubation, les distributions Linux tierces re-construisent Air depuis les sources. La reproductibilité leur donne un moyen technique de vérifier qu’aucun écart n’a été introduit.
- ADR-015 (Gouvernance et DCO) : la transparence de gouvernance se prolonge naturellement par la transparence de la chaîne de build.
- Phase C de setup (
../setup/phase-C-infrastructure.md, section 3 « Builds reproductibles ») : l’objectif a été inscrit dès la définition de l’infrastructure, en attente du présent ADR pour formalisation.
L’instruction technique de cet ADR a tranché trois arbitrages structurants : périmètre d’artefacts en phase 0, calendrier d’application de la contrainte, et famille d’outillage retenue. Ces arbitrages sont consignés dans la section Décision ci-après. Les pratiques détaillées qui en découlent (toolchain, env vars, flags, vérification CI) sont consignées dans la section Mécanismes techniques.
Décision
Air vise la reproductibilité bit-pour-bit des artefacts Rust natifs produits par cargo build, dès le premier commit de la phase 0, par une approche minimaliste fondée sur la discipline et les capacités natives de Rust.
Cette décision se décompose en quatre points fermes :
-
Périmètre : la reproductibilité s’applique aux binaires et bibliothèques Rust produits par
cargo buildpour toutes les crates Air (couches 0 et 1 dans un premier temps, étendu mécaniquement aux couches supérieures à mesure de leur ajout au workspace). Les artefacts de plus haut niveau — bundles.airapp/.airservice(ADR-010), images OS, paquets distros tiers — font l’objet d’ADRs ultérieurs et ne sont pas couverts ici. -
Cible : un même tag Git, build sur n’importe laquelle des machines de référence d’ADR-014 (Raspberry Pi 4 ARM64, Mac Intel x86_64, runners cloud x86_64), produit des artefacts dont les hashes SHA-256 sont identiques pour une cible donnée. La reproductibilité est intra-cible (deux builds x86_64 produisent le même binaire x86_64), pas inter-cible.
-
Calendrier : la reproductibilité est bloquante en CI dès le premier commit de la phase 0. Toute PR qui casse la reproductibilité est rejetée. Rationale : Rust est en grande partie reproductible par défaut ; le coût d’introduction est faible à condition de l’imposer dès le départ. Reporter la contrainte serait accepter une dette technique rétroactive coûteuse.
-
Outillage : approche minimaliste —
rust-toolchain.tomlstrict,Cargo.lockcommitté,RUSTFLAGSwhitelisté,SOURCE_DATE_EPOCHdérivée du commit Git, build dans un container minimal Debian stable pinné par digest. Vérification par double-build et comparaison viadiffoscope. Aucun système de build alternatif (pas de Nix, pas de Bazel, pas derepro-envdédié). Cette approche est cohérente avec le Principe d’ingénierie 7 (verbosité au service de la clarté, pas de magie) et avec le Principe 6 (règle des 80 % sur les dépendances).
Mécanismes techniques
Toolchain pinnée
Un fichier rust-toolchain.toml à la racine du workspace fige la toolchain Rust de manière exacte :
[toolchain]
channel = "1.96.0"
components = ["rustfmt", "clippy", "llvm-tools-preview"]
targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"]
profile = "minimal"
Aucun usage de canaux flottants (stable, beta, nightly). Le channel est une version exacte. Toute mise à jour de toolchain passe par une PR dédiée qui (a) justifie la mise à jour, (b) exécute la suite de tests reproductibilité complète, (c) est explicitement approuvée par un mainteneur senior. La mise à jour ne peut pas être combinée à d’autres changements fonctionnels.
Dépendances pinnées
Le fichier Cargo.lock est committé à la racine du workspace. Il fige l’arbre transitif complet des dépendances à des versions exactes (incluant les hashes). Cohérent avec ADR-024 (workflow dépendances) qui acte le DEPENDENCIES.md par crate.
Dans les Cargo.toml, les dépendances utilisent des ranges semver explicites ("2.4", "2.4.0") sans wildcards ("*", ">=2.0" sans borne haute). Les ranges sont resserrés par le Cargo.lock lors du build effectif.
Container de build minimal
Le CI exécute les builds dans un container Debian stable dont l’image est référencée par son digest SHA-256 (ex. debian@sha256:abc123...). L’image contient uniquement : la toolchain pointée par rust-toolchain.toml (installée via rustup aux versions exactes), mold comme linker (version pinnée), et les utilitaires strictement nécessaires (git, pkg-config, clang si besoin pour build.rs FFI). Pas d’inclusion d’utilitaires « pratiques » qui pourraient introduire des variations.
Le digest de l’image est mis à jour par PR explicite, traitée comme une mise à jour de toolchain.
Variables d’environnement contrôlées
Le job CI démarre avec un environnement nettoyé. Une whitelist explicite définit les variables autorisées :
PATH(réduit à la toolchain et aux utilitaires du container).HOME,CARGO_HOME,RUSTUP_HOME(pointés vers des chemins déterministes dans le container).RUSTFLAGS(figé, voir ci-dessous).SOURCE_DATE_EPOCH(dérivée du timestamp du commit Git viagit log -1 --format=%ct).CARGO_NET_OFFLINE=trueaprès l’étape de récupération des dépendances, pour interdire tout fetch implicite.
Toute autre variable est explicitement unset. Le script CI vérifie cette whitelist avant de lancer cargo build.
Flags de compilation
RUSTFLAGS est figé dans le CI et dans .cargo/config.toml :
[build]
rustflags = [
"-D", "warnings",
"--remap-path-prefix", "/build=.",
"-C", "codegen-units=1",
"-C", "link-arg=-Wl,--build-id=none",
]
Justifications :
--remap-path-prefixneutralise les chemins absolus du workspace de build dans les binaires (sinon le chemin du runner CI s’incruste dans les symboles).codegen-units=1garantit un ordre déterministe de génération de code en LLVM, au prix d’un build légèrement plus lent. Acceptable.--build-id=noneneutralise l’ID de build généré par le linker (sinon il varie à chaque build par construction).
Profils release du Cargo.toml figés : pas de incremental = true, pas de variation de opt-level par machine.
Linker reproductible
Linker imposé : mold (version pinnée via le container). Justification : mold est plus déterministe que ld.bfd ou gold sur les détails de layout, et il est suffisamment mature pour Air. Cohérent avec Principe 6 (utilisation large justifiée).
Symboles debug : produits dans des fichiers compagnons .debug séparés via objcopy --only-keep-debug. Le binaire principal référence le fichier debug par chemin relatif (pas absolu). Les .debug sont reproductibles également.
Inputs déterministes dans le code
Discipline imposée par convention et vérifiée par revue de code :
build.rsne lit pas l’environnement non-déterministe : pas d’inclusion de date courante, hostname, UID, état machine. Si unbuild.rsgénère du code, il le génère à partir de fichiers committés dans le workspace, jamais depuis l’environnement courant.- Code généré (
include_str!, macros procédurales, codegen) : ordres déterministes garantis. En particulier, les itérations surHashMap/HashSetsont remplacées parBTreeMap/BTreeSetquand l’output dépend de l’ordre. - Inputs filesystem :
globet listings de répertoire triés alphabétiquement avant traitement.
Ces règles sont consignées dans STYLE_GUIDE.md (à produire, cf. backlog) avec exemples et anti-patterns.
Vérification en CI
Pipeline pr-quick
Pas de test de reproductibilité (priorité au feedback rapide). Validation des prérequis seulement : Cargo.lock à jour, rust-toolchain.toml non modifié hors PR dédiée.
Pipeline pr-full
Double cargo build --release consécutif sur la même machine, comparaison via diffoscope. Diff non vide → CI rouge, PR bloquée. Rapport diffoscope archivé comme artefact CI pour diagnostic.
Outil dédié : script air-repro-check (à produire au démarrage du projet) qui orchestre le double-build, neutralise les caches entre les deux passes, et invoque diffoscope avec format de rapport humain-lisible.
Pipeline pr-hardware
Build sur Pi 4 ARM64 et sur Mac Intel x86_64, comparaison intra-cible avec le build cloud x86_64. Vérifie la reproductibilité cross-machine pour chaque triplet cible. Obligatoire avant merge sur les PRs touchant la couche 0 ou les configurations de build.
Pipeline main-nightly
Triple build sur chacune des machines de référence (cf. ADR-014). Comparaison croisée complète. Hash SHA-256 des artefacts publié dans un journal auditable.
Outillage de vérification
diffoscope est l’outil de référence pour analyser les différences entre deux artefacts binaires. Largement utilisé par Debian, NixOS et le projet Reproducible Builds. Couvre les binaires ELF, les archives .rlib, la documentation HTML produite par cargo doc. Couverture API estimée >80 % au sens du Principe 6, donc dépendance recevable et inscrite dans DEPENDENCIES.md au moment de son introduction.
Articulation avec les ADRs existants
- ADR-010 (
.airapp/.airservice, entitlements signés) : la reproductibilité Air assure que les binaires inclus dans les bundles sont auditables avant signature. La signature des bundles vient au-dessus de cette base. Un ADR ultérieur traitera spécifiquement de la reproductibilité du processus de bundle (manifeste, layout, signature détachée). - ADR-013 (Distribution Modèle C → B) : la reproductibilité donne aux distributions tierces (Modèle C) un moyen technique de vérifier que leur build local correspond à l’upstream Air. Précieux pour la supply chain security et pour la confiance mutuelle pendant la phase d’incubation.
- ADR-014 (catalogue de matériel) : les machines de référence servent à la fois aux tests fonctionnels et à la validation de reproductibilité cross-machine. La diversité du catalogue (Pi 4, Mac Intel) est un atout : elle révèle des sources de non-déterminisme qu’un parc homogène masquerait.
- ADR-015 (Licence, DCO, gouvernance) : reproductibilité comme propriété auditable du projet, prolongement naturel de la transparence de gouvernance.
- ADR-024 (workflow de dépendances) :
Cargo.lockcommitté etDEPENDENCIES.mddétaillé sont des prérequis pour la reproductibilité. ADR-024 et ADR-025 se renforcent mutuellement. - Principe d’ingénierie 1 (test exhaustif des fondations) : la reproductibilité est un test transverse appliqué à tout commit ; cohérent avec l’esprit du Principe 1 même s’il n’est pas exprimé en termes de coverage.
- Principe d’ingénierie 6 (règle des 80 %) : l’approche minimaliste retenue (pas de système de build alternatif) découle directement de ce principe.
diffoscopeest la seule dépendance d’outillage spécifique introduite, et elle dépasse largement le seuil d’utilisation. - Principe d’ingénierie 7 (verbosité au service de la clarté) : la stratégie est lisible et auditable. Toutes les variables, flags, et étapes sont explicites dans des fichiers committés (
rust-toolchain.toml,.cargo/config.toml, scripts CI), pas dans des configurations cachées.
Conséquences
Bénéfices
- Confiance technique vérifiable. Un utilisateur, chercheur en sécurité, ou distributeur peut re-construire Air depuis les sources et vérifier que son binaire correspond exactement à l’upstream. Cette propriété n’est pas une déclaration, elle est un test reproductible.
- Supply chain security. Un attaquant qui distribuerait un binaire « légèrement modifié » de
libair-base.soserait détecté par toute partie qui vérifie le hash. La barrière à l’attaque est élevée. - Détection précoce de sources de non-déterminisme. Beaucoup de bugs liés à l’ordre d’itération sur
HashMap, aux conditions de course de codegen, ou aux interactions avec l’environnement, deviennent visibles dans l’écart entre deux builds. La reproductibilité bit-pour-bit force la chasse à ces sources, qui sont par ailleurs des sources de bugs subtils en production. - Préparation des couches supérieures. La reproductibilité des binaires Rust est la base sur laquelle s’appuieront, en phase 4+, la reproductibilité des bundles
.airappet celle des images OS.
Coûts
- Discipline supplémentaire pour les contributeurs. Pas de variables d’environnement libres dans le CI, pas de date/hostname/UID dans
build.rs, pas d’HashMapdans le code généré. Cette discipline est formalisée dans leSTYLE_GUIDE.mdà produire. - Investissement initial en outillage. Script
air-repro-check, configuration CI multi-pipeline, container minimal pinné. Le coût est concentré au démarrage du projet et amorti par la durée. - Procédure formalisée pour les mises à jour de toolchain. Une mise à jour de Rust ne peut plus être un changement opportuniste — elle est traitée comme une migration mineure d’infrastructure.
Risques et mitigations
- Risque : une dépendance externe introduit du non-déterminisme (rare en Rust, mais possible). Mitigation : analyse au moment de l’ajout (workflow ADR-024) ; détection en CI ; entrée d’exception dans
EXCEPTIONS.mdsi la dépendance est indispensable et le non-déterminisme négocié à un niveau cosmétique. - Risque : une mise à jour de toolchain Rust casse la reproductibilité existante (changements LLVM, codegen, etc.). Mitigation : la PR de mise à jour exécute le test de reproductibilité ; si elle échoue, l’écart est analysé et la mise à jour est différée ou accompagnée d’adaptations.
- Risque : différences subtiles inter-machines (Pi 4 vs Mac Intel) que la cible « intra-cible » ne couvre pas. Mitigation : le pipeline
pr-hardwaredétecte tôt ;diffoscopepermet le diagnostic ; le projet Reproducible Builds publie un catalogue de problèmes connus et leurs résolutions.
Statut futur
ADR immuable dans ses principes (périmètre, calendrier, approche outillage minimaliste). Les détails opérationnels (version exacte de rust-toolchain.toml, digest de l’image Debian, contenu de RUSTFLAGS) sont documentés dans INFRASTRUCTURE.md à la racine du workspace (à produire) et peuvent évoluer par convention sans amendement de l’ADR, tant que les principes de l’ADR sont préservés.
ADRs ultérieurs identifiés :
- Reproductibilité des bundles
.airapp/.airservice(instruction conjointe avec la chaîne de signature complète). - Reproductibilité des images OS Air (à instruire au moment de la définition de l’image OS).
- Politique d’archivage long terme des artefacts reproductibles auditables.
Licence du document : MPL 2.0 Statut : Document fondateur immuable.