erplibre/script/todo/dev_tools.py
Mathieu Benoit 01c5290e30 [FIX] qemu install : poser mise, pyenv et starship, verdict par outil
Without pipefail, « curl … | sh » returns the status of sh, 0 on empty input:
a failed download passed for an install, its fallback never ran, and the
failure surfaced later as « command not found ». mise and pyenv are downloaded
into a mktemp file, then run, and a check refuses any fallback behind a pipe
without pipefail. starship's installer runs as root under a root timeout, never
reaching the « sudo -v » that sudo-rs refuses, and its shell hook is guarded.
Each tool then reports the version found or « not installed », without
tripping « set -e ».

--- FR ---

Sans pipefail, « curl … | sh » rend le statut de sh, 0 sur une entrée vide :
un téléchargement raté passait pour une pose, son repli ne tournait jamais, et
l'échec se lisait plus loin sous « command not found ». mise et pyenv sont
téléchargés dans un fichier tiré par mktemp, puis exécutés, et un contrôle
refuse tout repli derrière un tube sans pipefail. L'installateur de starship
tourne en root sous un délai root, sans atteindre le « sudo -v » que sudo-rs
refuse, et son crochet de shell est gardé. Chaque outil dit ensuite la version
trouvée ou « non installé », sans faire tomber « set -e ».

Assisted-by: Claude Opus 5
2026-09-14 16:46:15 -04:00

108 lines
5 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Installateurs amont des outils de développement.
Les mêmes outils se posent à DEUX endroits : sur l'hôte par le menu Shell,
et dans une VM par le déploiement QEMU. Une seule table les décrit, parce
que deux copies d'une URL dérivent dès que l'amont en change une, et que la
seconde copie est celle qu'on oublie.
Chacun est un installateur amont plutôt qu'un paquet : aucun n'est présent
dans les dépôts des quatre familles que supporte ERPLibre, et celui qui l'est
y traîne d'une version.
"""
# Proxy CLI qui réduit la consommation de tokens des assistants.
RTK_UPSTREAM = (
"curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/"
"refs/heads/master/install.sh | sh"
)
# L'installateur amont pose un binaire statique. Il sert de recours parce
# que le paquet manque d'une partie des dépôts des plateformes supportées.
STARSHIP_URL = "https://starship.rs/install.sh"
# « -f » n'est pas un ornement quand la sortie part dans un shell : sans lui,
# curl écrit le CORPS d'une réponse d'erreur et sort avec 0, si bien qu'une
# page HTML de 34 ko arrive sur l'entrée de « sh » et s'y interprète comme
# un script. Le diagnostic obtenu est alors « Syntax error » sur une ligne du
# HTML, qui ne dit ni que l'URL est fautive ni que rien n'a été installé.
# Avec « -f », curl n'écrit rien et rend 22. « -L » suit une redirection,
# qu'un projet amont pose sans prévenir.
STARSHIP_UPSTREAM = f"curl -fsSL {STARSHIP_URL} | sh"
# Sans terminal — une pose par SSH dans une VM — l'installateur demande une
# confirmation que personne ne donnera. « -y » la donne d'avance.
#
# Et il tourne en root. Sans droit d'écriture sur /usr/local/bin, son
# répertoire par défaut, l'installateur appelle « sudo -v » AVANT de
# télécharger quoi que ce soit. Or « sudo -v » exige un mot de passe dès
# qu'une seule règle sudoers qui vise le compte n'est pas NOPASSWD — celle
# du groupe d'administration ne l'est pas —, et sudo-rs ignore « verifypw »,
# le réglage qui l'en dispenserait. En root le répertoire est inscriptible :
# aucun « sudo -v » n'est atteint, et le binaire atterrit dans un répertoire
# que porte le PATH de tout compte, SSH non interactif compris.
#
# La borne de temps passe DERRIÈRE sudo. Un « timeout » lancé sans privilège
# ne peut pas tuer un installateur root : son signal au groupe de processus
# échoue en EPERM sur les processus root, et sudo ne relaie pas un signal
# venu de son propre groupe. Un « timeout » root, lui, tue l'installateur et
# ses enfants. Il reste en deçà de la borne extérieure de la pose, qui ne
# couvre plus que la moitié non privilégiée du tube : curl.
#
# « -k » : SIGTERM se laisse ignorer, et « timeout » seul attendrait alors
# sans fin. Passé ce délai, il envoie SIGKILL, qui ne s'ignore pas. Borne et
# délai additionnés restent sous la borne extérieure.
STARSHIP_ROOT_TIMEOUT = 280
STARSHIP_ROOT_KILL_AFTER = 10
STARSHIP_UPSTREAM_VM = (
f"curl -fsSL {STARSHIP_URL}"
f" | sudo timeout -k {STARSHIP_ROOT_KILL_AFTER}"
f" {STARSHIP_ROOT_TIMEOUT} sh -s -- -y"
)
# Ce que chaque shell écrit pour lancer starship. La ligne va en FIN de
# fichier : starship compose le prompt et doit passer après tout ce qui y
# touche.
#
# bash et zsh la gardent par « command -v » : sans binaire — une pose qui a
# échoué, un binaire retiré depuis —, « eval "$(starship init …)" » écrirait
# « command not found » à l'ouverture de chaque shell. La garde est DANS la
# ligne, et non autour de son écriture : elle vaut aussi pour le binaire qui
# disparaît après coup. « starship init » y reste en clair, parce que c'est
# le motif qui dédoublonne, dans le menu de l'hôte comme dans la VM.
#
# Un « if » et non un « && » : la ligne est la DERNIÈRE du fichier, et le
# fichier rend son statut. « a && b » rend 1 quand starship manque, si bien
# qu'un script sous « set -e » qui lit ce fichier s'arrêterait là ; un « if »
# sans branche prise rend 0.
STARSHIP_LINE = {
"bash": (
"if command -v starship >/dev/null 2>&1; then"
' eval "$(starship init bash)"; fi'
),
"zsh": (
"if command -v starship >/dev/null 2>&1; then"
' eval "$(starship init zsh)"; fi'
),
"fish": "starship init fish | source",
}
# Les assistants posés par un installateur amont : le nom du binaire mène
# à (commande, répertoire d'installation). Le répertoire sert à garantir
# le PATH — un binaire posé hors des chemins du shell reste introuvable.
AGENTS = {
"claude": (
"curl -fsSL https://claude.ai/install.sh | bash",
"~/.local/bin",
),
"opencode": (
"curl -fsSL https://opencode.ai/install | bash",
"~/.opencode/bin",
),
}
# L'agent posé quand rien n'est choisi. Le premier de la table ferait
# dépendre le défaut de l'ordre d'écriture d'un dictionnaire.
AGENT_DEFAUT = "claude"