Simpl-Resultat/docs/adr/0023-windows-sys-pinning.md
le king fu 3b8105c253
All checks were successful
PR Check — Rust / rust (pull_request) Successful in 3m36s
docs: fix two sentences of ADR 0023 after self-review
"Ce sous-arbre" lost its referent once the trigger paragraph stopped
naming a subtree: the sentence now says what is measured, that both past
drifts (#310, #344) came with RustSec advisory fixes. Rule 1's sentence on
a missing block is reworded.

Refs #345

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:17:09 -04:00

15 KiB

ADR 0023 — Épinglage de windows-sys 0.61.2 : garde-fou CI sur la version résolue, condition de sortie écrite

  • Status: Accepted
  • Date: 2026-10-10
  • Issues: #345 (cette décision), #344 (troisième dérive, contournée en n'appliquant que la strophe h2 à la main), #348 (mesure rustls : la couche TLS en général ne déclenche pas la dérive)
  • S'appuie sur l'ADR 0018 : même motif de garde-fou (chaque vérification loggée, canari, code de sortie de l'outil testé à part de sa sortie)

Contexte

Sept crates du fichier de verrouillage déclarent windows-sys par une borne qui couvre plusieurs mineures :

crate exigence déclarée sur windows-sys
colored 3.1.1, winapi-util 0.1.11 >=0.48, <=0.61
errno 0.3.14, rustix 1.1.3, tempfile 3.24.0 >=0.52, <0.62
rustls-platform-verifier 0.6.2 >=0.52.0, <0.62.0
dirs-sys 0.5.0 >=0.59.0

Ce sont les seuls du graphe dans ce cas (relevé par cargo metadata le 2026-10-10). Toutes les autres déclarations sont des ^0.x, qui n'admettent qu'une mineure, donc une seule des versions présentes : elles ne peuvent pas dériver. Le lock contient six versions de windows-sys, toutes cibles confondues (0.45.0, 0.48.0, 0.52.0, 0.59.0, 0.60.2, 0.61.2), et les sept crates peuvent pointer n'importe laquelle de celles que leur borne admet, sans ajouter de paquet au lock et sans violer aucun contrat.

Les sept pointent 0.61.2. C'est l'état du tag v0.14.0 (2026-07-18), dont la release (run 322) a traversé un vrai build Windows NSIS, et celui de chaque release depuis : v0.15.0 (run 402), v0.16.0 (run 451), v0.17.0 (run 491), v0.17.1 (run 508), vérifié le 2026-10-10 en rejouant le garde ci-dessous sur le lock de chaque tag. L'état dérivé n'a traversé aucun build Windows : aucune release n'a eu lieu pendant la seule fenêtre où main l'a porté, de #310 (2026-07-27) à la PR #321 (2026-08-15).

La contrainte n'était tenue par rien

#310 (2026-07-27) cargo update -p rustls-webpki -p tar fait descendre les sept arêtes ; vu, et jugé inoffensif parce que sans effet sur Linux
PR #321 (2026-08-15) la revue les remonte à 0.61.2, crate par crate contre le tag v0.14.0
#344 (2026-09-04) cargo update -p h2 recommence ; contourné en n'appliquant que la strophe h2 à la main
#348 (2026-09-21) cargo update -p rustls --precise 0.23.45 ne bouge rien, mais il a fallu remesurer pour le savoir

Hors du lock, la contrainte ne vivait que dans de la prose (STATE.md, des messages de commit) : la prochaine mise à jour la régresserait, et il faudrait qu'un relecteur reconnaisse sept noms de crates pour le voir. C'est le mode d'échec que l'ADR 0018 traite pour les suppressions d'advisories : une décision qui ne vit que dans de la prose n'est pas une décision tenue.

Le déclencheur, mesuré

commande, sur une copie du lock effet sur les sept arêtes mesure
cargo update -p anyhow aucun 2026-09-04
cargo update -p rustls --precise 0.23.45 aucun ; le diff de toutes les lignes windows-sys du lock est vide 2026-09-21 (#348)
cargo update -p hyper les sept descendent : colored et winapi-util vers 0.48.0, errno, rustix, tempfile et rustls-platform-verifier vers 0.52.0, dirs-sys vers 0.59.0 2026-09-04, refaite le 2026-10-10 (hyper 1.8.1 → 1.12.0)

Dans cette mesure, chaque arête est descendue vers la plus basse version déjà présente que sa borne admet, et la dérive de #310 (commit f4b09b0) a la même répartition : deux arêtes vers 0.48.0, quatre vers 0.52.0, une vers 0.59.0. hyper et ce qu'il tire (h2 en #344) la déclenchent, la couche TLS en général non : rustls, en son cœur, ne déplace rien. Mais rustls-webpki et tar l'ont déclenchée en #310. Aucune règle simple ne dit quelle mise à jour la provoque, et la dernière qu'on a formulée (« tout bump du sous-arbre réseau/TLS ») a été démentie par #348. C'est donc la version résolue qu'il faut garder, pas une liste de déclencheurs.

Et cargo n'en dit rien. Le 2026-10-10, cargo update -p hyper annonce :

     Locking 2 packages to latest compatible versions
    Updating hyper v1.8.1 -> v1.12.0
    Updating want v0.3.1 -> v0.3.2

pendant qu'il réécrit aussi les sept arêtes. L'ensemble des paquets ne bouge pas (666 avant et après) ; seules les arêtes changent, et cargo ne rapporte que les paquets. Or les deux dérives passées sont venues avec des correctifs d'advisories RustSec (rustls-webpki et tar en #310, h2 en #344) : la dérive arrive avec les changements les plus pressés et les moins relus.

Ce que coûte la dérive

Pas un risque de correction : les deux états sont dans les bornes déclarées par les sept auteurs. Le coût est de taille et de temps de compilation, et il tombe sur le build Windows, que personne ne mesurait :

état versions de windows-sys compilées pour x86_64-pc-windows-msvc
épinglé (0.61.2) 3 : 0.59.0, 0.60.2, 0.61.2
dérivé 5 : les mêmes, plus 0.48.0 et 0.52.0

Mesuré par cargo tree --target x86_64-pc-windows-msvc -e normal,build, refait le 2026-10-10 sur le lock de main et sur sa copie dérivée par cargo update -p hyper. Deux crates windows-sys complets de plus dans chaque build Windows, ajoutés en effet de bord, dans une configuration qu'aucun build Windows n'a traversée.

Décision

Un garde-fou CI bloquant sur la version résolue, et cet ADR pour en porter la raison et la sortie.

L'étape Verify windows-sys pinning de check-rust.yml, placée après cargo check à côté du garde des suppressions, lit src-tauri/Cargo.lock avec awk (le conteneur ubuntu:22.04 n'a pas Python) et applique quatre règles :

  1. Chaque arête des sept crates. Pour colored, dirs-sys, errno, rustix, rustls-platform-verifier, tempfile et winapi-util, chaque bloc [[package]] de ce nom doit porter au moins une arête windows-sys, et chacune doit nommer 0.61.2. L'absence de bloc pour l'un de ces noms fait échouer l'étape (crate renommé ou sorti du graphe). Un suffixe de source sur une arête ( (registry+…)) est toléré.
  2. Arête nue. Cargo n'écrit "windows-sys 0.61.2" que tant que plusieurs versions coexistent ; quand il n'en reste qu'une, il écrit "windows-sys". Une arête nue ne passe que si le lock ne contient qu'un seul paquet windows-sys, en 0.61.2.
  3. Ensemble des versions. Les versions de windows-sys du lock, toutes cibles confondues et doublons compris, doivent égaler exactement {0.45.0, 0.48.0, 0.52.0, 0.59.0, 0.60.2, 0.61.2} (état du 2026-10-10). Une version qui apparaît ou disparaît fait échouer l'étape avec un renvoi à cet ADR : c'est soit le coût réel (une version de plus), soit le début de la sortie, et dans les deux cas une décision.
  4. Canari. La boucle doit avoir lu et vérifié une arête de tempfile, crate connu pour en porter une, faute de quoi son silence ne prouve rien et l'étape échoue.

Comme dans l'ADR 0018, chaque vérification est loggée, succès compris (ok: rustix 1.1.3 -> windows-sys 0.61.2), puis un bilan (windows-sys pinning: 9 checks, exit 0), et le code de sortie d'awk est testé à part de sa sortie : une panne de lecture fait échouer l'étape au lieu de passer pour un lock propre. Le garde n'appelle pas cargo, donc ni l'index chaud ni --locked ne le concernent ; il lit le lock tel que cargo check l'a laissé.

La liste gardée, la version épinglée et l'ensemble attendu sont en tête du script, sous un commentaire qui renvoie ici. On ne les modifie que sur la preuve que demandent les sections suivantes, jamais pour faire passer l'étape au vert.

Friction assumée : chaque mise à jour qui déplace ces arêtes (celles qui atteignent hyper, entre autres) fera échouer l'étape et demandera une édition à la main du lock. C'est l'objet : un effet de bord silencieux devient une décision explicite.

Éprouvé par mutation

Le 2026-10-10, le texte du run: a été extrait du workflow et lancé tel quel, avec les options du runner (bash --noprofile --norc -e -o pipefail), sur des copies du lock :

lock sortie
celui de main, et ceux des tags v0.14.0 à v0.17.1 exit 0, 9 vérifications
dérivé par cargo update -p hyper exit 1, les sept arêtes en échec
un bloc windows-sys 0.53.0 ajouté exit 1, ensemble des versions
colored renommé exit 1, bloc introuvable
absent (awk sort en 2) exit 1, 0 vérification
tempfile retiré de la liste gardée, ou liste vide (script modifié) exit 1, canari

Procédure quand le garde échoue

La ligne FAIL: dit quelle règle a cédé.

  • Une arête a quitté 0.61.2 (règle 1), en général après un cargo update (-p hyper, -p h2 et -p rustls-webpki -p tar l'ont fait). Garder la mise à jour voulue et remettre les arêtes, de l'une de deux façons qui mènent au même lock :

    • n'appliquer à la main que la ou les strophes du paquet visé (version, checksum, dépendances), comme #344 l'a fait pour h2 ;
    • ou garder la sortie de cargo update et ramener à la main les arêtes signalées à "windows-sys 0.61.2", les seules lignes à toucher.

    Puis vérifier que cargo accepte le lock tel quel, sans le réécrire :

    cargo metadata --manifest-path src-tauri/Cargo.toml --format-version 1 --locked > /dev/null
    

    Éprouvé le 2026-10-10 sur la copie dérivée par cargo update -p hyper : les sept arêtes remises, le diff contre main se réduit aux strophes hyper et want, cargo metadata --locked l'accepte sans modifier le fichier, et le garde repasse.

  • L'ensemble des versions a changé (règle 3). Pour une version ajoutée, chercher qui la tire et si elle est compilée pour Windows (cargo tree --manifest-path src-tauri/Cargo.toml -i windows-sys@<version> --target x86_64-pc-windows-msvc) ; si c'est le début du passage à 0.62, la condition de sortie est atteinte (section suivante). Une version retirée est un pas vers la convergence. Dans les deux cas, EXPECTED change dans la PR qui change le lock, preuve à l'appui dans la PR.

  • Bloc introuvable : le crate a été renommé ou est sorti du graphe. Retirer son nom de CRATES après avoir revérifié la liste des bornes larges (commande de la section Conséquences).

  • Canari ou awk en échec : le garde lui-même est cassé. Le réparer ; ne jamais changer le canari ou la liste pour le faire taire.

Condition de sortie

L'épinglage n'est pas fait pour durer indéfiniment. Il change ou disparaît dans deux cas.

  • Au passage de l'écosystème à windows-sys 0.62. La borne <=0.61 de colored et winapi-util, comme le <0.62 d'errno, rustix, tempfile et rustls-platform-verifier, les empêche de suivre ; seul dirs-sys (>=0.59.0, sans plafond) le peut. Quand un crate du graphe tirera 0.62, la règle 3 rougira et il faudra décider sur preuve : une nouvelle configuration n'est tenue qu'après un vrai build Windows, c'est-à-dire un run release.yml. PIN, EXPECTED et au besoin CRATES changent alors dans la même PR que le lock, avec un amendement de cet ADR. Le garde est mis à jour, jamais contourné.
  • À la convergence vers un seul windows-sys, la sortie souhaitée. Plus aucune arête n'a où dériver, et le coût mesuré disparaît. Le garde le signalera (règle 3 en échec, arêtes nues admises par la règle 2) ; l'étape peut alors être retirée, avec un amendement de cet ADR.

Alternatives considérées

  • Check croisé Windows en CI (cargo check --target x86_64-pc-windows-msvc depuis Linux). L'option la plus attirante, puisqu'elle prouverait que le graphe Windows compile quel que soit l'état. Testée à l'analyse de #345, elle échoue : cargo check a besoin du toolchain natif de la cible dès qu'un build script compile du C, et cc-rs s'arrête sur failed to find tool "lib.exe". Il faudrait installer sur chaque PR Rust le toolchain croisé que release.yml n'installe qu'au tag (cargo-xwin compilé depuis les sources, clang, lld), sur un runner à capacité 1 : un chantier, pas une étape bon marché. À rouvrir si le besoin d'une preuve Windows revient pour d'autres raisons.
  • Garde sur la propriété (« au plus 3 windows-sys dans le graphe Windows »). Il encode le dommage mesuré plutôt qu'un numéro, survit au passage à 0.62 et couvrirait un nouveau crate à borne large. Mais il reste indirect à la lecture, tolère un échange à compte constant, et demande cargo tree : le lock ne dit pas quelle arête est compilée pour quelle cible. Écarté au profit de la cohérence avec le motif existant.
  • Garde sur une liste de crates déclencheurs (hyper, la couche TLS). La liste est précisément ce que #348 a trouvé faux.
  • Commentaire seul dans Cargo.toml. Cargo.toml n'est pas ce qu'on lit avant de lancer cargo update, et rien ne l'exécute.

Conséquences

Positives

  • La dérive ne passe plus en silence : une mise à jour qui déplace une arête rougit check-rust sur la PR même, avec le nom de chaque arête déplacée, au lieu d'attendre qu'un relecteur reconnaisse sept noms de crates dans un diff que cargo n'a pas annoncé.
  • La contrainte est domiciliée : qui voit l'étape rougir trouve ici pourquoi, et quoi faire.
  • Le motif est celui que le dépôt connaît déjà (ADR 0018), pièges compris.

Négatives

  • Friction : chaque mise à jour qui déplace ces arêtes demande une édition à la main du lock.

  • La liste des sept est tenue à la main. Un crate à borne large qui entrerait dans le graphe ne serait pas gardé, et s'il pointait une version déjà présente, la règle 3 ne le verrait pas non plus. La commande suivante liste les exigences sur windows-sys qui ne sont pas un ^0.x ; le 2026-10-10, elle sort exactement les sept crates de CRATES :

    cargo metadata --manifest-path src-tauri/Cargo.toml --format-version 1 --locked \
      | python3 -c 'import json, sys; [print(p["name"], p["version"], d["req"]) for p in json.load(sys.stdin)["packages"] for d in p["dependencies"] if d["name"] == "windows-sys"]' \
      | grep -v ' \^0\.'
    
  • La règle 3 compte toutes les cibles : 0.45.0, 0.48.0 et 0.52.0 ne sont pas compilées pour x86_64-pc-windows-msvc aujourd'hui, et un changement qui ne touche qu'elles la fera rougir quand même. Assumé : le lock ne dit pas ce qui est compilé pour quelle cible, et une fausse alerte se lève en une vérification cargo tree.

  • Le garde ne tourne que sur les PR qui touchent src-tauri/**, .cargo/** ou le workflow, et le lundi sur main : exactement là où le lock change, mais pas en continu.

Neutre

  • Le garde lit le lock, il ne prouve pas que le graphe Windows compile. Cette preuve reste le run release.yml.
  • Rien ne change pour l'application : #345 lit Cargo.lock sans le modifier, et ne déplace aucune dépendance.

Liens

  • .forgejo/workflows/check-rust.yml — étape « Verify windows-sys pinning »
  • ADR 0018 — le garde-fou des suppressions, dont celui-ci reprend le motif
  • Issues #345 (cette décision), #344 (strophe h2 appliquée à la main), #348 (mesure rustls), #310 (première dérive) ; PR #321 (arêtes remontées à 0.61.2)