diff --git a/.claude/rules/08-deployment.md b/.claude/rules/08-deployment.md index cd8beb7..bd6c86a 100644 --- a/.claude/rules/08-deployment.md +++ b/.claude/rules/08-deployment.md @@ -7,7 +7,8 @@ - **DNS** : `script/deployment/update_dns_cloudflare.py` Plateformes supportées : Ubuntu 24.04 / 25.10 / 26.04, Linux Mint 22.3, -Debian 12, AlmaLinux 9+, Rocky Linux 9+, Arch Linux, macOS (pyenv), +Debian 12, AlmaLinux 9+, Rocky Linux 9+, openSUSE Tumbleweed, Arch Linux, +macOS (pyenv), Windows (WSL/Docker). Ubuntu 20.04 et 22.04 sont abandonnées : pikepdf exige qpdf >= 12.2, compilé diff --git a/CHANGELOG.base.md b/CHANGELOG.base.md index 56963b3..77386e5 100644 --- a/CHANGELOG.base.md +++ b/CHANGELOG.base.md @@ -58,7 +58,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Support Selenium grid from selenium_lib.py - Add addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, - AlmaLinux, Rocky Linux, Arch) on amd64, arm64 and s390x, with a menu to list, + AlmaLinux, Rocky Linux, openSUSE Tumbleweed, Arch) on amd64, + arm64 and s390x, with a menu to list, test, resize, delete and clean up - Graphical VMs: a server / desktop choice offering GNOME or Cinnamon (the Linux Mint desktop), with remote access through xrdp — TigerVNC on Arch @@ -111,7 +112,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Support de la grille Selenium depuis selenium_lib.py - Ajout des addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, - Debian, Fedora, AlmaLinux, Rocky Linux, Arch) en amd64, arm64 et s390x, avec + Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE Tumbleweed, + Arch) en amd64, arm64 et s390x, avec un menu pour lister, tester, redimensionner, supprimer et nettoyer - VM graphiques : un choix serveur / bureau proposant GNOME ou Cinnamon (le bureau de Linux Mint), avec accès distant par xrdp — TigerVNC sur Arch diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 9908a6e..040fc6c 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -32,7 +32,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Support de la grille Selenium depuis selenium_lib.py - Ajout des addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, - Debian, Fedora, AlmaLinux, Rocky Linux, Arch) en amd64, arm64 et s390x, avec + Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE Tumbleweed, + Arch) en amd64, arm64 et s390x, avec un menu pour lister, tester, redimensionner, supprimer et nettoyer - VM graphiques : un choix serveur / bureau proposant GNOME ou Cinnamon (le bureau de Linux Mint), avec accès distant par xrdp — TigerVNC sur Arch diff --git a/CHANGELOG.md b/CHANGELOG.md index 98cdeb8..7d4b996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,8 @@ Recreating the virtual environment, use installation guide from tool `make`. - Support Selenium grid from selenium_lib.py - Add addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, - AlmaLinux, Rocky Linux, Arch) on amd64, arm64 and s390x, with a menu to list, + AlmaLinux, Rocky Linux, openSUSE Tumbleweed, Arch) on amd64, + arm64 and s390x, with a menu to list, test, resize, delete and clean up - Graphical VMs: a server / desktop choice offering GNOME or Cinnamon (the Linux Mint desktop), with remote access through xrdp — TigerVNC on Arch diff --git a/script/install/install_dev.sh b/script/install/install_dev.sh index 9274230..3e6f8f7 100755 --- a/script/install/install_dev.sh +++ b/script/install/install_dev.sh @@ -1,6 +1,16 @@ #!/usr/bin/env bash -if [[ "${OSTYPE}" == "linux-gnu" ]]; then +# « $OSTYPE » ne decrit PAS le systeme : bash le fige au triplet avec lequel il +# a ete compile. openSUSE batit bash en « x86_64-suse-linux » et le renseigne +# donc a « linux », la ou Debian et Fedora donnent « linux-gnu ». Le test +# echouait, sans else, donc sans une ligne de sortie ni code d'erreur : AUCUNE +# dependance systeme n'etait posee, install_system() rendait 0, et +# l'installation s'arretait une heure plus tard sur « [Errno 2] ... 'cc' ». +# « uname -s » interroge le noyau et ne depend pas de la facon dont bash a ete +# bati. install_dev_extra_ubuntu.sh garde OSTYPE : il ne vise qu'Ubuntu. +OS_KERNEL="$(uname -s)" + +if [[ "${OS_KERNEL}" == "Linux" ]]; then source /etc/os-release if [[ "${ID}" == "ubuntu" ]]; then # 20.04 et 22.04 retirees : leur chaine d'outils ne suffit plus a ERPLibre @@ -24,6 +34,10 @@ if [[ "${OSTYPE}" == "linux-gnu" ]]; then ./script/install/install_debian_dependency.sh elif [[ "${ID}" == "arch" ]]; then ./script/install/install_arch_linux.sh + elif [[ "${ID}" == opensuse* || "${ID_LIKE}" == *"suse"* ]]; then + # openSUSE Tumbleweed / Leap, SLES : gestionnaire zypper. + echo "\n---- SUSE installation process started ----" + ./script/install/install_suse_dependency.sh elif [[ "${ID}" == "fedora" || "${ID_LIKE}" == *"fedora"* || "${ID_LIKE}" == *"rhel"* ]]; then # AlmaLinux et Rocky declarent ID_LIKE="rhel centos fedora" : elles passent # donc par ce meme script, qui aiguille dnf. @@ -31,9 +45,15 @@ if [[ "${OSTYPE}" == "linux-gnu" ]]; then ./script/install/install_fedora_dependency.sh else ./script/install/install_debian_dependency.sh - echo "Your Linux system is not supported, only support Ubuntu 24.04, 25.10, 26.04, Linux Mint 22.3, Debian, Fedora, AlmaLinux, Rocky Linux, Arch." + echo "Your Linux system is not supported, only support Ubuntu 24.04, 25.10, 26.04, Linux Mint 22.3, Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE, Arch." fi -elif [[ "${OSTYPE}" == "darwin"* ]]; then +elif [[ "${OS_KERNEL}" == "Darwin" ]]; then echo "\n---- Darwin installation process started ----" ./script/install/install_OSX_dependency.sh +else + # Un aiguillage muet qui rend 0 est le pire des cas : l'installation continue + # sur un systeme depourvu de tout. Meme sans savoir quoi poser, on le dit et + # on arrete. + echo "Systeme non reconnu (uname -s = ${OS_KERNEL}) : aucune dependance installee." + exit 1 fi diff --git a/script/install/install_suse_dependency.sh b/script/install/install_suse_dependency.sh new file mode 100755 index 0000000..9abddc2 --- /dev/null +++ b/script/install/install_suse_dependency.sh @@ -0,0 +1,337 @@ +#!/usr/bin/env bash +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +# +# Dépendances système ERPLibre pour la famille SUSE (zypper). Équivalent SUSE +# de install_debian_dependency.sh et install_fedora_dependency.sh. +# +# Visé : openSUSE Tumbleweed, la seule distribution du catalogue dont qpdf +# (12.3.2) dépasse déjà le seuil de pikepdf 10 — la compilation de qpdf, une +# demi-heure sous émulation s390x, ne s'y déclenche pas. + +. ./env_var.sh + +EL_USER=${USER} + +# « --non-interactive » est une option GLOBALE de zypper, elle précède la +# sous-commande. Les deux autres appartiennent à « install » et doivent la +# SUIVRE : placées avant, zypper répond « The flag +# --auto-agree-with-licenses is not known » et s'arrête. +# +# On demande à zypper quelles options il accepte, plutôt que de le supposer : +# la famille SUSE n'est pas testée ici, et un drapeau inconnu fait échouer la +# commande entière au lieu du seul paquet visé. +# --auto-agree-with-licenses : sinon zypper attend l'acceptation d'une +# licence, réponse que personne ne donnera dans une installation détachée. +# --ignore-unknown : saute un nom de paquet absent au lieu de refuser tout +# le lot, l'équivalent de « --skip-unavailable » côté dnf. +ZYP="sudo zypper --non-interactive" +ZYP_HELP="$(zypper install --help 2>&1 || true)" +ZYP_LIC="" +ZYP_IGN="" +case "${ZYP_HELP}" in + *--auto-agree-with-licenses*) ZYP_LIC="--auto-agree-with-licenses" ;; +esac +case "${ZYP_HELP}" in + *--ignore-unknown*) ZYP_IGN="--ignore-unknown" ;; +esac +echo "zypper : options retenues = ${ZYP_LIC} ${ZYP_IGN}" +# Lot obligatoire : un paquet manquant doit se voir. +ZYP_IN="${ZYP} install ${ZYP_LIC}" +# Lot best-effort : un nom absent est sauté. +ZYP_SOFT="${ZYP_IN} ${ZYP_IGN}" + +# openSUSE remplace des paquets par des variantes « compat » qui les FOURNISSENT +# sans en porter le nom : sur Tumbleweed, zlib-ng-compat-devel fournit +# zlib-devel et se trouve posé d'office. Demander « zlib-devel » par son nom +# force alors un échange, zypper soulève un conflit — et --non-interactive n'a +# aucune solution par défaut : il énumère les choix, puis ABANDONNE le lot +# entier. C'est ce qui a arrêté l'installation sur amd64, en emportant les huit +# autres dépendances de pyenv qui, elles, ne posaient aucun problème. +# « --ignore-unknown » ne couvre que les noms absents, jamais les conflits. +# +# On ne demande donc que ce que rien ne fournit déjà. rpm interroge les +# « Provides », pas seulement les noms : c'est la seule autorité sur la +# question. Effet de bord bienvenu, une relance ne redemande plus rien. +zyp_filter() { + local p + for p in "$@"; do + rpm -q --whatprovides "${p}" > /dev/null 2>&1 || printf '%s\n' "${p}" + done +} + +# Un telechargement tronque par le miroir fait echouer le LOT ENTIER, et +# zypper ne reessaie pas. Vecu : « libLLVM22 ... OpenSSL SSL_read: unexpected +# eof while reading » a emporte « rust cargo », et l'absence de cargo n'est +# apparue qu'une heure plus tard, dans la compilation de bcrypt. Une seconde +# tentative suffit dans la quasi-totalite des cas. +zyp_retry() { + local cmd=("$@") + "${cmd[@]}" && return 0 + echo " zypper a echoue (reseau ?), seconde tentative..." >&2 + "${cmd[@]}" +} + +zyp_in() { + local todo + mapfile -t todo < <(zyp_filter "$@") + if [ "${#todo[@]}" -eq 0 ]; then + echo " deja fourni : $*" + return 0 + fi + zyp_retry ${ZYP_IN} "${todo[@]}" +} + +zyp_soft() { + local todo + mapfile -t todo < <(zyp_filter "$@") + if [ "${#todo[@]}" -eq 0 ]; then + echo " deja fourni : $*" + return 0 + fi + zyp_retry ${ZYP_SOFT} "${todo[@]}" +} + +#-------------------------------------------------- +# Miroir des dépôts +#-------------------------------------------------- +# Le redirecteur officiel d'openSUSE n'est PAS géographique : mesuré depuis +# Montréal sur les métadonnées oss s390x (15 Mo), download.opensuse.org met +# 23,8 s — il sert depuis l'Europe — contre 2,7 s pour mirrors.rit.edu. +# +# Chaque miroir est SONDÉ sur le chemin de l'architecture courante : tous ne +# répliquent pas les architectures secondaires, et rit.edu sert justement +# zsystems mais pas x86_64. Aucun sondage concluant : on garde les dépôts de +# l'image, donc le comportement d'avant. +# Plusieurs miroirs, sondés dans l'ordre sur le chemin exact du produit et +# de l'architecture : aucun ne réplique tout. Relevé le 2026-08-12 — +# csclub sert Leap mais pas Tumbleweed, leaseweb sert Tumbleweed x86_64 +# mais pas les ports zsystems, et rit.edu — seule entrée jusqu'ici — était +# injoignable ce jour-là, ce qui renvoyait tout le monde en Europe. +ZYP_MIRRORS="https://mirror.csclub.uwaterloo.ca/opensuse https://mirrors.rit.edu/opensuse https://mirror.us.leaseweb.net/opensuse" +# Leap et Tumbleweed sont deux PRODUITS, pas deux versions du même : arbres de +# dépôts différents, et surtout une mise à jour de nature différente (plus bas). +SUSE_ID="$([ -r /etc/os-release ] && . /etc/os-release && echo "${ID}")" +SUSE_VER="$([ -r /etc/os-release ] && . /etc/os-release && echo "${VERSION_ID}")" +case "${SUSE_ID}" in + *tumbleweed*) SUSE_ROLLING=1 ;; + *) SUSE_ROLLING=0 ;; +esac +if [ "${SUSE_ROLLING}" = 1 ]; then + # Tumbleweed isole les architectures secondaires sous /ports/. + zp=tumbleweed + [ "$(uname -m)" = s390x ] && zp=ports/zsystems/tumbleweed +else + # Leap 16 unifie tout : un seul arbre, avec s390x dedans. Les chemins + # /ports/ équivalents rendent 404 — relevé, pas déduit. + zp="distribution/leap/${SUSE_VER}" +fi +for zm in ${ZYP_MIRRORS}; do + if curl -fsS --max-time 20 -o /dev/null \ + "${zm}/${zp}/repo/oss/repodata/repomd.xml"; then + sudo sed -i "s|https\?://download\.opensuse\.org|${zm}|g" \ + /etc/zypp/repos.d/*.repo 2> /dev/null || true + echo "miroir openSUSE : ${zm}" + break + fi +done + +echo -e "\n---- Rafraichissement des depots ----" +# Tumbleweed est ROLLING : installer sans rafraîchir mène à des paquets +# introuvables (l'index local pointe des versions déjà retirées du miroir). +sudo zypper --non-interactive refresh || true + +# Mise à jour COMPLÈTE, obligatoire pour la même raison qu'Arch : une rolling +# release ne supporte PAS les mises à jour partielles. L'image cloud est un +# instantané figé dont les dépôts ont avancé — vécu sur s390x, git 2.54 +# réclamait un perl-Git bâti contre un perl-base plus ancien que celui de +# l'image. zypper proposait alors trois solutions et ATTENDAIT un choix ; +# « --non-interactive » prend le défaut, « c » = annuler, et tout s'arrêtait. +echo -e "\n---- Mise a jour complete (dup) ----" +${ZYP} dup ${ZYP_LIC} --allow-vendor-change || true + +#-------------------------------------------------- +# Outils de compilation (build Python via pyenv, extensions Python) +#-------------------------------------------------- +echo -e "\n---- Outils de developpement ----" +# Les compilateurs sont installés EXPLICITEMENT, jamais par le seul motif : +# un « pattern » absent ou incomplet laisserait le système sans g++, et +# l'échec n'apparaîtrait qu'à la compilation de numpy — la leçon d'EL, où le +# groupe « development-tools » n'existe pas et où le repli ne partait jamais. +# Les noms non versionnés existent bien dans Tumbleweed (relevé dans le +# primary.xml du dépôt oss) : « gcc » y est un méta-paquet qui tire gcc15. +zyp_in gcc gcc-c++ make automake patch +retVal=$? +if [[ ${retVal} -ne 0 ]]; then + echo "zypper install compilers error." + exit 1 +fi +# Le motif ensuite, en complément et sans jamais bloquer. +${ZYP_IN} -t pattern devel_basis > /dev/null 2>&1 || true + +# Python appelle son compilateur par le nom gravé dans sysconfig, souvent +# « cc ». Sur openSUSE ce lien n'est garanti par aucune déclaration de +# paquet — vécu sur amd64 : « cc -pthread … error: [Errno 2] No such file or +# directory » à la compilation de python-ldap, alors que gcc était installé. +if ! command -v cc > /dev/null 2>&1 && command -v gcc > /dev/null 2>&1; then + sudo ln -sf "$(command -v gcc)" /usr/local/bin/cc + echo "cc -> $(command -v gcc)" +fi + +#-------------------------------------------------- +# PostgreSQL +#-------------------------------------------------- +echo -e "\n---- Install PostgreSQL Server ----" +zyp_in postgresql-server postgresql-contrib postgresql-server-devel +retVal=$? +if [[ ${retVal} -ne 0 ]]; then + echo "zypper install postgresql installation error." + exit 1 +fi +# openSUSE n'initialise pas le cluster : le service le fait au 1er démarrage, +# mais seulement si le répertoire de données est vide. +sudo systemctl enable --now postgresql 2> /dev/null || true +# PostGIS : absent des dépôts Tumbleweed s390x. Optionnel, ne bloque pas. +zyp_soft postgis || echo "PostGIS non installe (optionnel)." + +echo -e "\n---- Creating the ERPLibre PostgreSQL User ----" +sudo su - postgres -c "createuser -s ${EL_USER}" 2> /dev/null || true + +#-------------------------------------------------- +# Dépendances de build (extensions Python, Odoo) +#-------------------------------------------------- +echo -e "\n--- Installing suse dependency --" +# git-daemon : comme sur Fedora, « git daemon » n'est pas dans le paquet git de +# base. ERPLibre sert son manifeste par git://127.0.0.1:9418/ pendant +# « repo sync » — sans lui, « Connection refused » et synchro impossible. +# Rust n'est nécessaire QUE là où les roues manquent : bcrypt et cryptography +# portent une extension Rust, et sur amd64 leurs roues masquent le besoin. Sur +# s390x tout compile, et bcrypt s'arrête sur « error: can't find Rust +# compiler ». Tumbleweed livre 1.94, bien au-dessus du 1.78 qu'exige le +# Cargo.lock v4 de cryptography. On évite ainsi ~200 Mo inutiles sur amd64. +if [ "$(uname -m)" = "s390x" ]; then + zyp_soft rust cargo + if ! command -v cargo > /dev/null 2>&1; then + # FATAL, et non plus un simple avertissement : sur s390x, bcrypt et + # cryptography n'ont aucune roue et portent une extension Rust. Sans + # cargo l'installation est perdue d'avance — mais elle ne le montrait + # qu'une heure plus tard, au fond d'une trace pip. Mieux vaut s'arreter + # ici, ou la cause est encore lisible. + echo "cargo introuvable apres deux tentatives." >&2 + echo " bcrypt et cryptography ne peuvent pas se compiler sans lui." >&2 + echo " Verifiez le miroir zypper, puis relancez." >&2 + exit 1 + fi + # Sans roue s390x, TOUT se compile contre les en-têtes de la distribution. + # Cette liste reprend donc celles d'apt et de dnf, aux noms d'openSUSE près + # — relevés dans l'index oss de zsystems, jamais devinés. La tenir alignée + # ici plutôt que d'attendre : trois fois de suite, le même manque a été + # découvert d'abord par Debian, puis retrouvé une distribution à la fois. + # tbb-devel manifold3d refuse de configurer sans lui, et le dit dans un + # message de CMake, pas de compilateur : « Parallel mode + # enabled, but tbb was not found ». + # clang-devel pymupdf charge libclang.so par ctypes (lien juste après). + # cmake, pkgconf-pkg-config : nécessaires à el_qpdf_ensure, et ils + # n'arrivent que plus bas dans le script. + # + # « pkgconf-pkg-config » et non « pkg-config » : aucun RPM ne porte ce + # dernier nom dans Tumbleweed, il n'existe plus que comme capacité fournie + # par le premier. La capacité résout aujourd'hui, le nom réel ne dépend + # de rien. + # + # GeographicLib est absent : openSUSE n'empaquette que le binding Python, + # pas la bibliothèque C++. Rien à poser, contrairement à apt et dnf. + zyp_soft qpdf-devel libjpeg8-devel cmake pkgconf-pkg-config \ + tbb-devel geos-devel proj-devel krb5-devel ninja clang-devel llvm-devel + # Tumbleweed livre qpdf 12.3.2 : l'appel ne fait que le constater. Il est là + # pour que les trois familles suivent la même règle. + el_qpdf_ensure + # pymupdf charge « libclang.so » par son nom nu, via ctypes ; le paquet ne + # livre qu'un nom versionné. Il ne manque que le lien — même correctif que + # côté apt et dnf. + for d in /usr/lib64 /usr/lib; do + if [ -d "${d}" ] && [ ! -e "${d}/libclang.so" ]; then + so="$(ls -1 "${d}"/libclang.so.* 2> /dev/null | sort -V | tail -1)" + if [ -n "${so}" ]; then + sudo ln -s "${so}" "${d}/libclang.so" && sudo ldconfig + echo "libclang.so -> ${so}" + fi + fi + done +fi + +zyp_soft \ + git git-daemon wget libxslt-devel libzip-devel openldap2-devel \ + cyrus-sasl-devel libffi-devel libbz2-devel gnu_parallel swig cmake \ + portaudio-devel cups-devel xmlsec1 xmlsec1-openssl-devel \ + libmariadb-devel freetds-devel +retVal=$? +if [[ ${retVal} -ne 0 ]]; then + echo "zypper suse tool installation error." + exit 1 +fi + +# Dépendances de build pour pyenv (compilation de CPython) — CRITIQUE. +echo -e "\n---- Dependances pyenv (compilation Python) ----" +zyp_in \ + make gcc zlib-devel libbz2-devel readline-devel sqlite3-devel \ + libopenssl-devel tk-devel libffi-devel xz-devel patch findutils +retVal=$? +if [[ ${retVal} -ne 0 ]]; then + echo "zypper pyenv dependencies installation error." + exit 1 +fi + +# Dépendances selenium / bindings. +zyp_soft \ + cairo-devel python3-devel pkgconf-pkg-config gobject-introspection-devel \ + libXt-devel || echo "Dependances selenium partielles (optionnel)." + +#-------------------------------------------------- +# Node.js + npm (rtlcss, less) +#-------------------------------------------------- +echo -e "\n---- Installing nodeJS NPM and rtlcss ----" +# openSUSE versionne les paquets node (nodejs22, nodejs24…) et ne fournit pas +# toujours de méta-paquet « nodejs ». On prend le plus récent disponible. +NODE_OK=0 +for pkg in nodejs24 nodejs22 nodejs20 nodejs; do + if ${ZYP_IN} "${pkg}" "${pkg/nodejs/npm}" 2> /dev/null; then + NODE_OK=1 + echo "node fourni par ${pkg}" + break + fi +done +if [[ ${NODE_OK} -ne 1 ]] || ! command -v npm > /dev/null 2>&1; then + echo "zypper nodejs installation error." + exit 1 +fi +sudo npm install -g rtlcss less || echo "npm rtlcss/less: erreur (optionnel)." + +echo -e "\n---- Test tool ----" +npm install || echo "npm install (prettier/plugin-xml): erreur (optionnel)." +sudo ln -fs /usr/local/bin/lessc /usr/bin/lessc 2> /dev/null || true + +#-------------------------------------------------- +# nginx (optionnel) +#-------------------------------------------------- +if [ "${EL_INSTALL_NGINX}" = "True" ]; then + echo -e "\n---- Installing nginx ----" + zyp_soft nginx || echo "nginx: erreur (optionnel)." +fi + +#-------------------------------------------------- +# wkhtmltopdf (optionnel) +#-------------------------------------------------- +if [ "${EL_INSTALL_WKHTMLTOPDF}" = "True" ]; then + if ! command -v wkhtmltopdf > /dev/null 2>&1; then + echo -e "\n---- Installing wkhtml (best-effort) ----" + # wkhtmltopdf ne publie AUCUN paquet openSUSE. Le dépôt de la distribution + # fournit « wkhtmltopdf » quand il existe pour l'architecture ; sinon on + # renonce, Odoo sait imprimer sans lui (rendu dégradé des PDF). + zyp_soft wkhtmltopdf || echo "wkhtmltopdf non installe (optionnel)." + else + echo -e "\n---- Already installed wkhtml ----" + fi +fi + +echo -e "\n---- SUSE dependency installation done ----" diff --git a/script/qemu/README.base.md b/script/qemu/README.base.md index 240ac2c..19fef19 100644 --- a/script/qemu/README.base.md +++ b/script/qemu/README.base.md @@ -182,11 +182,16 @@ Catalog, per architecture (`deploy_qemu.py` is the source of truth): | fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only | | almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | +| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora builds s390x only for the current release, and on a separate tree (`fedora-secondary`) — hence the single version there. +`opensuse` is openSUSE Tumbleweed, the only entry whose qpdf (12.3.2) already +clears the pikepdf threshold: the half-hour qpdf build never runs there, which +matters under s390x emulation. + Provide an explicit image path as a positional argument to override the automatic download location. @@ -207,11 +212,16 @@ Catalogue, par architecture (`deploy_qemu.py` fait autorité) : | fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | `43` seule | | almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | +| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora ne construit s390x que pour la version courante, et sur une arborescence à part (`fedora-secondary`) — d'où la version unique. +`opensuse` désigne openSUSE Tumbleweed, seule entrée dont qpdf (12.3.2) +dépasse déjà le seuil de pikepdf : la compilation de qpdf, une demi-heure, ne +s'y déclenche jamais — ce qui compte sous émulation s390x. + Fournissez un chemin d'image en argument positionnel pour surcharger l'emplacement de téléchargement automatique. diff --git a/script/qemu/README.fr.md b/script/qemu/README.fr.md index 063279b..52c68ae 100644 --- a/script/qemu/README.fr.md +++ b/script/qemu/README.fr.md @@ -101,11 +101,16 @@ Catalogue, par architecture (`deploy_qemu.py` fait autorité) : | fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | `43` seule | | almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | +| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora ne construit s390x que pour la version courante, et sur une arborescence à part (`fedora-secondary`) — d'où la version unique. +`opensuse` désigne openSUSE Tumbleweed, seule entrée dont qpdf (12.3.2) +dépasse déjà le seuil de pikepdf : la compilation de qpdf, une demi-heure, ne +s'y déclenche jamais — ce qui compte sous émulation s390x. + Fournissez un chemin d'image en argument positionnel pour surcharger l'emplacement de téléchargement automatique. diff --git a/script/qemu/README.md b/script/qemu/README.md index 0ca3842..cda760f 100644 --- a/script/qemu/README.md +++ b/script/qemu/README.md @@ -95,11 +95,16 @@ Catalog, per architecture (`deploy_qemu.py` is the source of truth): | fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only | | almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | +| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora builds s390x only for the current release, and on a separate tree (`fedora-secondary`) — hence the single version there. +`opensuse` is openSUSE Tumbleweed, the only entry whose qpdf (12.3.2) already +clears the pikepdf threshold: the half-hour qpdf build never runs there, which +matters under s390x emulation. + Provide an explicit image path as a positional argument to override the automatic download location. diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index 267c0ef..c9da563 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -113,6 +113,12 @@ ROCKY_VERSIONS: dict[str, tuple[str, str, int, str]] = { "9": ("9", "rocky9", 2048, "20G"), "10": ("10", "rocky10", 2048, "20G"), } +# openSUSE Tumbleweed : rolling, donc une seule « version ». C'est la seule +# distribution du catalogue dont qpdf (12.3.2) dépasse déjà le seuil de +# pikepdf 10 — aucune compilation de qpdf, ce qui change tout sous émulation. +OPENSUSE_VERSIONS: dict[str, tuple[str, str, int, str]] = { + "tumbleweed": ("tumbleweed", "opensusetumbleweed", 2048, "20G"), +} # Arch est en rolling release : une seule « version » (latest). ARCH_VERSIONS: dict[str, tuple[str, str, int, str]] = { "latest": ("latest", "archlinux", 1024, "20G"), @@ -125,6 +131,7 @@ DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = { "fedora": (FEDORA_VERSIONS, "42"), "almalinux": (ALMALINUX_VERSIONS, "9"), "rocky": (ROCKY_VERSIONS, "10"), + "opensuse": (OPENSUSE_VERSIONS, "tumbleweed"), "arch": (ARCH_VERSIONS, "latest"), } @@ -134,6 +141,7 @@ ARCH_ALIASES: dict[str, dict[str, str]] = { "fedora": {"amd64": "x86_64", "arm64": "aarch64"}, "almalinux": {"amd64": "x86_64", "arm64": "aarch64"}, "rocky": {"amd64": "x86_64", "arm64": "aarch64"}, + "opensuse": {"amd64": "x86_64", "arm64": "aarch64"}, "arch": {"amd64": "x86_64", "arm64": "aarch64"}, } @@ -146,7 +154,13 @@ NON_X86_ARCHES: tuple[str, ...] = ("arm64", "s390x") # - s390x (IBM Z) : Ubuntu seulement (Debian/Fedora : 404 ; Arch : x86/arm). # - arm64/aarch64 : Ubuntu, Debian, Fedora (Arch : pas d'image cloud officielle # aarch64 sur geo.mirror.pkgbuild.com). -S390X_DISTROS: tuple[str, ...] = ("ubuntu", "almalinux", "rocky", "fedora") +S390X_DISTROS: tuple[str, ...] = ( + "ubuntu", + "almalinux", + "rocky", + "fedora", + "opensuse", +) # Une distro peut ne publier qu'une PARTIE de ses versions sur une # architecture. Déclaration POSITIVE : hors de cette table, toutes les versions @@ -173,6 +187,7 @@ ARM64_DISTROS: tuple[str, ...] = ( "fedora", "almalinux", "rocky", + "opensuse", ) @@ -211,6 +226,7 @@ DEBIAN_CLOUD_BASES: tuple[str, ...] = ( "https://laotzu.ftp.acc.umu.se/cdimage/cloud", ) ALMALINUX_CLOUD_BASE = "https://repo.almalinux.org/almalinux" +OPENSUSE_BASE = "https://download.opensuse.org" ROCKY_CLOUD_BASE = "https://dl.rockylinux.org/pub/rocky" FEDORA_BASE = "https://download.fedoraproject.org/pub/fedora/linux/releases" # Serveur MAÎTRE (pas de redirection MirrorManager) : repli fiable quand le @@ -287,6 +303,18 @@ def image_candidates( f"{ROCKY_CLOUD_BASE}/{version}/images/{a}/" f"Rocky-{version}-GenericCloud.latest.{a}.qcow2" ] + if distro == "opensuse": + # Les architectures secondaires vivent sous /ports/, et zsystems DOUBLE + # l'architecture dans le nom du fichier — irrégularité vérifiée dans + # l'index, pas déduite : « .s390x-s390x-Cloud » contre « .x86_64-Cloud ». + port = {"s390x": "ports/zsystems/", "arm64": "ports/aarch64/"}.get( + arch, "" + ) + tag = f"{a}-{a}" if arch == "s390x" else a + return [ + f"{OPENSUSE_BASE}/{port}tumbleweed/appliances/" + f"openSUSE-Tumbleweed-Minimal-VM.{tag}-Cloud.qcow2" + ] if distro == "arch": # Rolling release : image « latest » officielle (cloud-init inclus). return [f"{ARCH_CLOUD_BASE}/Arch-Linux-{a}-cloudimg.qcow2"] @@ -346,6 +374,8 @@ def default_image_name(distro: str, code: str, arch: str, version: str) -> str: return f"debian-{version}-genericcloud-{a}.qcow2" if distro == "arch": return f"arch-linux-{a}-cloudimg.qcow2" + if distro == "opensuse": + return f"opensuse-tumbleweed-minimal-vm-{a}.qcow2" if distro in ("almalinux", "rocky"): # « latest » est MUTABLE : le nom de cache porte donc la majeure, et # une image déjà téléchargée sera réutilisée telle quelle. C'est voulu @@ -1374,12 +1404,25 @@ def host_timezone() -> str: return "UTC" -def admin_group(distro: str) -> str: - """Groupe d'administration de la distribution. +def user_groups(distro: str) -> str: + """Groupes secondaires du compte créé par cloud-init. - Debian et Ubuntu : « sudo ». Famille RHEL (AlmaLinux, Rocky, Fedora) et - Arch : « wheel » — elles n'ont aucun groupe « sudo ».""" - return "sudo" if distro in ("ubuntu", "debian") else "wheel" + Le nom du groupe d'administration change d'une famille à l'autre, et un + nom INCONNU fait échouer « useradd -G » : l'utilisateur n'est alors pas + créé du tout, donc ni mot de passe ni clé SSH, et la VM démarre + inaccessible. Le privilège vient de toute façon de la directive « sudo: » + du cloud-config, pas du groupe — celui-ci n'est qu'une commodité. + + - Debian, Ubuntu : « sudo ». + - Famille RHEL (AlmaLinux, Rocky, CentOS, Fedora), Arch : « wheel ». + - openSUSE : aucun. Son cloud-init par défaut n'en met pas, et rien ne + garantit « wheel » sur une image Minimal-VM — dans le doute on s'abstient + plutôt que de risquer un compte non créé.""" + if distro in ("ubuntu", "debian"): + return "users, sudo" + if distro == "opensuse": + return "users" + return "users, wheel" def build_cloud_config( @@ -1400,7 +1443,7 @@ def build_cloud_config( # même écart, elles n'ont pas de groupe « sudo » mais « wheel ». # Le privilège lui-même vient de la ligne « sudo: » ci-dessus, pas du # groupe : celui-ci n'est qu'une commodité. - f" groups: users, {admin_group(args.distro)}", + f" groups: {user_groups(args.distro)}", " shell: /bin/bash", " lock_passwd: false" if pw_hash else " lock_passwd: true", ] diff --git a/script/todo/todo.py b/script/todo/todo.py index c3709f3..44c8d41 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -1027,6 +1027,7 @@ class TODO: "fedora": (["41", "42", "43", "44"], "42"), "almalinux": (["9", "10"], "9"), "rocky": (["9", "10"], "10"), + "opensuse": (["tumbleweed"], "tumbleweed"), "arch": (["latest"], "latest"), } @@ -1074,13 +1075,20 @@ class TODO: # Distros publiant des images cloud par architecture (cohérent avec # S390X_DISTROS / ARM64_DISTROS de deploy_qemu.py). amd64 : toutes. - _QEMU_S390X_DISTROS = ("ubuntu", "almalinux", "rocky", "fedora") + _QEMU_S390X_DISTROS = ( + "ubuntu", + "almalinux", + "rocky", + "fedora", + "opensuse", + ) _QEMU_ARM64_DISTROS = ( "ubuntu", "debian", "fedora", "almalinux", "rocky", + "opensuse", ) # Alias distro pour l'affichage (jeton générique -> nom courant). _QEMU_ARCH_ALIAS = {"amd64": "x86_64", "arm64": "aarch64"} @@ -4325,6 +4333,7 @@ class TODO: "dnf_env": "graphical-server-environment " "workstation-product-environment gnome-desktop", "pacman": "gnome gdm", + "zypper": "patterns-gnome-gnome_basic gdm", "service": "gdm", }, "cinnamon": { @@ -4336,6 +4345,7 @@ class TODO: "apt": "cinnamon-desktop-environment dbus-x11", "dnf_env": "cinnamon-desktop", "pacman": "cinnamon lightdm lightdm-gtk-greeter", + "zypper": "cinnamon lightdm", "service": "lightdm", }, } @@ -4344,6 +4354,7 @@ class TODO: "apt": {"packages": "xrdp", "port": 3389, "client": "RDP"}, "dnf": {"packages": "xrdp", "port": 3389, "client": "RDP"}, "pacman": {"packages": "tigervnc", "port": 5901, "client": "VNC"}, + "zypper": {"packages": "xrdp", "port": 3389, "client": "RDP"}, } # Place que prend un bureau complet, annoncée dans le plan : sur une image # cloud de 40 G, l'oublier remplit le disque en pleine installation. @@ -4408,6 +4419,44 @@ class TODO: ">/dev/null 2>&1 || true; " ) + # Miroirs openSUSE préférés, du plus proche au dernier recours. Le + # redirecteur officiel n'est PAS géographique pour cette distribution : + # mesuré depuis Montréal sur les métadonnées oss s390x (15 Mo), + # download.opensuse.org met 23,8 s — il sert depuis l'Europe — contre + # 2,7 s pour mirrors.rit.edu. Les trois familles dnf, elles, choisissent + # déjà un miroir canadien toutes seules ; rien à faire de ce côté. + # + # Chaque miroir est SONDÉ sur le chemin de l'architecture ET du produit + # courants, puis le premier qui répond gagne. C'est nécessaire : aucun ne + # réplique tout. Relevé le 2026-08-12 — + # csclub Leap oui, Tumbleweed non (404) + # rit.edu zsystems oui ; injoignable ce jour-là (curl 7) + # leaseweb Tumbleweed x86_64 et Leap oui, ports zsystems non + # D'où plusieurs entrées plutôt qu'une : avec la seule rit.edu, sa panne + # renvoyait tout le monde sur download.opensuse.org, servi d'Europe. + # Ordonnées par proximité de Montréal. Aucun sondage concluant : on garde + # les dépôts de l'image, donc le comportement d'avant. + _QEMU_ZYPPER_MIRRORS = ( + "https://mirror.csclub.uwaterloo.ca/opensuse", + "https://mirrors.rit.edu/opensuse", + "https://mirror.us.leaseweb.net/opensuse", + ) + + 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) + return ( + "zp=tumbleweed; " + '[ "$(uname -m)" = s390x ] && zp=ports/zsystems/tumbleweed; ' + f"for zm in {mirrors}; do " + "if curl -fsS --max-time 20 -o /dev/null " + '"$zm/$zp/repo/oss/repodata/repomd.xml"; then ' + "sudo sed -i " + '"s|https\\?://download\\.opensuse\\.org|$zm|g" ' + "/etc/zypp/repos.d/*.repo 2>/dev/null || true; " + f'echo " {t("openSUSE mirror:")} $zm"; break; fi; done; ' + ) + def _qemu_desktop_remote_cmd(self, flavour="gnome"): """Bloc shell installant le bureau choisi + son accès distant, quelle que soit la distribution. Même aiguillage que l'installation ERPLibre, @@ -4440,6 +4489,14 @@ class TODO: "|| sudo rm -f /var/lib/pacman/db.lck; " f"sudo pacman -S --needed --noconfirm {de['pacman']} " f"{rem['pacman']['packages']}; " + "elif command -v zypper >/dev/null 2>&1; then " + "sudo zypper --non-interactive refresh || true; " + # « --auto-agree-with-licenses » appartient à la SOUS-COMMANDE + # install, pas aux options globales : placé avant, zypper répond + # « The flag --auto-agree-with-licenses is not known ». + "sudo zypper --non-interactive install " + f"--auto-agree-with-licenses {de['zypper']} " + f"{rem['zypper']['packages']}; " "else echo 'Gestionnaire de paquets inconnu'; exit 1; fi; " # Le bureau ne sert à rien s'il ne démarre pas tout seul : les # images cloud démarrent en multi-user.target. @@ -4550,10 +4607,29 @@ class TODO: "--save /etc/pacman.d/mirrorlist || true; " "sudo pacman -Syu --noconfirm || true; " "sudo pacman -S --needed --noconfirm $PKGS; " + "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. + # Tumbleweed étant rolling, on rafraîchit avant d'installer. + + self._qemu_zypper_mirror_cmd() + + "sudo zypper --non-interactive refresh || true; " + # Tumbleweed est ROLLING et ne supporte pas les mises à jour + # partielles, exactement comme Arch. L'image cloud est un + # instantané figé : ses dépôts ont avancé depuis, et un + # « install » simple bute sur une incohérence — vécu, git 2.54 + # réclamait perl-Git bâti contre un perl-base plus ancien que + # celui de l'image. zypper proposait alors trois solutions et + # attendait un choix ; « --non-interactive » prend le défaut, + # « c » = annuler, et l'installation s'arrêtait là. + "sudo zypper --non-interactive dup --auto-agree-with-licenses " + "--allow-vendor-change || true; " + "sudo zypper --non-interactive install " + "--auto-agree-with-licenses $PKGS; " "elif command -v yum >/dev/null 2>&1; then " "sudo yum makecache -q || true; sudo yum install -y $PKGS; " "else echo 'Aucun gestionnaire de paquets " - "(apt/dnf/pacman/yum)'; exit 1; fi; " + "(apt/dnf/pacman/zypper/yum)'; exit 1; fi; " # Vérifie explicitement que tout est là : erreur nette plutôt # qu'un « command not found » cryptique plus loin. "for t in curl git make; do command -v $t >/dev/null 2>&1 || " diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 4c5ffde..8ad421b 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2055,6 +2055,10 @@ TRANSLATIONS = { "fr": "Installation du bureau GNOME (long)", "en": "Installing the GNOME desktop (long)", }, + "openSUSE mirror:": { + "fr": "miroir openSUSE :", + "en": "openSUSE mirror:", + }, "Remote desktop:": { "fr": "Bureau distant :", "en": "Remote desktop:",