erplibre/test/test_qemu_bootstrap_nix.py
Mathieu Benoit a9f91dda87 [FIX] install nixos : l'amorçage, les en-têtes, les manuels écartés
L'amorçage d'ERPLibre n'atteignait pas NixOS : le Makefile force
« SHELL := /bin/bash », que l'image ne porte pas, et le fournisseur de
Python ne cherche l'interpréteur du système qu'en /usr/bin/pythonX.Y.
Les deux répondent une fois envfs déclaré.

Ce qui n'a pas de roue amont se compile et réclame ses en-têtes : ils sont
liés au profil du système, leurs bibliothèques vont au chargeur, et
pg_config — dérivation à part dans nixpkgs — est nommé. La sortie « doc »
de CPython est écartée : sa construction est un Sphinx de 3 000 pages qui
domine le temps d'installation d'une VM de 4 Go.

--- EN ---

ERPLibre's bootstrap did not reach NixOS: the Makefile forces "SHELL :=
/bin/bash", which the image lacks, and the Python provider only looks for
the system interpreter at /usr/bin/pythonX.Y. Both answer once envfs is
declared.

What has no upstream wheel compiles and demands its headers: they are
linked into the system profile, their libraries reach the loader, and
pg_config — a separate derivation in nixpkgs — is named. CPython's "doc"
output is left out: building it is a 3000-page Sphinx run that dominates
install time on a 4 GB VM.

Assisted-by: Claude Opus 5
2026-09-16 21:31:03 -04:00

127 lines
5.2 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""L'amorçage d'une installation ERPLibre sur NixOS.
Le catalogue offre NixOS et le dépôt sait y déclarer ses dépendances ; entre
les deux manquait le geste qui va du système nu au dépôt cloné. L'amorçage
posait curl, git et make par apt, dnf, pacman, zypper ou yum, et s'arrêtait
sur « Aucun gestionnaire de paquets » — avant le clone, donc avant tout.
Deux nœuds, et chacun se dénoue au même endroit : on ne peut pas être
déclaratif AVANT d'avoir le dépôt qui porte la déclaration.
- git et make sont ABSENTS de l'image NixOS (curl et sudo, eux, y sont). Ils
sont donc posés dans le profil de l'utilisateur pour la durée du clone, et
« make install_os » les redéclare ensuite pour le système entier ;
- « /bin/bash » n'existe pas non plus — le shell vit dans le store. Le
Makefile l'exigeait dès sa première ligne, et make s'arrêtait avant
d'exécuter la moindre recette, y compris celle qui installe de quoi créer
ce chemin.
"""
import subprocess
import sys
import unittest
from pathlib import Path
sys.argv = ["todo.py"]
from script.todo.todo import TODO # noqa: E402
RACINE = Path(__file__).resolve().parents[1]
class LAmorcage(unittest.TestCase):
def setUp(self):
self.cmd = TODO.__new__(TODO)._qemu_erplibre_remote_cmd("develop")
def test_a_nixos_vm_is_no_longer_turned_away(self):
"""Le message d'arrêt nomme désormais nix : sans la branche, une VM
NixOS n'atteignait jamais son clone."""
self.assertIn("command -v nix-env", self.cmd)
self.assertIn("(apt/dnf/pacman/zypper/yum/nix)", self.cmd)
def test_the_tools_come_from_nixpkgs_not_from_a_channel(self):
"""L'utilisateur n'a aucun canal sur cette image, là où NIX_PATH est
posé pour tout le monde et pointe les canaux de root."""
self.assertIn("nix-env -f '<nixpkgs>' -iA", self.cmd)
def test_make_is_asked_for_by_its_nixpkgs_name(self):
"""Dans nixpkgs, make s'appelle gnumake : demander « make » ferait
échouer la pose sans que rien ne dise pourquoi."""
i = self.cmd.index("nix-env -f")
pose = self.cmd[i : self.cmd.index(";", i)]
self.assertIn("gnumake", pose)
self.assertNotIn(" make", pose)
def test_python3_is_posed_here_and_only_here(self):
"""Les images des quatre autres familles l'embarquent — cloud-init
est écrit en Python. Sur NixOS il vit dans le store, hors PATH, et la
première recette de « make install_os » s'arrêtait sur « env:
python3: No such file or directory »."""
i = self.cmd.index("nix-env -f")
pose = self.cmd[i : self.cmd.index(";", i)]
self.assertIn("python3", pose)
self.assertNotIn("python3", self.cmd[: self.cmd.index("PKGS=")])
def test_the_path_is_widened_after_the_pose(self):
"""Le PATH du shell distant est figé à son ouverture : sans cette
ligne, le contrôle qui suit déclare git manquant sur une machine où
il vient d'être installé."""
i = self.cmd.index("nix-env -f")
j = self.cmd.index("for t in curl git make")
self.assertIn('PATH="$HOME/.nix-profile/bin:$PATH"', self.cmd[i:j])
def test_the_check_that_follows_is_unchanged(self):
"""Le même contrôle vaut pour les six familles : une erreur nette
plutôt qu'un « command not found » cryptique plus loin."""
self.assertIn("for t in curl git make; do command -v $t", self.cmd)
def test_it_is_valid_shell(self):
fini = subprocess.run(
["bash", "-n"], input=self.cmd, text=True, capture_output=True
)
self.assertEqual(0, fini.returncode, fini.stderr)
class LeShellDuMakefile(unittest.TestCase):
PREMIERES = (RACINE / "Makefile").read_text(encoding="utf-8")[:600]
def test_bash_is_looked_for_rather_than_assumed(self):
"""« /bin/bash » n'existe pas sur NixOS."""
self.assertIn("command -v bash", self.PREMIERES)
self.assertNotIn("SHELL := /bin/bash", self.PREMIERES)
def test_a_host_without_bash_keeps_the_old_path(self):
"""Le repli ne rend pas la panne pire qu'avant sur une plateforme qui
n'a pas bash du tout."""
self.assertIn("|| echo /bin/bash", self.PREMIERES)
def test_the_assignment_stays_immediate(self):
"""« = » différé ferait relancer le sous-shell à chaque recette, et
SHELL est lu à chacune."""
self.assertIn("SHELL := ", self.PREMIERES)
def test_the_resolved_shell_exists_here(self):
"""L'épreuve sur la machine qui lit ce test : make doit pouvoir
exécuter une recette."""
ligne = next(
x
for x in (RACINE / "Makefile")
.read_text(encoding="utf-8")
.splitlines()
if x.startswith("SHELL")
)
fini = subprocess.run(
["make", "-s", "-f", "-", "essai"],
input=f"{ligne}\nessai:\n\techo ok\n",
text=True,
capture_output=True,
cwd=RACINE,
)
self.assertEqual(0, fini.returncode, fini.stderr)
self.assertIn("ok", fini.stdout)
if __name__ == "__main__":
unittest.main()