Review follow-ups of PR #415. The container does have python3 (pulled by the apt packages of the install step, run 517): awk is kept because mawk is part of the ubuntu:22.04 base image, which the workflow comment and the ADR now say. #344 is a PR and the second drift, not the third. The awk exit-code test is described by what it alone catches (an awk that fails after printing every fact). The repair procedure gains the two failures it had no answer for, and the ADR names the canary's limit. The release commit edits Cargo.lock on main without a PR, so /release now replays the guard's exact step text right after that edit (step 2), checked on the stack's lock (exit 0) and on a drifted edge (exit 1). Refs #345 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
17 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), PR #344 (deuxième dérive, contournée en n'appliquant que la strophe
h2à la main), #348 (mesurerustls: 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 |
| PR #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 dans la PR #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 dans la PR #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 et applique quatre règles. awk (mawk) fait partie de l'image de base ubuntu:22.04, alors que python3 n'est présent dans ce conteneur que comme dépendance transitive des paquets apt installés à l'étape précédente (journal du run 517), ce que rien ne garantit :
- Chaque arête des sept crates. Pour
colored,dirs-sys,errno,rustix,rustls-platform-verifier,tempfileetwinapi-util, chaque bloc[[package]]de ce nom doit porter au moins une arêtewindows-sys, et chacune doit nommer0.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é. - 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 paquetwindows-sys, en0.61.2. - Ensemble des versions. Les versions de
windows-sysdu 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. - 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 sortie vide échoue déjà (ensemble des versions, blocs introuvables, canari) ; ce test attrape seul un awk qui échoue après avoir tout imprimé, et sans lui le garde sortirait en 0 (éprouvé à la revue de la PR #415). 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 uncargo update(-p hyper,-p h2et-p rustls-webpki -p tarl'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 la PR #344 l'a fait pour
h2; - ou garder la sortie de
cargo updateet 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 contremainse réduit aux stropheshyperetwant,cargo metadata --lockedl'accepte sans modifier le fichier, et le garde repasse. - n'appliquer à la main que la ou les strophes du paquet visé (version, checksum, dépendances), comme la PR #344 l'a fait pour
-
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,EXPECTEDchange 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
CRATESaprès avoir revérifié la liste des bornes larges (commande de la section Conséquences). -
Plus d'arête
windows-sysdans un bloc gardé (has no windows-sys edge left) : le crate a cessé de dépendre dewindows-sysdans cette version. Même traitement qu'un bloc introuvable ; si c'esttempfile, choisir aussi un autre canari parmi les crates restants. -
Un second bloc d'un crate gardé dont la borne exclut
0.61.2(par exemple untempfileplus ancien, d'avant les bornes larges, tiré par une autre dépendance) : la règle 1 échoue, et la réparation de la première puce est impossible, puisquecargo metadata --lockedrefuse une arête hors borne (code 101, éprouvé à la revue de la PR #415). Décider dans la PR qui change le lock : admettre ce bloc par nom et version dans le script, ou écarter la dépendance qui le tire ; dans les deux cas, amender cet ADR. -
Canari ou
awken é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-sys0.62. La borne<=0.61decoloredetwinapi-util, comme le<0.62d'errno,rustix,tempfileetrustls-platform-verifier, les empêche de suivre ; seuldirs-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 runrelease.yml.PIN,EXPECTEDet au besoinCRATESchangent 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-msvcdepuis 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 checka besoin du toolchain natif de la cible dès qu'un build script compile du C, etcc-rss'arrête surfailed to find tool "lib.exe". Il faudrait installer sur chaque PR Rust le toolchain croisé querelease.ymln'installe qu'au tag (cargo-xwincompilé 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-sysdans 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 demandecargo 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.tomln'est pas ce qu'on lit avant de lancercargo 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-rustsur 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. À l'inverse, retirer de
CRATESun autre nom que le canari passe, avec une vérification de moins dans le bilan (9 → 8) : seule la relecture du diff, sous le commentaire qui renvoie ici, l'attrape. La commande suivante liste les exigences surwindows-sysqui ne sont pas un^0.x; le 2026-10-10, elle sort exactement les sept crates deCRATES: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-msvcaujourd'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érificationcargo tree. -
Le garde tourne sur les PR qui touchent
src-tauri/**,.cargo/**ou le workflow, et le lundi surmain, pas en continu. Une exception : le commit de/releasemodifieCargo.lockdirectement surmain, sans PR, et le garde ne le verrait qu'au lundi suivant, après le build du tag. Le skill/releasele rejoue donc en local juste après avoir édité le lock, avant de tagger (étape 2).
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.locksans 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), #348 (mesure
rustls), #310 (première dérive) ; PR #344 (stropheh2appliquée à la main), PR #321 (arêtes remontées à0.61.2)