[ADD] qemu arch : yay et bash-completion sur l'invité, guide au MOTD
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
This commit is contained in:
parent
736ab3b676
commit
29b12c8ca1
5 changed files with 220 additions and 3 deletions
|
|
@ -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 <paquet>", "installer depuis l'AUR", "install from the AUR"),
|
||||
("yay -Ss <motif>", "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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
154
test/test_qemu_arch_yay.py
Normal file
154
test/test_qemu_arch_yay.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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")
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue