[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], ...]:
|
def zypper_guide(rolling: bool) -> tuple[tuple[str, str, str], ...]:
|
||||||
"""Aide-mémoire zypper. `rolling` : Tumbleweed plutôt que Leap.
|
"""Aide-mémoire zypper. `rolling` : Tumbleweed plutôt que Leap.
|
||||||
|
|
||||||
|
|
@ -1965,6 +1978,12 @@ def build_motd(
|
||||||
lang,
|
lang,
|
||||||
narrow,
|
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:
|
if el_dir:
|
||||||
body.append("")
|
body.append("")
|
||||||
el_rows = erplibre_guide(el_dir, el_make, editor)
|
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; }; "
|
"{ sudo dnf clean all; sudo dnf install -y --refresh $PKGS; }; "
|
||||||
"elif command -v pacman >/dev/null 2>&1; then "
|
"elif command -v pacman >/dev/null 2>&1; then "
|
||||||
+ self._qemu_pacman_prepare_cmd()
|
+ self._qemu_pacman_prepare_cmd()
|
||||||
+ "sudo pacman -S --needed --noconfirm $PKGS; "
|
# bash-completion n'est PAS dans une image cloud Arch, là où les
|
||||||
"elif command -v zypper >/dev/null 2>&1; then "
|
# 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
|
# openSUSE : « --non-interactive » vaut le -y des autres, et
|
||||||
# « --auto-agree-with-licenses », qui va APRÈS « install »,
|
# « --auto-agree-with-licenses », qui va APRÈS « install »,
|
||||||
# évite un blocage sur une licence à accepter.
|
# évite un blocage sur une licence à accepter.
|
||||||
|
|
|
||||||
|
|
@ -584,6 +584,41 @@ class QemuInstallMixin:
|
||||||
"fi; "
|
"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):
|
def _qemu_zypper_mirror_cmd(self):
|
||||||
"""Réécrit l'hôte des dépôts zypper vers un miroir plus proche."""
|
"""Réécrit l'hôte des dépôts zypper vers un miroir plus proche."""
|
||||||
mirrors = " ".join(self._QEMU_ZYPPER_MIRRORS)
|
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(
|
script = self.todo._qemu_erplibre_remote_cmd(
|
||||||
"develop", None, False, "gnome", "", "deb", ("pycharm",)
|
"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(
|
self.assertLess(
|
||||||
script.index("PyCharm"), script.index("make install_os")
|
script.index("PyCharm"), script.index("make install_os")
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue