From 29b12c8ca1b478b6e59b5f11a0278c9e052b8534 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 2 Sep 2026 08:04:05 -0400 Subject: [PATCH] =?UTF-8?q?[ADD]=20qemu=20arch=20:=20yay=20et=20bash-compl?= =?UTF-8?q?etion=20sur=20l'invit=C3=A9,=20guide=20au=20MOTD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Une image cloud Arch est nue : ni bash-completion, ni accès à l'AUR. Les deux arrivent avec l'amorçage, sur la seule branche pacman. yay-bin plutôt que yay, dont le paquet source compile Go pour le même outil ; la construction reste sous l'utilisateur de la VM, makepkg refusant root. Le « || true » qui ferme le bloc porte : le groupe est le dernier membre de sa liste « || », donc set -e s'y applique et un sudo en échec emporterait l'installation entière. Le guide de connexion n'annonce yay que si une installation a eu lieu. Vérifié : 10 tests, dont « bash -n » sur la commande distante entière et la survie du bloc sous set -e avec un PATH vide. --- EN --- An Arch cloud image is bare: no bash-completion, no AUR access. Both come with the bootstrap, on the pacman branch alone. yay-bin rather than yay, whose source package compiles Go for the same tool; the build stays under the VM user, makepkg refusing root. The « || true » closing the block carries weight: the group is the last member of its « || » list, so set -e applies inside it and one failing sudo would take the whole install down. The login guide announces yay only when an install ran. Checked: 10 tests, among them « bash -n » over the whole remote command and the block surviving set -e with an empty PATH. Assisted-by: Claude Opus 5 --- script/qemu/deploy_qemu.py | 19 ++++ script/todo/qemu_deploy.py | 8 +- script/todo/qemu_install.py | 35 ++++++++ test/test_qemu_arch_yay.py | 154 ++++++++++++++++++++++++++++++++ test/test_qemu_desktop_tools.py | 7 +- 5 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 test/test_qemu_arch_yay.py diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index 8955823..52a96a7 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -1759,6 +1759,19 @@ PKG_GUIDE: dict[str, tuple[tuple[str, str, str], ...]] = { } +# Assistant AUR posé sur l'invité Arch par l'amorçage d'installation. Les +# formes viennent du manuel de yay : il reprend les options de pacman, sauf +# « -Yc » qui lui est propre. Jamais sous sudo — yay appelle sudo lui-même +# pour la seule étape qui en a besoin, et le lancer en root fait échouer +# makepkg, qui refuse de construire sous cet utilisateur. +AUR_GUIDE: tuple[tuple[str, str, str], ...] = ( + ("yay -Syu", "mettre à jour dépôts + AUR", "upgrade repos and AUR"), + ("yay -S ", "installer depuis l'AUR", "install from the AUR"), + ("yay -Ss ", "chercher dans l'AUR", "search the AUR"), + ("yay -Yc", "retirer les orphelins", "remove orphans"), +) + + def zypper_guide(rolling: bool) -> tuple[tuple[str, str, str], ...]: """Aide-mémoire zypper. `rolling` : Tumbleweed plutôt que Leap. @@ -1965,6 +1978,12 @@ def build_motd( lang, narrow, ) + # yay arrive avec l'amorçage d'installation, pas avec l'image : une VM + # déployée sans installation n'annonce donc pas une commande absente. + # C'est la règle du bloc ERPLibre ci-dessous, appliquée au même signal. + if mgr == "pacman" and el_dir: + body.append("") + body += motd_block("AUR — yay", AUR_GUIDE, lang, narrow) if el_dir: body.append("") el_rows = erplibre_guide(el_dir, el_make, editor) diff --git a/script/todo/qemu_deploy.py b/script/todo/qemu_deploy.py index 9ea06f1..00ef384 100644 --- a/script/todo/qemu_deploy.py +++ b/script/todo/qemu_deploy.py @@ -186,8 +186,12 @@ class QemuDeployMixin: "{ sudo dnf clean all; sudo dnf install -y --refresh $PKGS; }; " "elif command -v pacman >/dev/null 2>&1; then " + self._qemu_pacman_prepare_cmd() - + "sudo pacman -S --needed --noconfirm $PKGS; " - "elif command -v zypper >/dev/null 2>&1; then " + # bash-completion n'est PAS dans une image cloud Arch, là où les + # images Debian et Fedora l'embarquent : sans lui, la tabulation + # ne complète que les noms de fichiers, pas les sous-commandes. + + "sudo pacman -S --needed --noconfirm $PKGS bash-completion; " + + self._qemu_yay_install_cmd() + + "elif command -v zypper >/dev/null 2>&1; then " # openSUSE : « --non-interactive » vaut le -y des autres, et # « --auto-agree-with-licenses », qui va APRÈS « install », # évite un blocage sur une licence à accepter. diff --git a/script/todo/qemu_install.py b/script/todo/qemu_install.py index daafb07..f4c419c 100644 --- a/script/todo/qemu_install.py +++ b/script/todo/qemu_install.py @@ -584,6 +584,41 @@ class QemuInstallMixin: "fi; " ) + @staticmethod + def _qemu_yay_install_cmd(): + """Pose yay, l'assistant AUR, sur un invité Arch. + + « yay-bin » et non « yay » : le paquet source compile son propre Go, + ce qui coûte plusieurs minutes et le compilateur avec ; le binaire + précompilé donne le même outil. + + makepkg REFUSE de tourner en root et sort en erreur ; le clonage et la + construction restent donc sous l'utilisateur de la VM, qui appelle + sudo pour la seule installation finale. Le NOPASSWD posé par + cloud-init rend ce sudo silencieux. + + yay est un bonus, pas une condition : le bloc se termine par « true » + pour qu'un AUR injoignable ne fasse pas échouer, sous « set -e », une + installation par ailleurs complète. + """ + return ( + "command -v yay >/dev/null 2>&1 || { " + "sudo pacman -S --needed --noconfirm base-devel git && " + "yd=$(mktemp -d) && " + "git clone --depth 1 https://aur.archlinux.org/yay-bin.git " + '"$yd" && ( cd "$yd" && makepkg -si --noconfirm ); ' + # « rm -rf » sur une variable vide rend 0 en silence sous -f : le + # nettoyage n'a donc pas besoin de savoir si le clonage a eu lieu. + 'rm -rf "$yd"; ' + # « || true » ferme le groupe ENTIER, et il porte. Le groupe est + # le DERNIER membre de la liste « || », donc set -e s'y applique + # et le premier sudo en échec emporterait toute l'installation. + # Un membre de plus l'y suspend, et rend le bloc inoffensif. + "} || true; " + "command -v yay >/dev/null 2>&1 " + '&& echo " yay installé" || echo " ⚠ yay non installé"; ' + ) + def _qemu_zypper_mirror_cmd(self): """Réécrit l'hôte des dépôts zypper vers un miroir plus proche.""" mirrors = " ".join(self._QEMU_ZYPPER_MIRRORS) diff --git a/test/test_qemu_arch_yay.py b/test/test_qemu_arch_yay.py new file mode 100644 index 0000000..37cc43a --- /dev/null +++ b/test/test_qemu_arch_yay.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# © 2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +"""L'invité Arch : yay et bash-completion, puis leur trace dans l'accueil. + +Une image cloud Arch est nue : ni bash-completion, ni accès à l'AUR. Les deux +s'ajoutent à l'amorçage d'installation, sur la seule branche pacman. + +Ce que ces tests gardent : + +- makepkg REFUSE de construire en root : la construction ne passe jamais par + sudo, et un « sudo makepkg » repasserait le test au rouge. +- yay est un bonus : sous « set -e », son échec ne doit pas emporter une + installation par ailleurs complète. +- L'accueil de session n'annonce yay que sur une VM qui l'aura vraiment — + la règle déjà tenue par le bloc ERPLibre, appliquée au même signal. +""" + +import importlib.util +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.argv = ["todo.py"] +from script.todo.todo import TODO # noqa: E402 + +RACINE = Path(__file__).resolve().parents[1] + + +def _deploy_qemu(): + """deploy_qemu.py chargé comme module, comme le fait todo.py.""" + path = RACINE / "script/qemu/deploy_qemu.py" + spec = importlib.util.spec_from_file_location("deploy_qemu", path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +DQ = _deploy_qemu() + + +class ArchGuestBootstrap(unittest.TestCase): + def setUp(self): + self.todo = TODO.__new__(TODO) + self.cmd = self.todo._qemu_erplibre_remote_cmd( + "develop", None, False, False, "", "deb", () + ) + + def test_bash_completion_rides_the_pacman_branch(self): + self.assertIn("bash-completion", self.cmd) + # Sur la branche pacman et nulle part ailleurs : les images Debian et + # Fedora l'embarquent déjà, l'ajouter là serait du bruit. + pacman = self.cmd.index("command -v pacman") + zypper = self.cmd.index("command -v zypper", pacman) + self.assertIn("bash-completion", self.cmd[pacman:zypper]) + + def test_yay_is_installed_from_the_prebuilt_package(self): + """yay-bin plutôt que yay : le paquet source compile Go, et le + compilateur avec, pour le même outil.""" + self.assertIn("aur.archlinux.org/yay-bin.git", self.cmd) + + def test_makepkg_never_runs_under_sudo(self): + """makepkg sort en erreur sous root : « running makepkg as root is + not allowed ». La construction reste sous l'utilisateur de la VM.""" + yay = self.todo._qemu_yay_install_cmd() + self.assertIn("makepkg -si --noconfirm", yay) + self.assertNotIn("sudo makepkg", yay) + + def test_a_failing_aur_does_not_break_the_chain(self): + """Sous « set -e », un groupe qui échoue arrête tout. Le bloc doit + rendre 0 même sans réseau, sans quoi une VM par ailleurs installée + serait comptée en échec.""" + yay = self.todo._qemu_yay_install_cmd() + script = "set -e\nPATH=/nonexistent\n" + yay + "\necho SURVECU" + with tempfile.NamedTemporaryFile( + "w", suffix=".sh", delete=False + ) as fh: + fh.write(script) + chemin = fh.name + res = subprocess.run( + ["bash", chemin], capture_output=True, text=True, timeout=60 + ) + self.assertEqual(res.returncode, 0, res.stderr) + self.assertIn("SURVECU", res.stdout) + + def test_the_whole_remote_command_is_valid_shell(self): + """Une erreur de syntaxe ne se verrait qu'une fois la VM déployée.""" + res = subprocess.run( + ["bash", "-n"], + input=self.cmd, + capture_output=True, + text=True, + timeout=60, + ) + self.assertEqual(res.returncode, 0, res.stderr) + + +class AurInTheMotd(unittest.TestCase): + def test_arch_with_an_install_announces_yay(self): + motd = DQ.build_motd( + "arch", "latest", "amd64", "fr", "~/git/erplibre", "install", "vi" + ) + self.assertIn("AUR — yay", motd) + self.assertIn("yay -Syu", motd) + + def test_arch_without_an_install_announces_nothing(self): + motd = DQ.build_motd("arch", "latest", "amd64", "fr") + self.assertNotIn("yay", motd) + + def test_another_distro_never_gets_the_block(self): + for distro, version in ( + ("ubuntu", "24.04"), + ("debian", "13"), + ("fedora", "43"), + ("opensuse", "tumbleweed"), + ): + with self.subTest(distro=distro): + motd = DQ.build_motd( + distro, + version, + "amd64", + "fr", + "~/git/erplibre", + "install", + "vi", + ) + self.assertNotIn("yay", motd) + + def test_the_block_is_translated(self): + motd = DQ.build_motd( + "arch", "latest", "amd64", "en", "~/git/erplibre", "install", "vi" + ) + self.assertIn("search the AUR", motd) + self.assertNotIn("chercher dans l'AUR", motd) + + def test_the_block_stays_under_a_standard_terminal(self): + for lang in ("fr", "en"): + motd = DQ.build_motd( + "arch", + "latest", + "amd64", + lang, + "~/git/erplibre", + "install", + "vi", + ) + for line in motd.splitlines(): + self.assertLessEqual(len(line), 80, line) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_qemu_desktop_tools.py b/test/test_qemu_desktop_tools.py index 2e1e2d8..75cba88 100644 --- a/test/test_qemu_desktop_tools.py +++ b/test/test_qemu_desktop_tools.py @@ -182,7 +182,12 @@ class TestToolRemoteCommand(unittest.TestCase): script = self.todo._qemu_erplibre_remote_cmd( "develop", None, False, "gnome", "", "deb", ("pycharm",) ) - self.assertLess(script.index("PyCharm"), script.index("git clone")) + # « git clone --branch » et non « git clone » : l'amorçage Arch en + # pose un autre, celui de yay-bin, et il arrive plus tôt. Seul le + # clone du dépôt, qui seul porte une branche, est en jeu ici. + self.assertLess( + script.index("PyCharm"), script.index("git clone --branch") + ) self.assertLess( script.index("PyCharm"), script.index("make install_os") )