[ADD] script todo : installer les outils manquants, les quatre familles
La réduction sûre listait les outils manquants et s'arrêtait là. Trois
écritures séparées savaient installer, chacune un sous-ensemble
différent : openSUSE posait virt-viewer, mais ni navigateur CLI ni
lm-sensors.
Le binaire n'est presque jamais le paquet : sgdisk vit dans « gdisk »
chez Debian et Fedora, dans « gptfdisk » chez Arch et openSUSE. Deux
règles passent aussi dans la composante : l'ID de la distribution décide
avant le PATH, et la commande s'affiche avant la question.
Vérifié : commandes identiques sur les trois familles déjà couvertes,
4139 tests verts.
--- EN ---
The safe shrink listed the missing tools and stopped there. Three
separate writings knew how to install, each covering a different
subset: openSUSE could put virt-viewer down, but neither a CLI browser
nor lm-sensors.
A binary is almost never the package: sgdisk lives in "gdisk" on Debian
and Fedora, in "gptfdisk" on Arch and openSUSE. Two rules move into the
component as well: the distribution ID decides before the PATH, and the
command is shown before the question.
Checked: commands identical on the three families already covered, and
4139 tests green.
Assisted-by: Claude Opus 5
2026-08-31 07:18:13 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Poser un paquet, une seule fois pour tout le CLI.
|
|
|
|
|
|
|
|
|
|
Quatre familles de gestionnaires, et trois choses qui changent de l'une à
|
|
|
|
|
l'autre : la COMMANDE, le NOM du paquet, et le BINAIRE qu'il pose. Chaque
|
|
|
|
|
appelant qui refaisait ce travail chez lui en couvrait un sous-ensemble
|
|
|
|
|
différent — d'où un CLI où l'on pouvait installer virt-viewer sous openSUSE
|
|
|
|
|
mais pas un navigateur ni lm-sensors.
|
|
|
|
|
|
|
|
|
|
Deux règles que la composante tient à la place des appelants :
|
|
|
|
|
|
|
|
|
|
- l'ID de /etc/os-release décide AVANT le PATH. Une machine peut porter
|
|
|
|
|
plusieurs gestionnaires — un poste Debian avec dnf installé pour construire
|
|
|
|
|
un paquet — et c'est l'ID qui dit lequel gouverne le système ;
|
|
|
|
|
- la commande s'affiche AVANT la question. On approuve ce qu'on a lu, pas un
|
|
|
|
|
« (y/N) » posé à l'aveugle.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import shlex
|
|
|
|
|
import shutil
|
|
|
|
|
|
|
|
|
|
from script.todo.todo_i18n import t
|
|
|
|
|
|
|
|
|
|
# Les familles, dans l'ordre où le PATH est interrogé quand l'ID de la
|
|
|
|
|
# distribution est inconnu.
|
|
|
|
|
FAMILIES = ("apt-get", "dnf", "pacman", "zypper")
|
|
|
|
|
|
|
|
|
|
# Le préfixe de commande de chaque famille ; les paquets s'ajoutent au bout.
|
|
|
|
|
# Tous non interactifs : un menu qui rend la main à un prompt apt bloque.
|
|
|
|
|
_INSTALL = {
|
|
|
|
|
"apt-get": ("sudo", "apt-get", "install", "-y"),
|
|
|
|
|
"dnf": ("sudo", "dnf", "install", "-y"),
|
|
|
|
|
"pacman": ("sudo", "pacman", "-S", "--needed", "--noconfirm"),
|
|
|
|
|
"zypper": ("sudo", "zypper", "--non-interactive", "install"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ID de /etc/os-release -> famille. Les dérivées sont nommées explicitement :
|
|
|
|
|
# ID_LIKE existe mais manque ou ment sur assez de distributions pour qu'on ne
|
|
|
|
|
# s'y fie pas seul, et le repli par le PATH couvre ce qui n'est pas listé.
|
|
|
|
|
_OS_ID_FAMILY = {
|
|
|
|
|
"ubuntu": "apt-get",
|
|
|
|
|
"debian": "apt-get",
|
|
|
|
|
"linuxmint": "apt-get",
|
|
|
|
|
"pop": "apt-get",
|
|
|
|
|
"raspbian": "apt-get",
|
|
|
|
|
"fedora": "dnf",
|
|
|
|
|
"rhel": "dnf",
|
|
|
|
|
"centos": "dnf",
|
|
|
|
|
"almalinux": "dnf",
|
|
|
|
|
"rocky": "dnf",
|
|
|
|
|
"arch": "pacman",
|
|
|
|
|
"manjaro": "pacman",
|
|
|
|
|
"endeavouros": "pacman",
|
|
|
|
|
"opensuse": "zypper",
|
|
|
|
|
"opensuse-leap": "zypper",
|
|
|
|
|
"opensuse-tumbleweed": "zypper",
|
|
|
|
|
"sles": "zypper",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def os_id() -> str:
|
|
|
|
|
"""L'ID de la distribution hôte, ou '' si /etc/os-release est illisible."""
|
|
|
|
|
try:
|
|
|
|
|
with open("/etc/os-release", encoding="utf-8") as fh:
|
|
|
|
|
for line in fh:
|
|
|
|
|
if line.startswith("ID="):
|
|
|
|
|
return line.split("=", 1)[1].strip().strip('"').lower()
|
|
|
|
|
except OSError:
|
|
|
|
|
pass
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def family() -> str | None:
|
|
|
|
|
"""La famille qui gouverne cette machine, ou None si aucune n'est connue.
|
|
|
|
|
|
|
|
|
|
L'ID de la distribution d'abord, le PATH ensuite : sur une machine qui
|
|
|
|
|
porte deux gestionnaires, l'ID dit lequel possède le système.
|
|
|
|
|
"""
|
|
|
|
|
connue = _OS_ID_FAMILY.get(os_id())
|
|
|
|
|
if connue and shutil.which(connue):
|
|
|
|
|
return connue
|
|
|
|
|
for candidate in FAMILIES:
|
|
|
|
|
if shutil.which(candidate):
|
|
|
|
|
return candidate
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
[ADD] long_test : ERPLibre installé sur NixOS, de bout en bout
Une machine, une question binaire : le chemin que le MENU emprunte
aboutit-il sur un système déclaratif. La commande d'installation est celle
du menu, prise telle quelle — un test qui installerait par ses propres
soins prouverait SON chemin, pas celui du produit, et c'est là que les
pannes se cachaient.
Le verdict est l'ÉTAT de la machine, non un code de retour :
« nixos-rebuild » rend 4 quand une unité n'a pas redémarré alors que le
système est activé. Il vit dans long_test/ parce qu'il crée une VM et
prend des heures ; le lanceur unitaire doit rester lançable en secondes.
--- EN ---
One machine, one binary question: does the path the MENU takes succeed on
a declarative system. The install command is the menu's, taken as is — a
test installing by its own means would prove ITS path, not the product's,
and that is where the failures hid.
The verdict is the machine's STATE, not an exit code: "nixos-rebuild"
returns 4 when a unit failed to restart while the system is active. It
lives in long_test/ because it creates a VM and takes hours; the unit
runner must stay runnable in seconds.
Assisted-by: Claude Opus 5
2026-09-16 00:42:30 -04:00
|
|
|
def conseil_sans_gestionnaire() -> str:
|
|
|
|
|
"""Ce qu'on peut faire là où aucune famille impérative ne gouverne, ou "".
|
|
|
|
|
|
|
|
|
|
« Aucun gestionnaire connu » est VRAI sur NixOS et n'y mène nulle part :
|
|
|
|
|
rien ne s'y installe par une commande, tout s'y déclare. Le dire seul
|
|
|
|
|
ferme la porte ; nommer les deux gestes qui marchent l'ouvre — le shell
|
|
|
|
|
jetable pour essayer, la déclaration pour ce qui reste.
|
|
|
|
|
|
|
|
|
|
Vide ailleurs : sur une machine dont on ne sait rien, inventer un conseil
|
|
|
|
|
vaut moins que de se taire.
|
|
|
|
|
"""
|
|
|
|
|
if os_id() != "nixos":
|
|
|
|
|
return ""
|
|
|
|
|
return t(
|
|
|
|
|
"NixOS: nix-shell -p <package> for one shell,"
|
|
|
|
|
" or declare it in /etc/nixos/configuration.nix"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
[ADD] script todo : installer les outils manquants, les quatre familles
La réduction sûre listait les outils manquants et s'arrêtait là. Trois
écritures séparées savaient installer, chacune un sous-ensemble
différent : openSUSE posait virt-viewer, mais ni navigateur CLI ni
lm-sensors.
Le binaire n'est presque jamais le paquet : sgdisk vit dans « gdisk »
chez Debian et Fedora, dans « gptfdisk » chez Arch et openSUSE. Deux
règles passent aussi dans la composante : l'ID de la distribution décide
avant le PATH, et la commande s'affiche avant la question.
Vérifié : commandes identiques sur les trois familles déjà couvertes,
4139 tests verts.
--- EN ---
The safe shrink listed the missing tools and stopped there. Three
separate writings knew how to install, each covering a different
subset: openSUSE could put virt-viewer down, but neither a CLI browser
nor lm-sensors.
A binary is almost never the package: sgdisk lives in "gdisk" on Debian
and Fedora, in "gptfdisk" on Arch and openSUSE. Two rules move into the
component as well: the distribution ID decides before the PATH, and the
command is shown before the question.
Checked: commands identical on the three families already covered, and
4139 tests green.
Assisted-by: Claude Opus 5
2026-08-31 07:18:13 -04:00
|
|
|
def install_command(paquets, famille=None) -> list | None:
|
|
|
|
|
"""La commande d'installation, en liste d'arguments. None si personne.
|
|
|
|
|
|
|
|
|
|
`paquets` est une liste de noms, ou un dict {famille: liste} quand le
|
|
|
|
|
paquet ne porte pas le même nom partout (lm-sensors chez Debian,
|
|
|
|
|
lm_sensors chez Fedora et Arch).
|
|
|
|
|
"""
|
|
|
|
|
famille = famille or family()
|
|
|
|
|
if not famille:
|
|
|
|
|
return None
|
|
|
|
|
if isinstance(paquets, dict):
|
|
|
|
|
paquets = paquets.get(famille) or []
|
|
|
|
|
paquets = [paquets] if isinstance(paquets, str) else list(paquets)
|
|
|
|
|
if not paquets:
|
|
|
|
|
return None
|
|
|
|
|
return list(_INSTALL[famille]) + paquets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve(binaires, commun=None, par_famille=None, famille=None):
|
|
|
|
|
"""(paquets, inconnus) pour les binaires demandés.
|
|
|
|
|
|
|
|
|
|
Le nom du binaire n'est presque jamais celui du paquet : `commun` donne la
|
|
|
|
|
correspondance quand elle vaut pour les quatre familles, `par_famille` la
|
|
|
|
|
corrige là où elle diverge. Les paquets sortent dédoublonnés et dans
|
|
|
|
|
l'ordre demandé — trois binaires d'un même paquet ne le demandent qu'une
|
|
|
|
|
fois. `inconnus` liste ce pour quoi aucun paquet n'est déclaré : à dire,
|
|
|
|
|
jamais à deviner.
|
|
|
|
|
"""
|
|
|
|
|
famille = famille or family()
|
|
|
|
|
surcharge = (par_famille or {}).get(famille, {})
|
|
|
|
|
commun = commun or {}
|
|
|
|
|
paquets, inconnus = [], []
|
|
|
|
|
for binaire in binaires:
|
|
|
|
|
paquet = surcharge.get(binaire) or commun.get(binaire)
|
|
|
|
|
if not paquet:
|
|
|
|
|
inconnus.append(binaire)
|
|
|
|
|
elif paquet not in paquets:
|
|
|
|
|
paquets.append(paquet)
|
|
|
|
|
return paquets, inconnus
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def show_and_ask(cmd, question, is_yes, prefix=" ") -> bool:
|
|
|
|
|
"""Afficher la commande, PUIS demander. True si l'opérateur accepte.
|
|
|
|
|
|
|
|
|
|
L'ordre est le fond de l'affaire : une question posée avant la commande
|
|
|
|
|
fait approuver à l'aveugle. Centralisé ici pour qu'aucun appelant ne
|
|
|
|
|
puisse l'inverser.
|
|
|
|
|
"""
|
|
|
|
|
lisible = cmd if isinstance(cmd, str) else shlex.join(cmd)
|
|
|
|
|
print(f"{prefix}{t('Will execute:')} {lisible}")
|
|
|
|
|
return bool(is_yes(input(question)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ask_and_install(execute, cmd, question, is_yes, prefix=" "):
|
|
|
|
|
"""Proposer puis lancer. Rend le code de sortie, ou None si rien n'a été
|
|
|
|
|
lancé — refus de l'opérateur, ou aucune commande à proposer.
|
|
|
|
|
|
|
|
|
|
`execute` est le lanceur du CLI : exec_command_live REND le code de sortie
|
|
|
|
|
et ne lève rien, donc l'appelant doit tester ce qui revient.
|
|
|
|
|
"""
|
|
|
|
|
if not cmd:
|
|
|
|
|
print(f"{prefix}⚠ {t('no known package manager here.')}")
|
[ADD] long_test : ERPLibre installé sur NixOS, de bout en bout
Une machine, une question binaire : le chemin que le MENU emprunte
aboutit-il sur un système déclaratif. La commande d'installation est celle
du menu, prise telle quelle — un test qui installerait par ses propres
soins prouverait SON chemin, pas celui du produit, et c'est là que les
pannes se cachaient.
Le verdict est l'ÉTAT de la machine, non un code de retour :
« nixos-rebuild » rend 4 quand une unité n'a pas redémarré alors que le
système est activé. Il vit dans long_test/ parce qu'il crée une VM et
prend des heures ; le lanceur unitaire doit rester lançable en secondes.
--- EN ---
One machine, one binary question: does the path the MENU takes succeed on
a declarative system. The install command is the menu's, taken as is — a
test installing by its own means would prove ITS path, not the product's,
and that is where the failures hid.
The verdict is the machine's STATE, not an exit code: "nixos-rebuild"
returns 4 when a unit failed to restart while the system is active. It
lives in long_test/ because it creates a VM and takes hours; the unit
runner must stay runnable in seconds.
Assisted-by: Claude Opus 5
2026-09-16 00:42:30 -04:00
|
|
|
conseil = conseil_sans_gestionnaire()
|
|
|
|
|
if conseil:
|
|
|
|
|
print(f"{prefix} {conseil}")
|
[ADD] script todo : installer les outils manquants, les quatre familles
La réduction sûre listait les outils manquants et s'arrêtait là. Trois
écritures séparées savaient installer, chacune un sous-ensemble
différent : openSUSE posait virt-viewer, mais ni navigateur CLI ni
lm-sensors.
Le binaire n'est presque jamais le paquet : sgdisk vit dans « gdisk »
chez Debian et Fedora, dans « gptfdisk » chez Arch et openSUSE. Deux
règles passent aussi dans la composante : l'ID de la distribution décide
avant le PATH, et la commande s'affiche avant la question.
Vérifié : commandes identiques sur les trois familles déjà couvertes,
4139 tests verts.
--- EN ---
The safe shrink listed the missing tools and stopped there. Three
separate writings knew how to install, each covering a different
subset: openSUSE could put virt-viewer down, but neither a CLI browser
nor lm-sensors.
A binary is almost never the package: sgdisk lives in "gdisk" on Debian
and Fedora, in "gptfdisk" on Arch and openSUSE. Two rules move into the
component as well: the distribution ID decides before the PATH, and the
command is shown before the question.
Checked: commands identical on the three families already covered, and
4139 tests green.
Assisted-by: Claude Opus 5
2026-08-31 07:18:13 -04:00
|
|
|
return None
|
|
|
|
|
if not show_and_ask(cmd, question, is_yes, prefix=prefix):
|
|
|
|
|
print(t("Nothing to do."))
|
|
|
|
|
return None
|
|
|
|
|
lisible = cmd if isinstance(cmd, str) else shlex.join(cmd)
|
|
|
|
|
return execute.exec_command_live(lisible, source_erplibre=False)
|