Merge branch 'deploy_multi_os_and_qemu'
**Ubuntu 20.04 et 22.04 ne sont plus supportées**, sur toutes les architectures. ``` [UPD] release: deploy and run a fleet of VMs The QEMU deployment form stops treating the fleet as one machine: vCPU, RAM, disk, type, ERPLibre branch, Odoo version and timezone are set per VM, a lock shields one from the global profile, an entry can be renamed or duplicated. The install dashboard follows through -- update by parts, restart Odoo or delete a machine without leaving it. A graphical VM picks its desktop and its application store, and the SSH command to reach it is composed from ~/.ssh/config. Installation reaches four new grounds. openSUSE Leap and Tumbleweed, AlmaLinux, Rocky and Fedora 43 join the catalogue, and the s390x build chain works end to end on all of them: no wheel is published for that architecture, so everything compiles against distribution headers. mise lays down a precompiled CPython where pyenv built one, uv places the Python packages, and both fall back to the old path on failure. Ubuntu 20.04 and 22.04 are dropped -- pikepdf needs qpdf 12.2, built in C++20, while focal ships GCC 9. That is the only breaking change here. --- FR --- Le formulaire de déploiement QEMU cesse de traiter le parc comme une seule machine : vCPU, RAM, disque, type, branche ERPLibre, version d'Odoo et fuseau horaire se règlent par VM, un verrou en soustrait une au profil global, une entrée se renomme ou se duplique. Le tableau de bord suit — mettre à jour par parties, redémarrer Odoo ou supprimer une machine sans le quitter. Une VM graphique choisit son bureau et son magasin d'applications, et la commande SSH pour la joindre est composée depuis ~/.ssh/config. L'installation gagne quatre terrains. openSUSE Leap et Tumbleweed, AlmaLinux, Rocky et Fedora 43 rejoignent le catalogue, et la chaîne de compilation s390x y fonctionne de bout en bout : aucune roue n'est publiée pour cette architecture, donc tout se compile contre les en-têtes de la distribution. mise pose un CPython précompilé là où pyenv en compilait un, uv pose les paquets Python, et les deux retombent sur l'ancien chemin en cas d'échec. Ubuntu 20.04 et 22.04 partent — pikepdf exige qpdf 12.2, bâti en C++20, quand focal livre GCC 9. C'est le seul changement cassant du lot. Assisted-by: Claude Opus 5 ``` **Per-VM deployment.** vCPU, RAM, disk, VM type, ERPLibre branch, Odoo version and timezone are each set on the VM, all the way down to the remote command built per machine. A lock shields a VM from the global x1..x4 profile and shows on the whole row; an entry can be renamed or deployed in several copies. Presets are finer where development VMs actually live: every gigabyte and every vCPU up to 16. **Graphical VMs.** A server / desktop choice offering GNOME or Cinnamon, the application store picked per VM (.deb only, Flatpak tooling, or snap), and a composed SSH command to reach the remote desktop — read from `~/.ssh/config` so a nested VM is reachable through its ProxyJump, with the hypervisor console as a third route. **Install dashboard.** Acts on a VM without leaving it: update by parts — system packages, then git repositories, then Python dependencies, never chained so one failure cannot take the rest down — restart Odoo, or delete after a confirmation naming the disk. Architecture column, both-way scrolling and resizable columns. **Platforms.** openSUSE Leap 16.0 and Tumbleweed, AlmaLinux, Rocky, Fedora 43 and the CentOS-family fixes. The s390x build chain works end to end on Debian, EL9, EL10, Fedora and openSUSE. Ubuntu 20.04 and 22.04 are removed. **Toolchain.** `EL_PYTHON_PROVIDER` chooses mise or pyenv, `EL_PIP_PROVIDER` chooses uv or pip; each lives in a single library file, and each falls back on failure. Neither tool is ever installed on its own — `make install_mise` and `make install_uv` carry that decision. A Poetry dependency can be declined per architecture. **Déploiement par VM.** vCPU, RAM, disque, type de VM, branche ERPLibre, version d'Odoo et fuseau horaire se règlent chacun sur la VM, jusqu'à la commande distante bâtie par machine. Un verrou soustrait une VM au profil global x1..x4 et se voit à la ligne entière ; une entrée se renomme ou se déploie en plusieurs exemplaires. Les préréglages sont plus fins là où vivent les VM de développement : chaque gigaoctet et chaque vCPU jusqu'à 16. **VM graphiques.** Un choix serveur / bureau proposant GNOME ou Cinnamon, le magasin d'applications retenu par VM (.deb seul, outillage Flatpak, ou snap), et une commande SSH composée pour joindre le bureau distant — lue depuis `~/.ssh/config`, si bien qu'une VM imbriquée est atteignable par son ProxyJump, avec la console de l'hyperviseur comme troisième voie. **Tableau de bord d'installation.** Agit sur une VM sans la quitter : mise à jour par parties — paquets système, puis dépôts git, puis dépendances Python, jamais enchaînées pour qu'un échec n'emporte pas le reste — redémarrage d'Odoo, ou suppression après une confirmation nommant le disque. Colonne d'architecture, défilement dans les deux sens et colonnes ajustables. **Plateformes.** openSUSE Leap 16.0 et Tumbleweed, AlmaLinux, Rocky, Fedora 43 et les correctifs de la famille CentOS. La chaîne de compilation s390x fonctionne de bout en bout sur Debian, EL9, EL10, Fedora et openSUSE. Ubuntu 20.04 et 22.04 sont retirées. **Outillage.** `EL_PYTHON_PROVIDER` choisit mise ou pyenv, `EL_PIP_PROVIDER` choisit uv ou pip ; chacun vit dans un seul fichier de bibliothèque, et chacun retombe sur l'autre en cas d'échec. Aucun des deux n'est jamais installé de lui-même — `make install_mise` et `make install_uv` portent cette décision. Une dépendance Poetry peut être déclinée par architecture.
This commit is contained in:
commit
b63e99ad6c
46 changed files with 7410 additions and 1428 deletions
|
|
@ -6,4 +6,35 @@
|
|||
- **SSL** : Certbot pour les certificats
|
||||
- **DNS** : `script/deployment/update_dns_cloudflare.py`
|
||||
|
||||
Plateformes supportées : Ubuntu 20.04-25.04, Linux Mint 22.3, Debian 12, Arch Linux, macOS (pyenv), Windows (WSL/Docker).
|
||||
Plateformes supportées : Ubuntu 24.04 / 25.10 / 26.04, Linux Mint 22.3,
|
||||
Debian 12, AlmaLinux 9+, Rocky Linux 9+, openSUSE Leap 16 et Tumbleweed,
|
||||
Arch Linux,
|
||||
macOS (mise ou pyenv),
|
||||
Windows (WSL/Docker).
|
||||
|
||||
Ubuntu 20.04 et 22.04 sont abandonnées : pikepdf exige qpdf >= 12.2, compilé
|
||||
en C++20, quand focal livre GCC 9.
|
||||
|
||||
## Interpréteur Python
|
||||
|
||||
`EL_PYTHON_PROVIDER` (dans `env_var.sh`) vaut `auto`, `mise` ou `pyenv`.
|
||||
`auto` reprend un interpréteur DÉJÀ posé, quel que soit le fournisseur ; sinon
|
||||
il préfère mise, qui pose un CPython précompilé, et retombe sur pyenv, qui le
|
||||
compile.
|
||||
|
||||
Un seul fichier décide : `script/install/lib_python_provider.sh`. mise n'est
|
||||
jamais installé automatiquement — `make install_mise` porte cette décision.
|
||||
Pas de binaire mise pour s390x à ce jour : cette architecture reste sur pyenv.
|
||||
|
||||
## Paquets Python
|
||||
|
||||
`EL_PIP_PROVIDER` (dans `env_var.sh`) vaut `auto`, `uv` ou `pip`. `auto` prend
|
||||
uv s'il est présent et si le venv visé est en Python ≥ 3.8, sinon pip ; un
|
||||
échec d'uv retombe sur pip. Un seul fichier décide :
|
||||
`script/install/lib_pip_provider.sh`. uv n'est jamais installé
|
||||
automatiquement — `make install_uv` porte cette décision.
|
||||
|
||||
Portée réelle : le venv d'outils et l'amorçage de Poetry. **`poetry install`
|
||||
n'est pas concerné** — uv ne lit pas `poetry.lock` — et c'est pourtant l'étape
|
||||
qui domine. Sur s390x le gain est quasi nul : une trentaine de paquets sans
|
||||
roue se compilent, et uv n'enlève pas une seconde de gcc.
|
||||
|
|
|
|||
|
|
@ -57,9 +57,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- ERPLibre Home Mobile Application, use TODO to compile, deploy it and personalize it
|
||||
- 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,
|
||||
Arch) on amd64, arm64 and s390x, with a menu to list, test, resize, delete
|
||||
and clean up
|
||||
- Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, 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
|
||||
- Textual interfaces: install dashboard, VM deployment form, migration resume
|
||||
screen; Textual is installed on demand
|
||||
- Navigation telemetry for TODO, as a tree, a kanban or a list
|
||||
|
|
@ -89,6 +88,14 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- FAQ entry on wkhtmltopdf for recent distributions
|
||||
- brin_advisor and brin_cluster: recommend and apply the right PostgreSQL
|
||||
index for an Odoo model
|
||||
- Per-VM settings in the deployment form: vCPU, RAM, disk, type, ERPLibre branch, Odoo version and timezone, a lock that shields a VM from the global profile, renaming, and several copies of one entry
|
||||
- Python interpreter provider chosen through EL_PYTHON_PROVIDER — mise lays down a precompiled CPython, pyenv builds one — and picked per deployment
|
||||
- uv places the Python packages, pip as fallback, through EL_PIP_PROVIDER
|
||||
- openSUSE Leap 16.0 and Fedora 43 in the VM catalogue
|
||||
- Application store of a graphical VM: .deb only, Flatpak tooling, or snap
|
||||
- Remote desktop tunnel: the SSH command is composed from ~/.ssh/config, with the hypervisor console as a third route
|
||||
- The install dashboard acts on a VM without leaving it: update by parts, restart Odoo, delete; architecture column and resizable columns
|
||||
- A test checks that what the tooling imports is declared
|
||||
|
||||
<!-- [fr] -->
|
||||
|
||||
|
|
@ -108,9 +115,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- Application mobile ERPLibre Home, utiliser TODO pour compiler, déployer et personnaliser
|
||||
- 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, Arch) en amd64, arm64 et s390x, avec un menu pour lister,
|
||||
tester, redimensionner, supprimer et nettoyer
|
||||
- Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, 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
|
||||
- Interfaces Textual : tableau de bord d'installation, formulaire de
|
||||
déploiement de VM, écran de reprise de migration ; Textual s'installe à la
|
||||
demande
|
||||
|
|
@ -142,6 +148,26 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- Entrée de FAQ sur wkhtmltopdf pour les distributions récentes
|
||||
- brin_advisor et brin_cluster : recommander et appliquer le bon index
|
||||
PostgreSQL pour un modèle Odoo
|
||||
- Réglages par VM dans le formulaire de déploiement : vCPU, RAM, disque, type, branche ERPLibre, version d'Odoo et fuseau horaire, un verrou qui soustrait une VM au profil global, le renommage, et plusieurs exemplaires d'une entrée
|
||||
- Fournisseur d'interpréteur Python choisi par EL_PYTHON_PROVIDER — mise pose un CPython précompilé, pyenv en compile un — et retenu par déploiement
|
||||
- uv pose les paquets Python, pip en repli, par EL_PIP_PROVIDER
|
||||
- openSUSE Leap 16.0 et Fedora 43 au catalogue de VM
|
||||
- Magasin d'applications d'une VM graphique : .deb seul, outillage Flatpak, ou snap
|
||||
- Tunnel vers le bureau distant : la commande SSH est composée depuis ~/.ssh/config, avec la console de l'hyperviseur comme troisième voie
|
||||
- Le tableau de bord d'installation agit sur une VM sans la quitter : mise à jour par parties, redémarrage d'Odoo, suppression ; colonne d'architecture et colonnes ajustables
|
||||
- Un test vérifie que ce qu'importe l'outillage est bien déclaré
|
||||
|
||||
<!-- [en] -->
|
||||
## Removed
|
||||
<!-- [fr] -->
|
||||
## Retiré
|
||||
<!-- [en] -->
|
||||
|
||||
- Ubuntu 20.04 and 22.04 support, on every architecture: pikepdf needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and publishes no `g++-10` for s390x
|
||||
|
||||
<!-- [fr] -->
|
||||
|
||||
- Le support d'Ubuntu 20.04 et 22.04, sur toutes les architectures : pikepdf réclame qpdf 12.2, dont la compilation exige C++20, quand focal livre GCC 9 et ne publie pas de `g++-10` pour s390x
|
||||
|
||||
<!-- [en] -->
|
||||
## Changed
|
||||
|
|
@ -172,6 +198,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- LinuxMint 22.3 supported
|
||||
- Odoo 18 dependencies: flanker, orjson, python-magic, tldextract, PyYAML
|
||||
- Copyright year updated to 2026
|
||||
- Canadian pacman mirrors placed first on Arch, the official geographic mirror measuring four times slower from Montréal
|
||||
- A Poetry dependency can be declined per architecture: factur-x is pinned to 3.x on s390x, where saxonche publishes no wheel, and PyMuPDF is set aside there
|
||||
|
||||
<!-- [fr] -->
|
||||
|
||||
|
|
@ -198,6 +226,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- LinuxMint 22.3 pris en charge
|
||||
- Dépendances Odoo 18 : flanker, orjson, python-magic, tldextract, PyYAML
|
||||
- Année de copyright portée à 2026
|
||||
- Miroirs pacman canadiens placés en tête sur Arch, le miroir « géographique » officiel mesurant quatre fois plus lent depuis Montréal
|
||||
- Une dépendance Poetry peut être déclinée par architecture : factur-x est épinglé en 3.x sur s390x, où saxonche ne publie pas de roue, et PyMuPDF y est écarté
|
||||
|
||||
<!-- [en] -->
|
||||
## Fixed
|
||||
|
|
@ -215,6 +245,10 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- Installation on Debian 13, Fedora and Ubuntu 26.04: apt lock, wkhtmltopdf,
|
||||
SELinux and the missing C compiler
|
||||
- Documentation accents and the parallel markdown generation
|
||||
- The s390x build chain, on Debian as on EL9, EL10, Fedora and openSUSE: missing compilers and headers, Rust for cryptography and bcrypt, qpdf for pikepdf, libclang for pymupdf, package names that change across releases, and a batch that failed on one unknown name without ever naming it
|
||||
- A graphical desktop install froze for thirty minutes on a snap package that could not reach the store
|
||||
- The compiler was killed for lack of memory while building CPython on a small s390x guest
|
||||
- The README listed neither Fedora, openSUSE, Linux Mint nor Debian 13, all of them supported
|
||||
|
||||
<!-- [fr] -->
|
||||
|
||||
|
|
@ -228,6 +262,10 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- Installation sur Debian 13, Fedora et Ubuntu 26.04 : verrou apt, wkhtmltopdf,
|
||||
SELinux et le compilateur C manquant
|
||||
- Accents de la documentation et génération markdown en parallèle
|
||||
- La chaîne de compilation s390x, sur Debian comme sur EL9, EL10, Fedora et openSUSE : compilateurs et en-têtes manquants, Rust pour cryptography et bcrypt, qpdf pour pikepdf, libclang pour pymupdf, des noms de paquets qui changent d'une version à l'autre, et un lot en échec sur un seul nom inconnu sans jamais le nommer
|
||||
- L'installation d'un bureau graphique figeait trente minutes sur un paquet snap qui ne joignait pas le magasin
|
||||
- Le compilateur était tué faute de mémoire en bâtissant CPython sur une petite VM s390x
|
||||
- Le README ne listait ni Fedora, ni openSUSE, ni Linux Mint, ni Debian 13, toutes supportées
|
||||
|
||||
<!-- [en] -->
|
||||
## Security
|
||||
|
|
|
|||
|
|
@ -31,9 +31,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- Application mobile ERPLibre Home, utiliser TODO pour compiler, déployer et personnaliser
|
||||
- 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, Arch) en amd64, arm64 et s390x, avec un menu pour lister,
|
||||
tester, redimensionner, supprimer et nettoyer
|
||||
- Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, 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
|
||||
- Interfaces Textual : tableau de bord d'installation, formulaire de
|
||||
déploiement de VM, écran de reprise de migration ; Textual s'installe à la
|
||||
demande
|
||||
|
|
@ -65,6 +64,18 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- Entrée de FAQ sur wkhtmltopdf pour les distributions récentes
|
||||
- brin_advisor et brin_cluster : recommander et appliquer le bon index
|
||||
PostgreSQL pour un modèle Odoo
|
||||
- Réglages par VM dans le formulaire de déploiement : vCPU, RAM, disque, type, branche ERPLibre, version d'Odoo et fuseau horaire, un verrou qui soustrait une VM au profil global, le renommage, et plusieurs exemplaires d'une entrée
|
||||
- Fournisseur d'interpréteur Python choisi par EL_PYTHON_PROVIDER — mise pose un CPython précompilé, pyenv en compile un — et retenu par déploiement
|
||||
- uv pose les paquets Python, pip en repli, par EL_PIP_PROVIDER
|
||||
- openSUSE Leap 16.0 et Fedora 43 au catalogue de VM
|
||||
- Magasin d'applications d'une VM graphique : .deb seul, outillage Flatpak, ou snap
|
||||
- Tunnel vers le bureau distant : la commande SSH est composée depuis ~/.ssh/config, avec la console de l'hyperviseur comme troisième voie
|
||||
- Le tableau de bord d'installation agit sur une VM sans la quitter : mise à jour par parties, redémarrage d'Odoo, suppression ; colonne d'architecture et colonnes ajustables
|
||||
- Un test vérifie que ce qu'importe l'outillage est bien déclaré
|
||||
|
||||
## Retiré
|
||||
|
||||
- Le support d'Ubuntu 20.04 et 22.04, sur toutes les architectures : pikepdf réclame qpdf 12.2, dont la compilation exige C++20, quand focal livre GCC 9 et ne publie pas de `g++-10` pour s390x
|
||||
|
||||
## Modifié
|
||||
|
||||
|
|
@ -91,6 +102,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- LinuxMint 22.3 pris en charge
|
||||
- Dépendances Odoo 18 : flanker, orjson, python-magic, tldextract, PyYAML
|
||||
- Année de copyright portée à 2026
|
||||
- Miroirs pacman canadiens placés en tête sur Arch, le miroir « géographique » officiel mesurant quatre fois plus lent depuis Montréal
|
||||
- Une dépendance Poetry peut être déclinée par architecture : factur-x est épinglé en 3.x sur s390x, où saxonche ne publie pas de roue, et PyMuPDF y est écarté
|
||||
|
||||
## Corrigé
|
||||
|
||||
|
|
@ -104,6 +117,10 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
|
|||
- Installation sur Debian 13, Fedora et Ubuntu 26.04 : verrou apt, wkhtmltopdf,
|
||||
SELinux et le compilateur C manquant
|
||||
- Accents de la documentation et génération markdown en parallèle
|
||||
- La chaîne de compilation s390x, sur Debian comme sur EL9, EL10, Fedora et openSUSE : compilateurs et en-têtes manquants, Rust pour cryptography et bcrypt, qpdf pour pikepdf, libclang pour pymupdf, des noms de paquets qui changent d'une version à l'autre, et un lot en échec sur un seul nom inconnu sans jamais le nommer
|
||||
- L'installation d'un bureau graphique figeait trente minutes sur un paquet snap qui ne joignait pas le magasin
|
||||
- Le compilateur était tué faute de mémoire en bâtissant CPython sur une petite VM s390x
|
||||
- Le README ne listait ni Fedora, ni openSUSE, ni Linux Mint, ni Debian 13, toutes supportées
|
||||
|
||||
## Sécurité
|
||||
|
||||
|
|
|
|||
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -31,9 +31,8 @@ Recreating the virtual environment, use installation guide from tool `make`.
|
|||
- ERPLibre Home Mobile Application, use TODO to compile, deploy it and personalize it
|
||||
- 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,
|
||||
Arch) on amd64, arm64 and s390x, with a menu to list, test, resize, delete
|
||||
and clean up
|
||||
- Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, 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
|
||||
- Textual interfaces: install dashboard, VM deployment form, migration resume
|
||||
screen; Textual is installed on demand
|
||||
- Navigation telemetry for TODO, as a tree, a kanban or a list
|
||||
|
|
@ -63,6 +62,18 @@ Recreating the virtual environment, use installation guide from tool `make`.
|
|||
- FAQ entry on wkhtmltopdf for recent distributions
|
||||
- brin_advisor and brin_cluster: recommend and apply the right PostgreSQL
|
||||
index for an Odoo model
|
||||
- Per-VM settings in the deployment form: vCPU, RAM, disk, type, ERPLibre branch, Odoo version and timezone, a lock that shields a VM from the global profile, renaming, and several copies of one entry
|
||||
- Python interpreter provider chosen through EL_PYTHON_PROVIDER — mise lays down a precompiled CPython, pyenv builds one — and picked per deployment
|
||||
- uv places the Python packages, pip as fallback, through EL_PIP_PROVIDER
|
||||
- openSUSE Leap 16.0 and Fedora 43 in the VM catalogue
|
||||
- Application store of a graphical VM: .deb only, Flatpak tooling, or snap
|
||||
- Remote desktop tunnel: the SSH command is composed from ~/.ssh/config, with the hypervisor console as a third route
|
||||
- The install dashboard acts on a VM without leaving it: update by parts, restart Odoo, delete; architecture column and resizable columns
|
||||
- A test checks that what the tooling imports is declared
|
||||
|
||||
## Removed
|
||||
|
||||
- Ubuntu 20.04 and 22.04 support, on every architecture: pikepdf needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and publishes no `g++-10` for s390x
|
||||
|
||||
## Changed
|
||||
|
||||
|
|
@ -89,6 +100,8 @@ Recreating the virtual environment, use installation guide from tool `make`.
|
|||
- LinuxMint 22.3 supported
|
||||
- Odoo 18 dependencies: flanker, orjson, python-magic, tldextract, PyYAML
|
||||
- Copyright year updated to 2026
|
||||
- Canadian pacman mirrors placed first on Arch, the official geographic mirror measuring four times slower from Montréal
|
||||
- A Poetry dependency can be declined per architecture: factur-x is pinned to 3.x on s390x, where saxonche publishes no wheel, and PyMuPDF is set aside there
|
||||
|
||||
## Fixed
|
||||
|
||||
|
|
@ -102,6 +115,10 @@ Recreating the virtual environment, use installation guide from tool `make`.
|
|||
- Installation on Debian 13, Fedora and Ubuntu 26.04: apt lock, wkhtmltopdf,
|
||||
SELinux and the missing C compiler
|
||||
- Documentation accents and the parallel markdown generation
|
||||
- The s390x build chain, on Debian as on EL9, EL10, Fedora and openSUSE: missing compilers and headers, Rust for cryptography and bcrypt, qpdf for pikepdf, libclang for pymupdf, package names that change across releases, and a batch that failed on one unknown name without ever naming it
|
||||
- A graphical desktop install froze for thirty minutes on a snap package that could not reach the store
|
||||
- The compiler was killed for lack of memory while building CPython on a small s390x guest
|
||||
- The README listed neither Fedora, openSUSE, Linux Mint nor Debian 13, all of them supported
|
||||
|
||||
## Security
|
||||
|
||||
|
|
|
|||
41
Makefile
41
Makefile
|
|
@ -97,6 +97,47 @@ version:
|
|||
pyenv_update:
|
||||
~/.pyenv/bin/pyenv update
|
||||
|
||||
# mise pose un CPython PRECOMPILE la ou pyenv le compile : quelques secondes
|
||||
# contre une a trois minutes, et aucun paquet -dev requis. Volontairement
|
||||
# EXPLICITE : « curl | sh » engage, et ce n'est pas a un script d'installation
|
||||
# de le decider a la place de l'utilisateur. Une fois mise present,
|
||||
# EL_PYTHON_PROVIDER=auto s'en sert tout seul.
|
||||
#
|
||||
# Pas de binaire s390x publie a ce jour : sur cette architecture, la cible
|
||||
# le dit et n'installe rien.
|
||||
# uv installe les paquets Python nettement plus vite que pip, et met en cache
|
||||
# les roues qu'il CONSTRUIT — ce qui compte la ou rien n'a de roue publiee.
|
||||
# Contrairement a mise, uv publie une roue s390x : un « pip install uv » dans
|
||||
# le venv d'outils suffit, sans « curl | sh ». Reste EXPLICITE quand meme,
|
||||
# comme install_mise : EL_PIP_PROVIDER=auto s'en sert des qu'il est la.
|
||||
#
|
||||
# Attention a l'attente : « poetry install », qui domine le temps
|
||||
# d'installation, n'est PAS accelere — uv ne lit pas poetry.lock.
|
||||
.PHONY: install_uv
|
||||
install_uv:
|
||||
@if command -v uv >/dev/null 2>&1; then \
|
||||
echo "uv deja present : $$(uv --version)"; \
|
||||
else \
|
||||
echo "Installation de uv dans le venv d outils"; \
|
||||
./$$(cat conf/python-erplibre-venv | xargs)/bin/pip install uv; \
|
||||
echo "uv : ./$$(cat conf/python-erplibre-venv | xargs)/bin/uv"; \
|
||||
echo "Ajoutez ce repertoire au PATH, ou installez uv globalement."; \
|
||||
fi
|
||||
|
||||
.PHONY: install_mise
|
||||
install_mise:
|
||||
@if command -v mise >/dev/null 2>&1; then \
|
||||
echo "mise deja present : $$(mise --version)"; \
|
||||
elif command -v pacman >/dev/null 2>&1; then \
|
||||
sudo pacman -S --needed --noconfirm mise; \
|
||||
elif [ "$$(uname -m)" = "s390x" ]; then \
|
||||
echo "mise ne publie pas de binaire s390x : on reste sur pyenv."; \
|
||||
else \
|
||||
echo "Installation de mise depuis https://mise.run"; \
|
||||
curl -fsSL https://mise.run | sh; \
|
||||
echo "Ajoutez ~/.local/bin a votre PATH, puis relancez l installation."; \
|
||||
fi
|
||||
|
||||
.PHONY: db_create_db_test
|
||||
db_create_db_test:
|
||||
./script/make.sh db_drop_db_test
|
||||
|
|
|
|||
|
|
@ -94,19 +94,31 @@ Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc.
|
|||
|
||||
# Supported platforms
|
||||
|
||||
- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux
|
||||
- **macOS** : via pyenv
|
||||
- **Windows** : via WSL or Docker
|
||||
- **Mainframe** : 390x architecture
|
||||
- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 and 22.04 are dropped, pikepdf requiring a qpdf 12.2 built in C++20
|
||||
- **Linux Mint** : 22.3
|
||||
- **Debian** : 12 (bookworm) and 13 (trixie)
|
||||
- **Fedora** : 41 and later
|
||||
- **AlmaLinux, Rocky Linux** : 9 and 10 — RHEL and CentOS Stream take the same path
|
||||
- **openSUSE** : Leap 16.0 and Tumbleweed
|
||||
- **Arch Linux** : rolling release
|
||||
- **macOS** : through mise or pyenv
|
||||
- **Windows** : through WSL or Docker
|
||||
- **Architectures** : amd64, arm64 and s390x (IBM Z mainframe)
|
||||
|
||||
<!-- [fr] -->
|
||||
|
||||
# Plateformes supportées
|
||||
|
||||
- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux
|
||||
- **macOS** : via pyenv
|
||||
- **Windows** : via WSL ou Docker
|
||||
- **Mainframe** : architecture 390x
|
||||
- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 et 22.04 sont retirées, pikepdf réclamant un qpdf 12.2 bâti en C++20
|
||||
- **Linux Mint** : 22.3
|
||||
- **Debian** : 12 (bookworm) et 13 (trixie)
|
||||
- **Fedora** : 41 et suivantes
|
||||
- **AlmaLinux, Rocky Linux** : 9 et 10 — RHEL et CentOS Stream empruntent le même chemin
|
||||
- **openSUSE** : Leap 16.0 et Tumbleweed
|
||||
- **Arch Linux** : rolling release
|
||||
- **macOS** : par mise ou pyenv
|
||||
- **Windows** : par WSL ou Docker
|
||||
- **Architectures** : amd64, arm64 et s390x (mainframe IBM Z)
|
||||
|
||||
<!-- [en] -->
|
||||
|
||||
|
|
@ -276,11 +288,11 @@ cd ERPLibre
|
|||
|
||||
<!-- [en] -->
|
||||
|
||||
Support Ubuntu 20.04, 22.04, 24.04, 25.04 and OSX.
|
||||
`make install_os` detects the distribution and picks the right dependency script: apt for Ubuntu, Linux Mint and Debian, dnf for Fedora and the RHEL family, zypper for openSUSE, pacman for Arch. See the supported platforms above.
|
||||
|
||||
<!-- [fr] -->
|
||||
|
||||
Support Ubuntu 20.04, 22.04, 24.04, 25.04 et OSX.
|
||||
`make install_os` détecte la distribution et choisit le bon script de dépendances : apt pour Ubuntu, Linux Mint et Debian, dnf pour Fedora et la famille RHEL, zypper pour openSUSE, pacman pour Arch. Voir les plateformes supportées ci-dessus.
|
||||
|
||||
<!-- [common] -->
|
||||
|
||||
|
|
|
|||
16
README.fr.md
16
README.fr.md
|
|
@ -47,10 +47,16 @@ Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc.
|
|||
|
||||
# Plateformes supportées
|
||||
|
||||
- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux
|
||||
- **macOS** : via pyenv
|
||||
- **Windows** : via WSL ou Docker
|
||||
- **Mainframe** : architecture 390x
|
||||
- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 et 22.04 sont retirées, pikepdf réclamant un qpdf 12.2 bâti en C++20
|
||||
- **Linux Mint** : 22.3
|
||||
- **Debian** : 12 (bookworm) et 13 (trixie)
|
||||
- **Fedora** : 41 et suivantes
|
||||
- **AlmaLinux, Rocky Linux** : 9 et 10 — RHEL et CentOS Stream empruntent le même chemin
|
||||
- **openSUSE** : Leap 16.0 et Tumbleweed
|
||||
- **Arch Linux** : rolling release
|
||||
- **macOS** : par mise ou pyenv
|
||||
- **Windows** : par WSL ou Docker
|
||||
- **Architectures** : amd64, arm64 et s390x (mainframe IBM Z)
|
||||
|
||||
|
||||
# Installation en production
|
||||
|
|
@ -140,7 +146,7 @@ cd ERPLibre
|
|||
```
|
||||
|
||||
|
||||
Support Ubuntu 20.04, 22.04, 24.04, 25.04 et OSX.
|
||||
`make install_os` détecte la distribution et choisit le bon script de dépendances : apt pour Ubuntu, Linux Mint et Debian, dnf pour Fedora et la famille RHEL, zypper pour openSUSE, pacman pour Arch. Voir les plateformes supportées ci-dessus.
|
||||
|
||||
|
||||
```bash
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -46,10 +46,16 @@ Switch between versions with `make switch_odoo_18`, `make switch_odoo_16`, etc.
|
|||
|
||||
# Supported platforms
|
||||
|
||||
- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux
|
||||
- **macOS** : via pyenv
|
||||
- **Windows** : via WSL or Docker
|
||||
- **Mainframe** : 390x architecture
|
||||
- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 and 22.04 are dropped, pikepdf requiring a qpdf 12.2 built in C++20
|
||||
- **Linux Mint** : 22.3
|
||||
- **Debian** : 12 (bookworm) and 13 (trixie)
|
||||
- **Fedora** : 41 and later
|
||||
- **AlmaLinux, Rocky Linux** : 9 and 10 — RHEL and CentOS Stream take the same path
|
||||
- **openSUSE** : Leap 16.0 and Tumbleweed
|
||||
- **Arch Linux** : rolling release
|
||||
- **macOS** : through mise or pyenv
|
||||
- **Windows** : through WSL or Docker
|
||||
- **Architectures** : amd64, arm64 and s390x (IBM Z mainframe)
|
||||
|
||||
|
||||
# Installation in production
|
||||
|
|
@ -139,7 +145,7 @@ cd ERPLibre
|
|||
```
|
||||
|
||||
|
||||
Support Ubuntu 20.04, 22.04, 24.04, 25.04 and OSX.
|
||||
`make install_os` detects the distribution and picks the right dependency script: apt for Ubuntu, Linux Mint and Debian, dnf for Fedora and the RHEL family, zypper for openSUSE, pacman for Arch. See the supported platforms above.
|
||||
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -80,14 +80,14 @@ sudo ln -s /snap/bin/certbot /usr/bin/certbot
|
|||
<!-- [en] -->
|
||||
A service is running by SystemD. You can access it with the DNS name found in `env_var.sh`
|
||||
|
||||
#### Ubuntu 20.04 server
|
||||
#### Ubuntu 24.04 server
|
||||
|
||||
Apply fix libpng12-0: https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
||||
|
||||
<!-- [fr] -->
|
||||
Un service est en cours d'exécution via SystemD. Vous pouvez y accéder avec le nom DNS trouvé dans `env_var.sh`
|
||||
|
||||
#### Serveur Ubuntu 20.04
|
||||
#### Serveur Ubuntu 24.04
|
||||
|
||||
Appliquer le correctif libpng12-0 : https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ sudo ln -s /snap/bin/certbot /usr/bin/certbot
|
|||
|
||||
Un service est en cours d'exécution via SystemD. Vous pouvez y accéder avec le nom DNS trouvé dans `env_var.sh`
|
||||
|
||||
#### Serveur Ubuntu 20.04
|
||||
#### Serveur Ubuntu 24.04
|
||||
|
||||
Appliquer le correctif libpng12-0 : https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ sudo ln -s /snap/bin/certbot /usr/bin/certbot
|
|||
|
||||
A service is running by SystemD. You can access it with the DNS name found in `env_var.sh`
|
||||
|
||||
#### Ubuntu 20.04 server
|
||||
#### Ubuntu 24.04 server
|
||||
|
||||
Apply fix libpng12-0: https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
A guide on how to set up a workspace and run ERPLibre on Windows 10 version 2004 and up or Windows 11. There are two methods of installation, one is automatic and the other manual.
|
||||
|
||||
**"WSL2 Ubuntu 22.04" will be referred as "WSL2"**
|
||||
**"WSL2 Ubuntu 24.04" will be referred as "WSL2"**
|
||||
|
||||
**"PyCharm Professional" will be referred as "PyCharm"**
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ Run Powershell with administrator rights and run the following command:
|
|||
|
||||
Un guide pour configurer un espace de travail et exécuter ERPLibre sur Windows 10 version 2004 et plus ou Windows 11. Il existe deux méthodes d'installation, une automatique et l'autre manuelle.
|
||||
|
||||
**"WSL2 Ubuntu 22.04" sera désigné par "WSL2"**
|
||||
**"WSL2 Ubuntu 24.04" sera désigné par "WSL2"**
|
||||
|
||||
**"PyCharm Professional" sera désigné par "PyCharm"**
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ Exécutez Powershell avec les droits administrateur et lancez la commande suivan
|
|||
|
||||
<!-- [common] -->
|
||||
```bash
|
||||
wsl --install -d Ubuntu-22.04
|
||||
wsl --install -d Ubuntu-24.04
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
|
|
@ -406,12 +406,12 @@ https://www.jetbrains.com/pycharm/download/#section=windows
|
|||
<!-- [en] -->
|
||||
### Set up Pycharm
|
||||
|
||||
Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-22.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`.
|
||||
Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-24.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`.
|
||||
|
||||
<!-- [fr] -->
|
||||
### Configurer Pycharm
|
||||
|
||||
Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-22.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`.
|
||||
Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-24.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`.
|
||||
|
||||
<!-- [common] -->
|
||||

|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
Un guide pour configurer un espace de travail et exécuter ERPLibre sur Windows 10 version 2004 et plus ou Windows 11. Il existe deux méthodes d'installation, une automatique et l'autre manuelle.
|
||||
|
||||
**"WSL2 Ubuntu 22.04" sera désigné par "WSL2"**
|
||||
**"WSL2 Ubuntu 24.04" sera désigné par "WSL2"**
|
||||
|
||||
**"PyCharm Professional" sera désigné par "PyCharm"**
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ Un guide pour configurer un espace de travail et exécuter ERPLibre sur Windows
|
|||
Exécutez Powershell avec les droits administrateur et lancez la commande suivante :
|
||||
|
||||
```bash
|
||||
wsl --install -d Ubuntu-22.04
|
||||
wsl --install -d Ubuntu-24.04
|
||||
```
|
||||
|
||||
Si vous avez des difficultés à ouvrir Powershell avec les droits administrateur, appuyez sur `Windows + R`, entrez la ligne suivante et appuyez sur `OK`. Les droits administrateur vous seront automatiquement demandés.
|
||||
|
|
@ -220,7 +220,7 @@ https://www.jetbrains.com/pycharm/download/#section=windows
|
|||
|
||||
### Configurer Pycharm
|
||||
|
||||
Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-22.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`.
|
||||
Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-24.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
A guide on how to set up a workspace and run ERPLibre on Windows 10 version 2004 and up or Windows 11. There are two methods of installation, one is automatic and the other manual.
|
||||
|
||||
**"WSL2 Ubuntu 22.04" will be referred as "WSL2"**
|
||||
**"WSL2 Ubuntu 24.04" will be referred as "WSL2"**
|
||||
|
||||
**"PyCharm Professional" will be referred as "PyCharm"**
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ A guide on how to set up a workspace and run ERPLibre on Windows 10 version 2004
|
|||
Run Powershell with administrator rights and run the following command:
|
||||
|
||||
```bash
|
||||
wsl --install -d Ubuntu-22.04
|
||||
wsl --install -d Ubuntu-24.04
|
||||
```
|
||||
|
||||
If you have trouble opening the Powershell with administrator rights, press `Windows + R`, enter the following line and press `OK`. You will be automatically prompted for administrator rights.
|
||||
|
|
@ -220,7 +220,7 @@ https://www.jetbrains.com/pycharm/download/#section=windows
|
|||
|
||||
### Set up Pycharm
|
||||
|
||||
Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-22.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`.
|
||||
Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-24.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,11 @@ ENV ODOO_RC /etc/odoo/odoo.conf
|
|||
|
||||
COPY wait-for-psql.py /usr/local/bin/wait-for-psql.py
|
||||
|
||||
RUN chmod +X /usr/local/bin/wait-for-psql.py
|
||||
# « +X » en majuscule n'ajoute le bit que si l'un des trois est DÉJÀ posé,
|
||||
# ou sur un répertoire. Sur le fichier que COPY vient de déposer en 644, il
|
||||
# ne faisait rien : le script restait non exécutable dans l'image, malgré
|
||||
# sa place dans /usr/local/bin.
|
||||
RUN chmod +x /usr/local/bin/wait-for-psql.py
|
||||
|
||||
# Set default user when running the container
|
||||
USER odoo
|
||||
|
|
|
|||
11
env_var.sh
11
env_var.sh
|
|
@ -14,6 +14,17 @@ EL_POETRY_VERSION=$(cat ".poetry-version" | xargs)
|
|||
EL_PYTHON_ODOO_VERSION=$(cat ".python-odoo-version" | xargs)
|
||||
EL_PYTHON_ERPLIBRE_VERSION=$(cat "./conf/python-erplibre-version" | xargs)
|
||||
EL_ERPLIBRE_VERSION=$(cat ".erplibre-version" | xargs)
|
||||
# Fournisseur d'interpreteur Python : auto | mise | pyenv.
|
||||
# « auto » prend ce qui est DEJA pose, sinon mise s'il est present (Python
|
||||
# precompile, quelques secondes), sinon pyenv (compilation). Voir
|
||||
# script/install/lib_python_provider.sh.
|
||||
EL_PYTHON_PROVIDER="${EL_PYTHON_PROVIDER:-auto}"
|
||||
# Installateur de paquets Python : auto | uv | pip.
|
||||
# « auto » prend uv s'il est present et si le venv vise est en Python >= 3.8,
|
||||
# sinon pip ; un echec d'uv retombe sur pip. Voir
|
||||
# script/install/lib_pip_provider.sh. Note : « poetry install » n'est PAS
|
||||
# concerne, uv ne lit pas poetry.lock.
|
||||
EL_PIP_PROVIDER="${EL_PIP_PROVIDER:-auto}"
|
||||
# The default port where this Odoo instance will run under (provided you use the command -c in the terminal)
|
||||
# Set to true if you want to install it, false if you don't need it or have it already installed.
|
||||
EL_INSTALL_WKHTMLTOPDF="True"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -21,11 +21,12 @@ authors = [ "Mathieu Benoit <mathben@technolibre.ca>",]
|
|||
python = ">=3.12.10,<3.13"
|
||||
a2wsgi = ">=1.10.6"
|
||||
aiobotocore = "3.1.2"
|
||||
altcha = ">=2.0.0"
|
||||
ansi2html = "^1.9.2"
|
||||
apispec = "^6.10.0"
|
||||
asn1crypto = "1.5.1"
|
||||
astor = "^0.8.1"
|
||||
avalara = "^26.4.0"
|
||||
avalara = "^26.7.3"
|
||||
babel = "2.10.3"
|
||||
bandit = "^1.9.4"
|
||||
base64io = "^1.0.3"
|
||||
|
|
@ -33,7 +34,7 @@ beautifulsoup4 = "4.13.5"
|
|||
bokeh = "3.6.3"
|
||||
boto3 = "1.42.42"
|
||||
botocore = "1.42.42"
|
||||
cachetools = "^7.1.1"
|
||||
cachetools = "^7.1.7"
|
||||
cairosvg = "^2.9.0"
|
||||
caldav = "1.3.9"
|
||||
cbor2 = "5.6.2"
|
||||
|
|
@ -43,21 +44,20 @@ code-writer = "^1.3.0"
|
|||
colorama = "^0.4.6"
|
||||
contextvars = "^2.4"
|
||||
cryptography = "46.0.5"
|
||||
cssselect = "^1.4.0"
|
||||
cython = "^3.2.4"
|
||||
dateparser = "^1.4.0"
|
||||
cssselect = "^1.5.0"
|
||||
cython = "^3.2.9"
|
||||
dateparser = "^1.4.2"
|
||||
decorator = "5.1.1"
|
||||
dnspython = "^2.8.0"
|
||||
docutils = "0.20.1"
|
||||
dropbox = "^12.0.2"
|
||||
dropbox = "^12.2.1"
|
||||
easypost = "7.15.0"
|
||||
email-validator = "^2.3.0"
|
||||
extendable = ">=0.0.4"
|
||||
extendable-pydantic = ">=1.2.0"
|
||||
extract-msg = "^0.55.0"
|
||||
extract-msg = "^0.56.0"
|
||||
extruct = "^0.18.0"
|
||||
ezdxf = "^1.4.3"
|
||||
factur-x = "^4.2"
|
||||
ezdxf = "^1.4.4"
|
||||
fastapi = ">=0.110.0"
|
||||
filetype = "^1.2.0"
|
||||
flake8 = "^7.3.0"
|
||||
|
|
@ -65,9 +65,9 @@ formio-data = "^2.1.7"
|
|||
freezegun = "1.2.1"
|
||||
geocoder = "^1.38.1"
|
||||
geoip2 = "2.9.0"
|
||||
geojson = "^3.2.0"
|
||||
geojson = "^3.3.0"
|
||||
gevent = "24.2.1"
|
||||
gitpython = "^3.1.50"
|
||||
gitpython = "^3.1.59"
|
||||
giturlparse = "0.12.0"
|
||||
googletrans-py = "4.0.0"
|
||||
greenlet = "3.0.3"
|
||||
|
|
@ -76,36 +76,38 @@ icalendar = "5.0.13"
|
|||
idna = "3.6"
|
||||
img2pdf = "^0.6.3"
|
||||
isort = "7.0.0"
|
||||
itsdangerous = "^2.2.0"
|
||||
jinja2 = "3.1.2"
|
||||
js2py = "^0.74"
|
||||
libsass = "0.22.0"
|
||||
lottie = "^0.7.2"
|
||||
lxml = "5.2.1"
|
||||
lxml-html-clean = "^0.4.4"
|
||||
mako = "^1.3.12"
|
||||
lxml-html-clean = "<0.4.5"
|
||||
mako = "^1.4.1"
|
||||
markdown2 = "2.5.1"
|
||||
markdownify = "0.13.1"
|
||||
markupsafe = "2.1.5"
|
||||
matplotlib = "^3.10.9"
|
||||
matplotlib = "^3.11.1"
|
||||
meteostat = "1.6.8"
|
||||
mock = "^5.2.0"
|
||||
mpld3 = "0.5.10"
|
||||
msgraphfs = "^0.4"
|
||||
msgraphfs = "^0.5"
|
||||
mysqlclient = "^2.2.8"
|
||||
nextcloud-api-wrapper = "^0.2.3"
|
||||
num2words = "0.5.13"
|
||||
numpy = ">=1.15"
|
||||
numpy-financial = "<=1.0.0"
|
||||
numpy-stl = "^3.2.0"
|
||||
numpy-stl = "^4.0.0"
|
||||
oauthlib = "^3.3.1"
|
||||
odoo-test-helper = "^2.1.3"
|
||||
odoorpc = "^0.10.1"
|
||||
ofxparse = "0.21"
|
||||
ollama = "^0.6.2"
|
||||
openai = "^2.34.0"
|
||||
openai = "^2.53.0"
|
||||
openpyxl = "3.1.2"
|
||||
openupgradelib = "^3.13.1"
|
||||
openupgradelib = "^3.13.6"
|
||||
openwebui-client = ">=0.3.0"
|
||||
orjson = "^3.11.8"
|
||||
orjson = "^3.11.9"
|
||||
ovh = "^1.2.0"
|
||||
pandas = "2.3.3"
|
||||
paramiko = "<4.0.0"
|
||||
|
|
@ -115,14 +117,14 @@ pathspec = "^1.1.1"
|
|||
pdf2image = "^1.17.0"
|
||||
pdfminer-six = "20260107"
|
||||
pexpect = "^4.9.0"
|
||||
phonenumbers = "^9.0.29"
|
||||
phonenumbers = "^9.0.36"
|
||||
pillow = "12.2.0"
|
||||
plaid-python = "^39.2.0"
|
||||
plotly = "^6.7.0"
|
||||
plaid-python = "^42.0.0"
|
||||
plotly = "^6.9.0"
|
||||
polib = "1.1.1"
|
||||
pre-commit = "^4.6.0"
|
||||
pre-commit = "^4.6.2"
|
||||
premailer = "^3.10.0"
|
||||
prometheus-client = "^0.25.0"
|
||||
prometheus-client = "^0.26.0"
|
||||
psutil = "5.9.8"
|
||||
psycopg2 = "2.9.9"
|
||||
py3dns = "^4.0.2"
|
||||
|
|
@ -135,10 +137,9 @@ pydevd-odoo = "^1.2.2"
|
|||
pyftpdlib = "^2.2.0"
|
||||
pygithub = "^2.9.1"
|
||||
pyjsparser = "^2.7.1"
|
||||
pyjwt = "^2.12.1"
|
||||
pylint = "^4.0.5"
|
||||
pylint-odoo = "^10.0.2"
|
||||
pymupdf = "^1.27.2.3"
|
||||
pyjwt = "^2.13.0"
|
||||
pylint = "^4.0.7"
|
||||
pylint-odoo = "^10.0.9"
|
||||
pyncclient = "^0.7"
|
||||
pyopenssl = "26.0.0"
|
||||
pypdf = ">=3.1.0"
|
||||
|
|
@ -154,13 +155,13 @@ python-dotenv = ">=1.0.0"
|
|||
python-jose = "^3.5.0"
|
||||
python-json-logger = "^4.1.0"
|
||||
python-ldap = "3.4.4"
|
||||
python-multipart = "^0.0.27"
|
||||
python-multipart = "^0.0.32"
|
||||
python-pptx = "^1.0.2"
|
||||
python-resize-image = "^1.1.20"
|
||||
python-slugify = "^8.0.4"
|
||||
python-stdnum = "1.19"
|
||||
python-telegram-bot = "^22.7"
|
||||
pytz = "^2026.2"
|
||||
python-telegram-bot = "^22.8"
|
||||
pytz = "^2026.3.post1"
|
||||
pyusb = "1.2.1"
|
||||
pyyaml = "^6.0.3"
|
||||
pyzbar = "^0.1.9"
|
||||
|
|
@ -168,27 +169,28 @@ pyzk = "^0.9"
|
|||
qifparse = "^0.5"
|
||||
qrcode = "7.4.2"
|
||||
radon = "^6.0.1"
|
||||
redis = "^7.4.0"
|
||||
regex = "^2026.4.4"
|
||||
redis = "^8.1.0"
|
||||
regex = "^2026.7.19"
|
||||
reportlab = "4.1.0"
|
||||
requests = ">=2.31.0"
|
||||
requests = "2.31.0"
|
||||
requests-oauthlib = "^2.0.0"
|
||||
requests-toolbelt = "^1.0.0"
|
||||
responses = "^0.26.0"
|
||||
responses = "^0.26.2"
|
||||
rjsmin = "1.2.0"
|
||||
roulier = "^1.1.1"
|
||||
s3fs = "2026.4.0"
|
||||
s3fs = "2026.7.0"
|
||||
schwifty = "2024.4.0"
|
||||
sentry-sdk = ">=2.0.0,<=2.22.0"
|
||||
setuptools = "80.10.2"
|
||||
shapely = "^2.1.2"
|
||||
sqlalchemy = "^2.0.49"
|
||||
sqlalchemy = "^2.0.51"
|
||||
statsd = "^4.0.1"
|
||||
tldextract = "^5.3.1"
|
||||
tweepy = "^4.16.0"
|
||||
twilio = "^9.10.5"
|
||||
typing-extensions = "^4.15.0"
|
||||
ujson = "^5.12.1"
|
||||
tldextract = "^5.3.2"
|
||||
to-3mf = "^0.1.0"
|
||||
tweepy = "^4.17.0"
|
||||
twilio = "^9.10.9"
|
||||
typing-extensions = "^4.16.0"
|
||||
ujson = "^5.13.0"
|
||||
unidecode = "^1.4.0"
|
||||
urllib3 = "2.0.7"
|
||||
uvloop = "^0.22.1"
|
||||
|
|
@ -204,6 +206,18 @@ xlwt = "1.3.0"
|
|||
xmltodict = "0.13.0"
|
||||
zeep = "4.2.1"
|
||||
|
||||
[[tool.poetry.dependencies.pymupdf]]
|
||||
version = "^1.28.2"
|
||||
markers = "platform_machine != \"s390x\""
|
||||
|
||||
[[tool.poetry.dependencies.factur-x]]
|
||||
version = ">=4.2"
|
||||
markers = "platform_machine != \"s390x\""
|
||||
|
||||
[[tool.poetry.dependencies.factur-x]]
|
||||
version = "<4"
|
||||
markers = "platform_machine == \"s390x\""
|
||||
|
||||
[tool.poetry.dependencies.black]
|
||||
git = "https://github.com/psf/black.git"
|
||||
rev = "24.8.0"
|
||||
|
|
@ -214,7 +228,7 @@ rev = "update_imghdr_python3"
|
|||
|
||||
[tool.poetry.dependencies.fsspec]
|
||||
extras = [ "s3",]
|
||||
version = "^2026.4.0"
|
||||
version = "^2026.7.0"
|
||||
|
||||
[tool.poetry.dependencies.pysaml2]
|
||||
git = "https://github.com/prauscher/pysaml2.git"
|
||||
|
|
|
|||
|
|
@ -78,7 +78,26 @@ git+https://github.com/prauscher/pysaml2.git@replace-pyopenssl
|
|||
|
||||
pandas==2.3.3
|
||||
pillow==12.2.0
|
||||
s3fs==2026.4.0
|
||||
# OCA_storage amène « fsspec[s3] » sans borne : l'extra s3 de fsspec 2026.7.0
|
||||
# exige s3fs>=2026.6.0, incompatible avec l'épingle 2026.4.0. On aligne s3fs sur
|
||||
# fsspec ; aiobotocore 3.1.2 reste dans la plage exigée (>=2.19.0,<4.0.0), donc
|
||||
# le trio botocore/boto3 n'a pas à bouger.
|
||||
s3fs==2026.7.0
|
||||
aiobotocore==3.1.2
|
||||
botocore==1.42.42
|
||||
boto3==1.42.42
|
||||
|
||||
# factur-x >= 4.0 dépend de saxonche, qui encapsule SaxonC. Saxonica ne publie
|
||||
# de roue que pour x86_64, aarch64, macOS et Windows — aucune pour s390x, et
|
||||
# aucune distribution source : rien à installer, rien à compiler. La cause est
|
||||
# en amont, GraalVM Native Image ne ciblant pas s390x.
|
||||
# La 3.x n'exige que lxml et pypdf. On y retombe là où saxonche n'existe pas,
|
||||
# au prix de la validation Schematron.
|
||||
factur-x>=4.2 ; platform_machine != 's390x'
|
||||
factur-x<4 ; platform_machine == 's390x'
|
||||
|
||||
# lxml-html-clean 0.4.5 (2026-05-20) exige lxml>=6.1.1, alors qu'Odoo 18 épingle
|
||||
# lxml==5.2.1 : le solveur poetry n'a plus de solution. Les versions jusqu'à
|
||||
# 0.4.4 n'imposent aucune borne sur lxml — vérifié sur PyPI, release par
|
||||
# release. À lever le jour où lxml montera.
|
||||
lxml-html-clean<0.4.5
|
||||
|
|
|
|||
|
|
@ -16,6 +16,21 @@ EL_USER=${USER}
|
|||
# pacman résilient : --needed saute ce qui est déjà là, --noconfirm en non
|
||||
# interactif.
|
||||
PAC="sudo pacman -S --needed --noconfirm"
|
||||
# Miroirs canadiens EN TÊTE de la mirrorlist. Mesuré depuis Montreal sur
|
||||
# extra.db : quantum5 2,0 s, xenyth 7,1 s, contre 8,0 s pour
|
||||
# geo.mirror.pkgbuild.com — le miroir « geographique » officiel n'est donc pas
|
||||
# le meilleur ici. Les deux ne servent que x86_64 (Arch Linux ARM a ses propres
|
||||
# miroirs), d'où la garde. On les AJOUTE en tete sans effacer l'existant : les
|
||||
# miroirs deja configures restent en repli.
|
||||
if [ "$(uname -m)" = "x86_64" ] \
|
||||
&& ! grep -q "mirror.quantum5.ca" /etc/pacman.d/mirrorlist 2> /dev/null; then
|
||||
echo -e "\n---- Miroirs pacman canadiens ----"
|
||||
printf 'Server = https://mirror.quantum5.ca/archlinux/$repo/os/$arch\nServer = https://mirror.xenyth.net/archlinux/$repo/os/$arch\n' \
|
||||
| sudo tee /etc/pacman.d/mirrorlist.el > /dev/null
|
||||
sudo sh -c 'cat /etc/pacman.d/mirrorlist >> /etc/pacman.d/mirrorlist.el \
|
||||
&& mv /etc/pacman.d/mirrorlist.el /etc/pacman.d/mirrorlist'
|
||||
fi
|
||||
|
||||
# Mise à jour COMPLÈTE obligatoire : Arch est en rolling release et NE SUPPORTE
|
||||
# PAS les mises à jour partielles. Sur une image cloud dont la glibc est
|
||||
# ancienne, un « pacman -S <paquet récent> » lie le binaire à une glibc plus
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
. ./env_var.sh
|
||||
. ./script/install/lib_qpdf.sh
|
||||
. ./script/install/lib_lowmem.sh
|
||||
|
||||
EL_USER=${USER}
|
||||
#EL_INSTALL_WKHTMLTOPDF="True"
|
||||
|
|
@ -19,22 +21,35 @@ APT_GET="sudo apt-get -o DPkg::Lock::Timeout=600"
|
|||
UBUNTU_VERSION=$(lsb_release -rs)
|
||||
DEBIAN_VERSION=$(lsb_release -cs)
|
||||
OS=$(lsb_release -si)
|
||||
|
||||
# Ubuntu 18.04, 20.04 et 22.04 ne sont plus supportées, sur AUCUNE
|
||||
# architecture. Le mur le plus net est pikepdf, qui réclame qpdf >= 12.2,
|
||||
# lui-même en C++20 : focal livre GCC 9 et ne publie même pas de g++-10 pour
|
||||
# s390x. S'y ajoutaient Python 3.8 à l'amorçage, node 10, cargo 0.67 et
|
||||
# OpenSSL 1.1.1 — chacun avait son contournement, l'accumulation non.
|
||||
#
|
||||
# Le refus est ICI, avant tout le reste : ce script tourne aussi sur une
|
||||
# machine existante, pas seulement sur une VM fraîchement déployée.
|
||||
if [[ "${OS}" == "Ubuntu" ]]; then
|
||||
if [ "20.04" == "${UBUNTU_VERSION}" ]; then
|
||||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb
|
||||
elif [ "18.04" == "${UBUNTU_VERSION}" ]; then
|
||||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.bionic_amd64.deb
|
||||
else
|
||||
# 22.04+ (jusqu'à 26.04 et au-delà) : wkhtmltopdf ne publie pas de build
|
||||
# par version ; le .deb « jammy » est le plus récent et fonctionne. Un
|
||||
# « else » (au lieu d'énumérer les versions) évite une URL VIDE sur une
|
||||
# version récente (26.04) -> gdebi appelé sans fichier -> échec.
|
||||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
|
||||
fi
|
||||
case "${UBUNTU_VERSION}" in
|
||||
18.04 | 20.04 | 22.04)
|
||||
echo "Ubuntu ${UBUNTU_VERSION} n'est plus supporte par ERPLibre :"
|
||||
echo " sa chaine d'outils est trop ancienne (pikepdf exige qpdf 12.2,"
|
||||
echo " compile en C++20, quand cette version livre GCC 9)."
|
||||
echo " Utilisez Ubuntu 24.04, 25.10 ou 26.04."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ "${OS}" == "Ubuntu" ]]; then
|
||||
# wkhtmltopdf ne publie pas de build par version d'Ubuntu ; le .deb
|
||||
# « jammy » est le plus récent et fonctionne de 24.04 à 26.04 et au-delà.
|
||||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
|
||||
elif [[ "${OS}" == "Linuxmint" ]]; then
|
||||
if [ "22.3" == "${UBUNTU_VERSION}" ]; then
|
||||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
|
||||
fi
|
||||
# Sans « else », toute Mint autre que 22.3 laissait WKHTMLTOX_X64 VIDE, et
|
||||
# gdebi etait appele sans fichier. Mint 22.x repose sur noble : meme .deb.
|
||||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
|
||||
elif [[ "${OS}" == "Debian" ]]; then
|
||||
if [ "bullseye" == "${DEBIAN_VERSION}" ]; then
|
||||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bullseye_amd64.deb
|
||||
|
|
@ -46,10 +61,10 @@ elif [[ "${OS}" == "Debian" ]]; then
|
|||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb
|
||||
fi
|
||||
elif [[ "${OS}" == *"Ubuntu"* ]]; then
|
||||
echo "Your version of Ubuntu is not supported, only support 18.04, 20.04 and 22.04"
|
||||
echo "Your version of Ubuntu is not supported, only support 24.04, 25.10 and 26.04"
|
||||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
|
||||
else
|
||||
echo "Your version of Ubuntu is not supported, only support 18.04, 20.04 and 22.04"
|
||||
echo "Your version of Ubuntu is not supported, only support 24.04, 25.10 and 26.04"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -58,7 +73,144 @@ fi
|
|||
#--------------------------------------------------
|
||||
if [ "$(uname -m)" = "s390x" ]; then
|
||||
echo "Arch s390x detected"
|
||||
sudo apt install rust-all libqpdf-dev libgeos-dev libproj-dev proj-bin proj-data libgeographiclib-dev freetds-dev freetds-bin libkrb5-dev libssl-dev pkg-config build-essential npm -y
|
||||
# La mémoire est la ressource qui manque en premier ici : cc1plus demande
|
||||
# jusqu'à 2,5 Gio pour un seul fichier de matplotlib, et le tueur du noyau
|
||||
# abrège sans jamais nommer la mémoire (« Killed signal terminated program
|
||||
# cc1plus »). On complète par du swap avant d'en arriver là.
|
||||
el_swap_ensure
|
||||
# Sur une VM fraîche, l'index apt peut être vide : « apt install » échouerait
|
||||
# alors sur TOUT le lot, et l'absence d'un seul paquet ne se voit que bien
|
||||
# plus loin, sous la forme d'une commande introuvable.
|
||||
${APT_GET} update
|
||||
# « ${APT_GET} » et non « sudo apt » : au 1er boot d'une image cloud,
|
||||
# cloud-init tient le verrou apt, et un « apt install » nu échoue AUSSITÔT.
|
||||
# Ces trois lots étaient les seuls du script à contourner le wrapper, et
|
||||
# aucun ne vérifiait son résultat : libqpdf-dev manquait sans un mot, et
|
||||
# l'échec ne se voyait qu'une heure plus tard, à la compilation de pikepdf.
|
||||
# Un nom de paquet CHANGE d'une version d'Ubuntu à l'autre : GeographicLib
|
||||
# s'appelle « libgeographic-dev » sur 20.04 et 22.04, « libgeographiclib-dev »
|
||||
# depuis 24.04. Le nom récent existe pourtant dans la base apt des anciennes,
|
||||
# SANS version installable — apt refuse alors le lot ENTIER, emportant les
|
||||
# quatorze autres paquets. On demande donc à apt lui-même quel nom est
|
||||
# réellement installable, seule autorité sur la question.
|
||||
apt_pick() {
|
||||
for candidate in "$@"; do
|
||||
if apt-get install -s -qq "${candidate}" > /dev/null 2>&1; then
|
||||
echo "${candidate}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "$1"
|
||||
}
|
||||
# Vrai si dpkg considère le paquet installé et configuré.
|
||||
apt_is_installed() {
|
||||
[ "$(dpkg-query -W -f='${db:Status-Status}' "$1" 2>/dev/null)" = "installed" ]
|
||||
}
|
||||
# Le lot d'abord, pour la vitesse. S'il échoue, on reprend paquet par paquet
|
||||
# afin de NOMMER le fautif : sinon un seul nom obsolète masque les autres et
|
||||
# le message ne dit pas lequel corriger.
|
||||
apt_install_batch() {
|
||||
if ${APT_GET} install "$@" -y; then
|
||||
return 0
|
||||
fi
|
||||
# Un dépaquetage raté laisse dpkg à moitié configuré, et TOUT apt suivant
|
||||
# rend « Unmet dependencies » — y compris pour des paquets déjà installés.
|
||||
# Sans cette réparation, la boucle ci-dessous accusait les quinze paquets
|
||||
# à cause d'un seul, et l'installation s'arrêtait sur un paquet présent.
|
||||
echo "Lot apt en echec : tentative de reparation dpkg..."
|
||||
sudo dpkg --configure -a || true
|
||||
${APT_GET} --fix-broken install -y || true
|
||||
if ${APT_GET} install "$@" -y; then
|
||||
echo "Reparation reussie."
|
||||
return 0
|
||||
fi
|
||||
echo "Reprise paquet par paquet pour identifier le fautif..."
|
||||
df -h / | sed 's/^/ disque: /'
|
||||
APT_FAILED=""
|
||||
for one in "$@"; do
|
||||
${APT_GET} install "${one}" -y && continue
|
||||
# L'échec peut n'être qu'un contrecoup de l'état global : on ne l'impute
|
||||
# au paquet que si dpkg confirme qu'il n'est PAS installé.
|
||||
apt_is_installed "${one}" || APT_FAILED="${APT_FAILED} ${one}"
|
||||
done
|
||||
[ -z "${APT_FAILED}" ]
|
||||
}
|
||||
APT_FAILED=""
|
||||
GEO_DEV="$(apt_pick libgeographiclib-dev libgeographic-dev)"
|
||||
# manifold3d (via to-3mf) et pymupdf n'ont pas de roue s390x : ils se
|
||||
# compilent, d'où tbb, cmake et ninja. Un seul lot, une seule reprise.
|
||||
apt_install_batch rust-all libqpdf-dev libgeos-dev libproj-dev proj-bin \
|
||||
proj-data "${GEO_DEV}" freetds-dev freetds-bin libkrb5-dev libssl-dev \
|
||||
pkg-config build-essential zlib1g-dev libjpeg-dev libtbb-dev cmake \
|
||||
ninja-build
|
||||
if [[ -n "${APT_FAILED}" ]]; then
|
||||
# Seul un paquet dont dépend la SUITE immédiate est bloquant. Les autres
|
||||
# servent des modules Odoo optionnels : les rendre fatals immobiliserait
|
||||
# toute l'installation pour un module que personne n'utilise ici.
|
||||
echo "Paquets s390x non installables :${APT_FAILED}"
|
||||
for essential in build-essential pkg-config libssl-dev zlib1g-dev \
|
||||
libjpeg-dev cmake; do
|
||||
case " ${APT_FAILED} " in
|
||||
*" ${essential} "*)
|
||||
echo "apt-get s390x : '${essential}' est indispensable, arret."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
echo "Aucun n'est indispensable a la compilation : on continue."
|
||||
fi
|
||||
# pymupdf non plus n'a pas de roue s390x : il compile MuPDF, dont le
|
||||
# générateur de liaisons charge « libclang.so » par son nom nu, via ctypes.
|
||||
# La roue PyPI libclang, qui embarque cette bibliothèque, n'existe pas ici.
|
||||
# Le paquet de la distribution la fournit bien dans le chemin de l'éditeur de
|
||||
# liens, mais sous un nom versionné : il ne manque que le lien non versionné.
|
||||
${APT_GET} install libclang-dev -y
|
||||
CLANG_LIB_DIR="/usr/lib/s390x-linux-gnu"
|
||||
if [ ! -e "${CLANG_LIB_DIR}/libclang.so" ]; then
|
||||
CLANG_SO="$(ls -1 ${CLANG_LIB_DIR}/libclang-[0-9]*.so 2>/dev/null | sort -V | tail -1)"
|
||||
if [ -n "${CLANG_SO}" ]; then
|
||||
sudo ln -s "${CLANG_SO}" "${CLANG_LIB_DIR}/libclang.so"
|
||||
sudo ldconfig
|
||||
echo "libclang.so -> ${CLANG_SO}"
|
||||
else
|
||||
echo "Attention : libclang introuvable, la compilation de pymupdf va echouer."
|
||||
fi
|
||||
fi
|
||||
# pikepdf se lie a qpdf, dont il exige 12.2.0 au minimum. Ubuntu 24.04 en
|
||||
# livre 11.9.0 ; 25.10 et 26.04 passent, d'ou le partage observe. Le detail
|
||||
# -- seuil, version batie, chemin d'installation -- est dans lib_qpdf.sh,
|
||||
# partage avec les scripts dnf et zypper qui butaient sur le meme mur.
|
||||
el_qpdf_ensure
|
||||
# cryptography ne publie aucune roue s390x : elle se compile, et son
|
||||
# Cargo.lock est en version 4, que seul cargo >= 1.78 sait lire. Ubuntu 24.04
|
||||
# livre 1.75 et s'arrête sur « lock file version 4 requires
|
||||
# -Znext-lockfile-bump » ; 25.10 et au-delà passent. On complète alors par
|
||||
# rustup, en exposant la chaîne hors du shell de connexion : les étapes
|
||||
# suivantes sont des processus distincts, qui ne liront ni ~/.bashrc ni
|
||||
# ~/.cargo/env.
|
||||
CARGO_MIN_MINOR=78
|
||||
cargo_ver="$(cargo --version 2>/dev/null | awk '{print $2}')"
|
||||
cargo_major="${cargo_ver%%.*}"
|
||||
cargo_rest="${cargo_ver#*.}"
|
||||
cargo_minor="${cargo_rest%%.*}"
|
||||
cargo_ok=0
|
||||
if [[ "${cargo_major}" =~ ^[0-9]+$ && "${cargo_minor}" =~ ^[0-9]+$ ]]; then
|
||||
if [[ ${cargo_major} -gt 1 ]] ||
|
||||
[[ ${cargo_major} -eq 1 && ${cargo_minor} -ge ${CARGO_MIN_MINOR} ]]; then
|
||||
cargo_ok=1
|
||||
fi
|
||||
fi
|
||||
if [ "${cargo_ok}" -ne 1 ]; then
|
||||
echo "cargo trop ancien ($(cargo --version 2>/dev/null || echo absent)) pour cryptography : installation via rustup."
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||
| sh -s -- -y --profile minimal --default-toolchain stable
|
||||
for bin in cargo rustc; do
|
||||
if [ -x "${HOME}/.cargo/bin/${bin}" ]; then
|
||||
sudo ln -sf "${HOME}/.cargo/bin/${bin}" "/usr/local/bin/${bin}"
|
||||
fi
|
||||
done
|
||||
echo "cargo retenu : $(PATH=/usr/local/bin:$PATH cargo --version 2>/dev/null || echo absent)"
|
||||
fi
|
||||
fi
|
||||
|
||||
#--------------------------------------------------
|
||||
|
|
@ -66,16 +218,6 @@ fi
|
|||
#--------------------------------------------------
|
||||
echo -e "\n---- Update Server ----"
|
||||
|
||||
if [ "18.04" == "${UBUNTU_VERSION}" ]; then
|
||||
# add-apt-repository can install add-apt-repository Ubuntu 18.x
|
||||
${APT_GET} install software-properties-common curl -y
|
||||
# universe package is for Ubuntu 18.x
|
||||
sudo add-apt-repository universe
|
||||
# libpng12-0 dependency for wkhtmltopdf
|
||||
sudo add-apt-repository "deb http://mirrors.kernel.org/ubuntu/ xenial main"
|
||||
${APT_GET} update
|
||||
${APT_GET} upgrade -y
|
||||
fi
|
||||
|
||||
#--------------------------------------------------
|
||||
# Install PostgreSQL Server
|
||||
|
|
@ -119,14 +261,6 @@ if [[ $retVal -ne 0 ]]; then
|
|||
echo "apt-get libmariadb installation error."
|
||||
exit 1
|
||||
fi
|
||||
if [ "18.04" == "${UBUNTU_VERSION}" ]; then
|
||||
${APT_GET} install libpng12-0 -y
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
echo "apt-get libpng installation error."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# Dependencies for pyenv
|
||||
${APT_GET} install make libssl-dev zlib1g-dev libreadline-dev libsqlite3-dev curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev liblzma-dev -y
|
||||
retVal=$?
|
||||
|
|
@ -148,24 +282,100 @@ ${APT_GET} install -y ca-certificates curl gnupg
|
|||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
|
||||
if [ "18.04" == "${UBUNTU_VERSION}" ]; then
|
||||
sudo apt remove nodeJS npm
|
||||
NODE_MAJOR=16
|
||||
else
|
||||
# Node 22+ required by @capacitor/cli v8.x (mobile app dependency)
|
||||
NODE_MAJOR=22
|
||||
# Node 22+ required by @capacitor/cli v8.x (mobile app dependency)
|
||||
NODE_MAJOR=22
|
||||
|
||||
# NodeSource ne publie que amd64, arm64 et armhf : vérifié, binary-s390x
|
||||
# répond 404. Ajouter le dépôt sur une autre architecture n'apporte rien et
|
||||
# fait échouer « apt update » sur un index introuvable. La distribution
|
||||
# fournit alors nodejs elle-même — Ubuntu 26.04 s390x livre la 22.22.1, ce qui
|
||||
# convient.
|
||||
NODE_ARCH="$(dpkg --print-architecture)"
|
||||
case "${NODE_ARCH}" in
|
||||
amd64 | arm64 | armhf)
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
|
||||
${APT_GET} update
|
||||
;;
|
||||
*)
|
||||
echo "NodeSource ne publie pas pour ${NODE_ARCH} : nodejs vient de la distribution."
|
||||
sudo rm -f /etc/apt/sources.list.d/nodesource.list
|
||||
${APT_GET} update
|
||||
# Le paquet « nodejs » de NodeSource embarque npm ; celui d'Ubuntu NON —
|
||||
# vérifié : /usr/bin/npm est absent du paquet sur jammy, noble et questing.
|
||||
# Sans cette ligne, tout ce qui suit tombe sur « npm: command not found ».
|
||||
NODE_PKG="nodejs npm"
|
||||
;;
|
||||
esac
|
||||
${APT_GET} install ${NODE_PKG:-nodejs} -y
|
||||
|
||||
node_major() {
|
||||
node --version 2>/dev/null | sed -n 's/^v\([0-9]*\).*/\1/p'
|
||||
}
|
||||
|
||||
# Le node de la distribution suffit sur les versions récentes, pas sur les
|
||||
# anciennes : focal livre la 10, jammy la 12, alors que « less » exige node 18
|
||||
# et « rtlcss » node 12. Là où NodeSource n'a rien à offrir, l'archive
|
||||
# officielle de nodejs.org prend le relais — elle publie bien linux-s390x,
|
||||
# vérifié dans son index. Rien n'est téléchargé si la distribution suffit.
|
||||
NODE_MIN=18
|
||||
node_have="$(node_major)"
|
||||
if [[ -n "${NODE_PKG}" && ! (${node_have:-0} -ge ${NODE_MIN}) ]]; then
|
||||
echo "node $(node --version 2>/dev/null || echo absent) trop ancien (< ${NODE_MIN}) : archive officielle Node ${NODE_MAJOR}."
|
||||
case "${NODE_ARCH}" in
|
||||
# Node 22 publie linux-{x64,arm64,armv7l,ppc64le,s390x} — vérifié dans son
|
||||
# index. Pas de riscv64 : cette architecture garde le node de sa
|
||||
# distribution, et le message plus bas le dit.
|
||||
s390x) NODE_DIST_ARCH=s390x ;;
|
||||
ppc64el) NODE_DIST_ARCH=ppc64le ;;
|
||||
*) NODE_DIST_ARCH="" ;;
|
||||
esac
|
||||
NODE_VER="$(curl -fsSL --max-time 60 https://nodejs.org/dist/index.json |
|
||||
grep -o "\"version\":\"v${NODE_MAJOR}\.[0-9.]*\"" | head -1 | cut -d'"' -f4)"
|
||||
if [[ -z "${NODE_DIST_ARCH}" || -z "${NODE_VER}" ]]; then
|
||||
echo "Pas d'archive Node officielle pour ${NODE_ARCH} : on garde celle de la distribution."
|
||||
else
|
||||
NODE_TGZ="node-${NODE_VER}-linux-${NODE_DIST_ARCH}.tar.xz"
|
||||
NODE_TMP="$(mktemp -d)"
|
||||
if curl -fsSL --max-time 600 -o "${NODE_TMP}/${NODE_TGZ}" \
|
||||
"https://nodejs.org/dist/${NODE_VER}/${NODE_TGZ}" &&
|
||||
curl -fsSL --max-time 120 -o "${NODE_TMP}/SHASUMS256.txt" \
|
||||
"https://nodejs.org/dist/${NODE_VER}/SHASUMS256.txt" &&
|
||||
(cd "${NODE_TMP}" && grep " ${NODE_TGZ}\$" SHASUMS256.txt | sha256sum -c -); then
|
||||
# Un « npm install npm@latest -g » antérieur a pu déposer un npm
|
||||
# incompatible ici : l'archive écrase les fichiers mais ne supprime pas
|
||||
# ceux qu'elle n'a pas, ce qui laisserait un mélange des deux versions.
|
||||
sudo rm -rf /usr/local/lib/node_modules/npm
|
||||
sudo tar -xJf "${NODE_TMP}/${NODE_TGZ}" -C /usr/local --strip-components=1
|
||||
hash -r
|
||||
echo "node $(node --version) / npm $(npm --version) installés dans /usr/local."
|
||||
else
|
||||
echo "Telechargement de Node ${NODE_VER} impossible : on garde celle de la distribution."
|
||||
fi
|
||||
rm -rf "${NODE_TMP}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
|
||||
${APT_GET} update
|
||||
${APT_GET} install nodejs -y
|
||||
|
||||
sudo npm install npm@latest -g
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
echo "npm install npm lastest installation error."
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
echo "npm est introuvable apres l'installation de nodejs (${NODE_ARCH})."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# « npm@latest » dépasse régulièrement le node en place : sur Ubuntu 26.04
|
||||
# s390x, npm 12 exige node ^22.22.2 alors que l'archive livre la 22.22.1 — un
|
||||
# correctif d'écart, et EBADENGINE. Pire sur un node ancien : npm 6 n'applique
|
||||
# PAS « engines », il se contente d'avertir, installe npm 12 par-dessus et le
|
||||
# rend inutilisable (« Cannot find module 'node:path' », absent avant node
|
||||
# 14.18). Le npm livré AVEC node est compatible par construction ; cette mise à
|
||||
# niveau n'est qu'un confort, on ne la tente que si node est assez récent.
|
||||
if [[ "$(node_major)" =~ ^[0-9]+$ && $(node_major) -ge ${NODE_MAJOR} ]]; then
|
||||
sudo npm install npm@latest -g
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
echo "Avertissement : npm n'a pas pu être mis à niveau, on garde $(npm --version)."
|
||||
fi
|
||||
else
|
||||
echo "npm $(npm --version) conservé : node $(node --version) est en deçà de ${NODE_MAJOR}."
|
||||
fi
|
||||
sudo npm install -g rtlcss
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
|
|
|
|||
|
|
@ -1,13 +1,32 @@
|
|||
#!/usr/bin/env bash
|
||||
# Le code de retour du script de distribution DOIT remonter : il est aujourd'hui
|
||||
# la derniere commande executee, donc propage par accident. « set -e » le rend
|
||||
# explicite, et update_env_version.py:818 s'en sert pour arreter l'installation
|
||||
# au lieu d'enchainer sur un systeme sans compilateur.
|
||||
set -e
|
||||
|
||||
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
|
||||
if [[ "${VERSION_ID}" == "18.04" || "${VERSION_ID}" == "20.04" || "${VERSION_ID}" == "22.04" || "${VERSION_ID}" == "22.10" || "${VERSION_ID}" == "23.04" || "${VERSION_ID}" == "23.10" || "${VERSION_ID}" == "24.04" || "${VERSION_ID}" == "25.04" || "${VERSION_ID}" == "25.10" || "${VERSION_ID}" == "26.04" ]]; then
|
||||
# 20.04 et 22.04 retirees : leur chaine d'outils ne suffit plus a ERPLibre
|
||||
# (pikepdf exige qpdf >= 12.2, en C++20, quand focal livre GCC 9). Les
|
||||
# versions intermediaires EOL (22.10, 23.04, 23.10, 25.04) partent avec.
|
||||
if [[ "${VERSION_ID}" == "24.04" || "${VERSION_ID}" == "25.10" || "${VERSION_ID}" == "26.04" ]]; then
|
||||
echo "\n---- linux-gnu installation process started ----"
|
||||
./script/install/install_debian_dependency.sh
|
||||
else
|
||||
echo "Your version is not supported, only support 18.04, 20.04 and 22.04 - 24.04, 25.04, 25.10, 26.04 : ${VERSION_ID}"
|
||||
echo "Your version is not supported, only support 24.04, 25.10, 26.04 : ${VERSION_ID}"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "${ID}" == "linuxmint" ]]; then
|
||||
if [[ "${VERSION_ID}" == "22.3" ]]; then
|
||||
|
|
@ -20,14 +39,26 @@ 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
|
||||
echo "\n---- Fedora installation process started ----"
|
||||
# AlmaLinux et Rocky declarent ID_LIKE="rhel centos fedora" : elles passent
|
||||
# donc par ce meme script, qui aiguille dnf.
|
||||
echo "\n---- Fedora/RHEL installation process started ----"
|
||||
./script/install/install_fedora_dependency.sh
|
||||
else
|
||||
./script/install/install_debian_dependency.sh
|
||||
echo "Your Linux system is not supported, only support Ubuntu 18.04 or Ubuntu 20.04 or Ubuntu 22.04 - Ubuntu 23.10 - Ubuntu 24.04, Ubuntu 25.04, Ubuntu 25.10, Debian, Fedora, 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
|
||||
|
|
|
|||
|
|
@ -2,25 +2,159 @@
|
|||
# © 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 Fedora (dnf). Équivalent Fedora de
|
||||
# install_debian_dependency.sh. « --skip-unavailable » tolère un nom de paquet
|
||||
# absent (dnf5) au lieu d'échouer sur tout le lot.
|
||||
# Dépendances système ERPLibre pour la famille dnf : Fedora, mais aussi
|
||||
# AlmaLinux, Rocky et RHEL, qu'install_dev.sh aiguille ici sur ID_LIKE=rhel.
|
||||
# Équivalent de install_debian_dependency.sh.
|
||||
#
|
||||
# Attention : Fedora 41+ livre dnf5, EL9 et EL10 encore dnf4. Les deux ne
|
||||
# comprennent pas les mêmes options — voir DNF_SKIP plus bas.
|
||||
|
||||
. ./env_var.sh
|
||||
. ./script/install/lib_qpdf.sh
|
||||
. ./script/install/lib_lowmem.sh
|
||||
|
||||
EL_USER=${USER}
|
||||
|
||||
# « Sauter un paquet introuvable au lieu d'échouer sur tout le lot » ne
|
||||
# s'écrit PAS pareil selon la version de dnf : « --skip-unavailable » est une
|
||||
# option de dnf5 (Fedora 41+), et dnf4 — encore livré par EL9 et EL10, donc
|
||||
# par AlmaLinux et Rocky — la refuse net : « dnf install: error: unrecognized
|
||||
# arguments ». Son équivalent y est « --setopt=strict=0 ». On demande donc à
|
||||
# dnf lui-même ce qu'il comprend, plutôt que de deviner d'après la distro.
|
||||
if dnf install --help 2>&1 | grep -q -- "--skip-unavailable"; then
|
||||
DNF_SKIP="--skip-unavailable"
|
||||
else
|
||||
DNF_SKIP="--setopt=strict=0"
|
||||
fi
|
||||
echo "dnf : option de tolerance retenue = ${DNF_SKIP}"
|
||||
# dnf résilient : rafraîchit le cache (évite « checksum doesn't match » /
|
||||
# signature après un cache périmé) et saute les paquets introuvables.
|
||||
DNF="sudo dnf install -y --refresh --skip-unavailable"
|
||||
DNF="sudo dnf install -y --refresh ${DNF_SKIP}"
|
||||
|
||||
#--------------------------------------------------
|
||||
# Dérivés RHEL : dépôts supplémentaires
|
||||
#--------------------------------------------------
|
||||
# AlmaLinux, Rocky, CentOS Stream et RHEL passent par ce script (install_dev.sh
|
||||
# les aiguille sur ID_LIKE=rhel). Contrairement à Fedora, leur jeu de dépôts
|
||||
# par défaut est ÉTROIT : la plupart des paquets « -devel » vivent dans CRB
|
||||
# (CodeReady Builder), et tout ce qui est communautaire dans EPEL. Sans ces
|
||||
# deux dépôts, la tolérance aux paquets absents les saute EN SILENCE et
|
||||
# l'échec n'apparaît qu'à la compilation, très loin d'ici.
|
||||
if [ -r /etc/os-release ]; then
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
fi
|
||||
case "${ID}" in
|
||||
almalinux | rocky | rhel | centos)
|
||||
echo -e "\n---- Depots EPEL et CRB (famille RHEL) ----"
|
||||
sudo dnf install -y epel-release \
|
||||
|| echo "epel-release indisponible : certains paquets manqueront."
|
||||
# /usr/bin/crb est l'outil fourni par epel-release, et ce que son propre
|
||||
# scriptlet recommande d'exécuter — plus fiable que de deviner le nom du
|
||||
# dépôt, qui vaut « crb » sur EL9/EL10 et « powertools » sur EL8.
|
||||
sudo /usr/bin/crb enable 2> /dev/null \
|
||||
|| sudo dnf config-manager --set-enabled crb 2> /dev/null \
|
||||
|| sudo dnf config-manager --set-enabled powertools 2> /dev/null \
|
||||
|| sudo dnf config-manager --enable crb 2> /dev/null \
|
||||
|| echo "CRB/PowerTools non active : certains -devel manqueront."
|
||||
# Un dépôt fraîchement activé dont les métadonnées ne descendent pas fait
|
||||
# échouer TOUS les dnf suivants. On le voit ici, avec son nom, plutôt que
|
||||
# trois sections plus loin sur un paquet sans rapport.
|
||||
sudo dnf makecache 2> /dev/null \
|
||||
|| { sudo dnf clean all; sudo dnf makecache; } \
|
||||
|| echo "Attention : metadonnees de depot incompletes."
|
||||
;;
|
||||
esac
|
||||
|
||||
#--------------------------------------------------
|
||||
# Outils de compilation (build Python via pyenv, extensions Python)
|
||||
#--------------------------------------------------
|
||||
echo -e "\n---- Groupe outils de développement ----"
|
||||
# Groupes par ID (le nom affiché « C Development Tools... » n'est pas matché).
|
||||
sudo dnf group install -y --skip-unavailable development-tools c-development \
|
||||
|| sudo dnf install -y gcc gcc-c++ make automake patch
|
||||
echo -e "\n---- Outils de developpement ----"
|
||||
# Les GROUPES ne sont pas les mêmes d'une famille à l'autre : Fedora a
|
||||
# « c-development », EL ne connaît que « development » — vérifié dans le
|
||||
# comps.xml d'AlmaLinux 10, qui n'a même pas « development-tools ».
|
||||
#
|
||||
# Pire, la tolérance aux paquets absents les rend INOFFENSIFS : un
|
||||
# « dnf group install » de deux groupes inconnus REND ZÉRO sans rien poser,
|
||||
# donc le « || » de repli ne se déclenchait jamais. gcc arrivait par la
|
||||
# section pyenv plus bas, gcc-c++ par personne, et numpy s'arrêtait sur
|
||||
# « Unknown compiler(s): [['c++'], ['g++'], …] ».
|
||||
#
|
||||
# On installe donc les compilateurs EXPLICITEMENT, sans dépendre d'un groupe.
|
||||
# Le groupe reste ensuite, en complément et en best-effort.
|
||||
${DNF} gcc gcc-c++ make automake patch
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
echo "dnf install compilers error."
|
||||
exit 1
|
||||
fi
|
||||
sudo dnf group install -y ${DNF_SKIP} development c-development \
|
||||
> /dev/null 2>&1 || true
|
||||
|
||||
#--------------------------------------------------
|
||||
# Mainframe s390x
|
||||
#--------------------------------------------------
|
||||
# Sur s390x, AUCUNE roue PyPI n'existe : numpy, pillow, lxml, pikepdf,
|
||||
# psycopg2, cryptography… tout se compile contre les bibliothèques de la
|
||||
# distribution. Ces en-têtes ne servent à rien sur amd64, où pip pose des
|
||||
# roues, mais leur absence ici arrête l'installation très loin de sa cause —
|
||||
# « The headers or library files could not be found for jpeg » pour pillow.
|
||||
if [ "$(uname -m)" = "s390x" ]; then
|
||||
echo -e "\n---- Dependances de compilation s390x ----"
|
||||
# La mémoire est la ressource qui manque en premier ici : cc1plus
|
||||
# demande jusqu'à 2,5 Gio pour un seul fichier de matplotlib, et le
|
||||
# tueur du noyau abrège sans jamais nommer la mémoire (« Killed signal
|
||||
# terminated program cc1plus »). On complète par du swap avant d'en
|
||||
# arriver là.
|
||||
el_swap_ensure
|
||||
# Best-effort : un nom qui change d'une version à l'autre ne doit pas
|
||||
# emporter le lot. Ce qui est vraiment indispensable est déjà installé
|
||||
# au-dessus (compilateurs) ou plus bas (pyenv, PostgreSQL).
|
||||
# « rust » et « cargo » : plusieurs paquets Python portent une extension
|
||||
# RUST, pas seulement C — bcrypt et cryptography en tête. Sur amd64 leurs
|
||||
# roues masquent le besoin ; ici tout compile, et bcrypt s'arrête net sur
|
||||
# « error: can't find Rust compiler ». Les versions livrées suffisent
|
||||
# (AlmaLinux 1.92, Fedora plus récent) au Cargo.lock v4 qui exige 1.78.
|
||||
${DNF} \
|
||||
rust cargo \
|
||||
libjpeg-turbo-devel zlib-devel geos-devel proj-devel \
|
||||
krb5-devel tbb-devel ninja-build clang-devel llvm-devel \
|
||||
GeographicLib-devel pkgconf-pkg-config cmake
|
||||
|
||||
# qpdf : pikepdf en exige 12.2.0, et le paquet de la distribution ne le donne
|
||||
# que sur Fedora. EL9 livre 10.3 — même pas « qpdf/QPDFJob.hh », d'où trois
|
||||
# cents lignes de g++ sans un mot sur qpdf ; EL10 livre 11.9.
|
||||
#
|
||||
# On ne pose donc « qpdf-devel » QUE s'il atteint le seuil, au lieu de le
|
||||
# poser puis de le contourner : contrairement à Debian, l'éditeur de liens de
|
||||
# RHEL cherche /usr/lib64 AVANT /usr/local/lib64. Le -devel trop ancien ferait
|
||||
# lier pikepdf contre l'ancienne bibliothèque malgré les en-têtes neufs de
|
||||
# /usr/local/include — une incohérence bien plus difficile à lire qu'un
|
||||
# en-tête absent. Sans lui, el_qpdf_ensure fournit tout depuis les sources.
|
||||
qpdf_repo="$(dnf repoquery --qf '%{version}' --latest-limit 1 qpdf-devel \
|
||||
2> /dev/null | tail -1)"
|
||||
if el_qpdf_ge_min "${qpdf_repo}"; then
|
||||
${DNF} qpdf-devel
|
||||
else
|
||||
echo "qpdf-devel ${qpdf_repo:-absent} sous le seuil de pikepdf : compilation depuis les sources."
|
||||
fi
|
||||
el_qpdf_ensure
|
||||
# Le dire ICI plutôt que de laisser bcrypt le découvrir une heure plus tard.
|
||||
if ! command -v cargo > /dev/null 2>&1; then
|
||||
echo "Attention : cargo absent, bcrypt et cryptography ne compileront pas."
|
||||
fi
|
||||
# pymupdf charge « libclang.so » par son nom nu, via ctypes. Le paquet le
|
||||
# livre sous un nom versionné : il ne manque que le lien.
|
||||
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
|
||||
|
||||
#--------------------------------------------------
|
||||
# PostgreSQL
|
||||
|
|
|
|||
|
|
@ -19,15 +19,29 @@ Color_Off='\033[0m' # Text Reset
|
|||
PYTHON_VERSION_MAJOR=$(echo "$EL_PYTHON_ODOO_VERSION" | sed 's/\.[^\.]*$//')
|
||||
VENV_ERPLIBRE_PATH=$(cat "conf/python-erplibre-venv" | xargs)
|
||||
VENV_ODOO_PATH=".venv.${EL_ERPLIBRE_VERSION}"
|
||||
POETRY_ODOO_PATH=${VENV_ERPLIBRE_PATH}/bin/poetry
|
||||
# Poetry est installé dans le venv ODOO, pas dans celui des outils : le garde
|
||||
# d'idempotence visait le mauvais chemin, donc il était TOUJOURS vrai. Sans
|
||||
# conséquence visible — poetry se réinstallait pour rien — mais le jour où ce
|
||||
# fichier aurait existé, « poetry install » aurait été purement sauté.
|
||||
POETRY_ODOO_PATH=${VENV_ODOO_PATH}/bin/poetry
|
||||
|
||||
# Choix pip/uv : un seul endroit décide, comme pour mise/pyenv.
|
||||
# shellcheck source=script/install/lib_pip_provider.sh
|
||||
. ./script/install/lib_pip_provider.sh
|
||||
# Bornage des compilations : voir el_build_limit_jobs.
|
||||
. ./script/install/lib_lowmem.sh
|
||||
export WITH_POETRY_INSTALLATION=1
|
||||
|
||||
# Verbosité de l'installation Poetry : silencieuse (-q) par défaut, les logs
|
||||
# détaillés (-vvv) reviennent avec la variable d'environnement EL_VERBOSE=1.
|
||||
# Verbosité de l'installation Poetry. « -q » a été retiré du défaut : dans Cleo,
|
||||
# le mode silencieux coupe AUSSI la sortie d'erreur. Mesuré — « poetry -q
|
||||
# commande-inexistante » rend 1 sans imprimer un caractère, quand la même sans
|
||||
# « -q » nomme le problème. Un échec devenait donc parfaitement muet, et c'est
|
||||
# pour cela qu'il avait fallu ajouter un rejeu de diagnostic plus bas.
|
||||
# La verbosité par défaut de Poetry tient en une ligne par paquet.
|
||||
if [[ "${EL_VERBOSE:-0}" == "1" ]]; then
|
||||
POETRY_VERBOSE="-vvv"
|
||||
else
|
||||
POETRY_VERBOSE="-q"
|
||||
POETRY_VERBOSE=""
|
||||
fi
|
||||
|
||||
# EL_PHASE controls which steps to execute. Used by install_locally_dev.sh
|
||||
|
|
@ -50,10 +64,21 @@ if [[ "${EL_PHASE}" != "poetry" ]]; then
|
|||
if [[ ! -n "${DOCKER_BUILD}" ]]; then
|
||||
# Install ERPLibre venv
|
||||
echo -e "Install ${VENV_ERPLIBRE_PATH} with ${EL_PYTHON_ERPLIBRE_VERSION}"
|
||||
./script/install/install_venv.sh "ERPLibre" "${VENV_ERPLIBRE_PATH}" "${EL_PYTHON_ERPLIBRE_VERSION}"
|
||||
# Le code de retour n'était PAS regardé : quand l'interpréteur ne
|
||||
# pouvait pas être obtenu, le script continuait, et tout ce qui suit
|
||||
# échouait à son tour sur un venv inexistant. Le log devenait une
|
||||
# cascade de « No such file or directory » répartie sur trois fichiers,
|
||||
# où la cause réelle — pas de compilateur C — se perdait tout en haut.
|
||||
if ! ./script/install/install_venv.sh "ERPLibre" "${VENV_ERPLIBRE_PATH}" "${EL_PYTHON_ERPLIBRE_VERSION}"; then
|
||||
echo "Echec de creation de ${VENV_ERPLIBRE_PATH}, arret."
|
||||
exit 1
|
||||
fi
|
||||
# Install Odoo venv
|
||||
echo -e "Install ${VENV_ODOO_PATH} with ${EL_PYTHON_ODOO_VERSION}"
|
||||
./script/install/install_venv.sh "Odoo" "${VENV_ODOO_PATH}" "${EL_PYTHON_ODOO_VERSION}"
|
||||
if ! ./script/install/install_venv.sh "Odoo" "${VENV_ODOO_PATH}" "${EL_PYTHON_ODOO_VERSION}"; then
|
||||
echo "Echec de creation de ${VENV_ODOO_PATH}, arret."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
mkdir .venv
|
||||
fi
|
||||
|
|
@ -61,7 +86,8 @@ if [[ "${EL_PHASE}" != "poetry" ]]; then
|
|||
source ./${VENV_ERPLIBRE_PATH}/bin/activate
|
||||
echo -e "Upgrade pip to ${VENV_ERPLIBRE_PATH}"
|
||||
pip install --upgrade pip
|
||||
pip install -r requirement/erplibre_require-ments.txt
|
||||
el_pip_install "${VENV_ERPLIBRE_PATH}" \
|
||||
-r requirement/erplibre_require-ments.txt
|
||||
|
||||
./script/install/install_git_repo.sh
|
||||
fi
|
||||
|
|
@ -81,28 +107,87 @@ if [[ "${EL_PHASE}" != "setup" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Delete artifacts created by pip, cause error in next "poetry install"
|
||||
if [[ ! -f "${POETRY_ODOO_PATH}" ]]; then
|
||||
# s390x UNIQUEMENT — la borne ci-dessous ne doit toucher aucune autre
|
||||
# architecture, et le test d'architecture vient donc en premier.
|
||||
#
|
||||
# Poetry tire keyring, donc SecretStorage, donc cryptography, dont la
|
||||
# DERNIÈRE version : rien ne la borne à cette étape. Partout ailleurs c'est
|
||||
# sans conséquence, pip y pose une roue manylinux qui embarque son propre
|
||||
# OpenSSL en statique — vérifié, la 50.0.0 en publie pour x86_64, aarch64,
|
||||
# ppc64le et armv7l. s390x est la SEULE sans roue : elle compile, contre
|
||||
# l'OpenSSL du système.
|
||||
#
|
||||
# Or cryptography 47 a retiré le support d'OpenSSL 1.1.1 — vérifié version
|
||||
# par version, les gardes « CRYPTOGRAPHY_OPENSSL_300_OR_GREATER » de
|
||||
# fips.rs disparaissent entre la 46 et la 47 — et Ubuntu 20.04 livre
|
||||
# 1.1.1f. D'où « EVP_default_properties_is_fips_enabled not found in ffi ».
|
||||
# Sur s390x en 22.04 et au-delà, OpenSSL est en 3.x : aucune borne.
|
||||
PIP_CONSTRAINT_CRYPTO=""
|
||||
if [[ "$(uname -m)" == "s390x" ]]; then
|
||||
OPENSSL_VER="$(pkg-config --modversion openssl 2>/dev/null \
|
||||
|| openssl version 2>/dev/null | awk '{print $2}')"
|
||||
case "${OPENSSL_VER}" in
|
||||
3.* | 4.*) ;;
|
||||
"") echo "s390x : version d'OpenSSL indeterminee, aucune borne." ;;
|
||||
*)
|
||||
echo "s390x, OpenSSL ${OPENSSL_VER} < 3 : cryptography borne a <47 pour poetry."
|
||||
PIP_CONSTRAINT_CRYPTO="cryptography<47"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Le garde ne couvre QUE l'amorçage de Poetry. « poetry install » doit
|
||||
# rejouer à chaque fois : c'est lui qui applique un lock régénéré par
|
||||
# « make poetry_update ». L'englober rendrait la mise à jour sans effet
|
||||
# dès la seconde installation.
|
||||
if [[ ! -x "${POETRY_ODOO_PATH}" ]]; then
|
||||
echo -e "Install Poetry ${POETRY_ODOO_PATH}"
|
||||
pip install poetry==${EL_POETRY_VERSION}
|
||||
poetry --version
|
||||
# Fix broken poetry by installing ignored dependence
|
||||
# poetry lock --no-update
|
||||
# To fix keyring problem when installation is blocked, use
|
||||
export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
|
||||
if [[ ${WITH_POETRY_INSTALLATION} -ne 0 ]]; then
|
||||
poetry install --no-root ${POETRY_VERBOSE}
|
||||
fi
|
||||
el_pip_install "${VENV_ODOO_PATH}" \
|
||||
${PIP_CONSTRAINT_CRYPTO} "poetry==${EL_POETRY_VERSION}"
|
||||
fi
|
||||
# Chemin EXPLICITE, jamais le « poetry » du PATH. Le script active pourtant
|
||||
# le bon venv juste avant, mais poetry.toml porte « virtualenvs.create =
|
||||
# false » : hors activation, Poetry installe dans l'interpréteur de BASE.
|
||||
# Vérifié à la dure sur une VM — 772 paquets posés dans le CPython de mise,
|
||||
# invisibles du venv qui a « include-system-site-packages = false », et
|
||||
# promis à polluer tout autre venv bâti sur le même interpréteur.
|
||||
if [[ ! -x "${POETRY_ODOO_PATH}" ]]; then
|
||||
echo "Poetry introuvable a ${POETRY_ODOO_PATH}, arret."
|
||||
exit 1
|
||||
fi
|
||||
# Là où rien n'a de roue, « poetry install » COMPILE des centaines de
|
||||
# paquets, et le moteur de build de chacun lance nproc tâches en
|
||||
# parallèle. cc1plus demande jusqu'à 2,5 Gio pour un seul fichier de
|
||||
# matplotlib : six cœurs épuisent 8 Gio, et le noyau tue le compilateur
|
||||
# sans jamais nommer la mémoire. On borne d'après la mémoire disponible.
|
||||
if [[ "$(uname -m)" == "s390x" ]]; then
|
||||
el_build_limit_jobs
|
||||
fi
|
||||
"${POETRY_ODOO_PATH}" --version
|
||||
# To fix keyring problem when installation is blocked, use
|
||||
export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
|
||||
# « poetry install » reste à Poetry : uv ne lit pas poetry.lock
|
||||
# (astral-sh/uv#1804, « not planned ») et Poetry 2.1.3 n'a plus « export ».
|
||||
if [[ ${WITH_POETRY_INSTALLATION} -ne 0 ]]; then
|
||||
"${POETRY_ODOO_PATH}" install --no-root ${POETRY_VERBOSE}
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
echo "Poetry installation error with status ${retVal}"
|
||||
# « -q » masque la CAUSE. On rejoue en « -vvv » (debug) car c'est
|
||||
# le SEUL niveau où Poetry affiche la sortie des sous-processus
|
||||
# (git clone/checkout, build pip) — donc l'erreur réelle d'une
|
||||
# dépendance VCS/build. Capturé dans le log pour diagnostic.
|
||||
echo "---- Poetry: rejeu -vvv pour diagnostic ----"
|
||||
poetry install --no-root -vvv 2>&1 || true
|
||||
exit 1
|
||||
# Un rejeu, parce qu'une partie des échecs sont des aléas de
|
||||
# téléchargement : le lock est figé, rien ne dépend de l'instant.
|
||||
# « -vvv » est le SEUL niveau où Poetry montre la sortie des
|
||||
# sous-processus (git clone, build pip), donc l'erreur réelle.
|
||||
#
|
||||
# Le « exit 1 » était INCONDITIONNEL : un rejeu réussi laissait une
|
||||
# installation complète... et la déclarait quand même en échec.
|
||||
# C'est ce qui a arrêté openSUSE Leap 16 sur un pdfminer-six posé
|
||||
# correctement au second essai.
|
||||
echo "---- Poetry: rejeu -vvv (diagnostic et seconde chance) ----"
|
||||
if "${POETRY_ODOO_PATH}" install --no-root -vvv 2>&1; then
|
||||
echo "Le rejeu a REUSSI : le premier echec etait transitoire."
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
|
|||
370
script/install/install_suse_dependency.sh
Executable file
370
script/install/install_suse_dependency.sh
Executable file
|
|
@ -0,0 +1,370 @@
|
|||
#!/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 Leap 16 (le défaut, numéroté et stable) et Tumbleweed (la
|
||||
# rolling, gardée comme banc d'essai). Les deux livrent un qpdf au-dessus du
|
||||
# seuil de pikepdf 10 — la compilation de qpdf, une demi-heure sous émulation
|
||||
# s390x, ne s'y déclenche pas.
|
||||
#
|
||||
# Ce qui les sépare dans ce script tient en deux points, tous deux marqués par
|
||||
# SUSE_ROLLING : le chemin des dépôts, et « dup » contre « up ».
|
||||
|
||||
. ./env_var.sh
|
||||
. ./script/install/lib_qpdf.sh
|
||||
. ./script/install/lib_lowmem.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.
|
||||
if [ "${SUSE_ROLLING}" = 1 ]; then
|
||||
echo -e "\n---- Mise a jour complete (dup) ----"
|
||||
${ZYP} dup ${ZYP_LIC} --allow-vendor-change || true
|
||||
else
|
||||
# Sur Leap, « dup » sert à CHANGER de version, pas à mettre à jour : l'y
|
||||
# appeler alignerait la machine sur ce que les dépôts contiennent, ce qui
|
||||
# n'est pas ce qu'on veut d'une distribution justement choisie pour ne pas
|
||||
# bouger. « up » suffit, l'image et ses dépôts étant de la même version.
|
||||
echo -e "\n---- Mise a jour (up) ----"
|
||||
${ZYP} up ${ZYP_LIC} || true
|
||||
fi
|
||||
|
||||
#--------------------------------------------------
|
||||
# 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
|
||||
# La mémoire est la ressource qui manque en premier ici : cc1plus
|
||||
# demande jusqu'à 2,5 Gio pour un seul fichier de matplotlib, et le
|
||||
# tueur du noyau abrège sans jamais nommer la mémoire (« Killed signal
|
||||
# terminated program cc1plus »). On complète par du swap avant d'en
|
||||
# arriver là.
|
||||
el_swap_ensure
|
||||
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.
|
||||
# Python de la DISTRIBUTION d'abord : Tumbleweed livre python312 en 3.12.13,
|
||||
# qui satisfait « >=3.12.10,<3.13 ». lib_python_provider.sh le prefere alors a
|
||||
# toute compilation — decisif sur s390x, ou gcc 15.2 s'arrete sur une erreur
|
||||
# interne en batissant Parser/parser.c de CPython 3.12.10.
|
||||
# Le nom suit la version demandee : python312 pour 3.12.x.
|
||||
PY_WANT="$(cat .python-odoo-version 2>/dev/null | xargs)"
|
||||
PY_PKG="python$(echo "${PY_WANT}" | awk -F. '{print $1 $2}')"
|
||||
if [ -n "${PY_WANT}" ]; then
|
||||
echo -e "\n---- Python ${PY_WANT} depuis la distribution (${PY_PKG}) ----"
|
||||
zyp_soft "${PY_PKG}" "${PY_PKG}-devel" "${PY_PKG}-pip"
|
||||
fi
|
||||
|
||||
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 ----"
|
||||
|
|
@ -17,52 +17,25 @@ echo "Context: $CONTEXT"
|
|||
echo "Venv Path: $VENV_PATH"
|
||||
echo "Python Version: $PYTHON_VERSION"
|
||||
|
||||
PYENV_PATH=~/.pyenv
|
||||
PYENV_VERSION_PATH=${PYENV_PATH}/versions/${PYTHON_VERSION}
|
||||
PYTHON_EXEC=${PYENV_VERSION_PATH}/bin/python
|
||||
echo "Python path version home"
|
||||
echo ${PYENV_VERSION_PATH}
|
||||
#echo "Python path version local"
|
||||
#echo ${LOCAL_PYTHON_EXEC}
|
||||
# Le CHOIX du fournisseur (mise ou pyenv) vit dans la bibliothèque : ce script
|
||||
# ne connaît qu'un chemin d'interpréteur. C'est ce qui permet d'en ajouter un
|
||||
# troisième sans toucher ici.
|
||||
# shellcheck source=script/install/lib_python_provider.sh
|
||||
. ./script/install/lib_python_provider.sh
|
||||
|
||||
if [[ ! -d "${PYENV_PATH}" ]]; then
|
||||
echo -e "\n---- Installing pyenv in ${PYENV_PATH} ----"
|
||||
# export PYENV_GIT_TAG=v2.3.35
|
||||
# To change version
|
||||
# rm ~/.pyenv to uninstall it
|
||||
curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
|
||||
PYTHON_EXEC="$(el_python_exec "${PYTHON_VERSION}")"
|
||||
if [[ -z "${PYTHON_EXEC}" ]] || [[ ! -x "${PYTHON_EXEC}" ]]; then
|
||||
echo "Aucun interpreteur Python ${PYTHON_VERSION} n'a pu etre obtenu."
|
||||
echo " Fournisseur demande : ${EL_PYTHON_PROVIDER:-auto}"
|
||||
echo " Voir 'make install_mise', ou installez pyenv."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "\n---- Export pyenv in ${PYENV_PATH} ----"
|
||||
export PATH="${PYENV_PATH}/bin:$PATH"
|
||||
eval "$(pyenv init -)"
|
||||
eval "$(pyenv virtualenv-init -)"
|
||||
|
||||
if [[ ! -d "${PYENV_VERSION_PATH}" ]]; then
|
||||
echo -e "\n---- Installing python ${PYTHON_VERSION} with pyenv in ${PYENV_VERSION_PATH} ----"
|
||||
# Update all python version list
|
||||
cd "${PYENV_PATH}" && git pull && cd -
|
||||
yes n|pyenv install ${PYTHON_VERSION}
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
echo -e "${Red}Error${Color_Off} when installing pyenv"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# This will write .python-version
|
||||
# pyenv local ${PYTHON_VERSION}
|
||||
echo "Interpreteur retenu : ${PYTHON_EXEC}"
|
||||
|
||||
if [[ ! -d ${VENV_PATH} ]]; then
|
||||
echo -e "\n---- Create Virtual environment Python ----"
|
||||
if [[ -e ${PYTHON_EXEC} ]]; then
|
||||
${PYTHON_EXEC} -m venv "${VENV_PATH}"
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
echo "Virtual environment, error when creating ${VENV_PATH}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Missing pyenv, please refer installation guide. Check variable '${PYTHON_EXEC}'."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo -e "\n---- Create Virtual environment Python ----"
|
||||
if ! "${PYTHON_EXEC}" -m venv "${VENV_PATH}"; then
|
||||
echo "Virtual environment, error when creating ${VENV_PATH}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
|
|
|||
123
script/install/lib_lowmem.sh
Normal file
123
script/install/lib_lowmem.sh
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/env bash
|
||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
#
|
||||
# Bibliothèque SOURÇABLE : compiler sans se faire tuer par manque de mémoire.
|
||||
#
|
||||
# Le symptôme est toujours le même, et il ne nomme jamais la mémoire :
|
||||
# c++: fatal error: Killed signal terminated program cc1plus
|
||||
# « Killed » est un SIGKILL, donc le tueur de mémoire du noyau. Il est arrivé
|
||||
# sur matplotlib, dont _backend_agg_wrapper.cpp instancie assez de gabarits
|
||||
# C++ pour demander 1,5 à 2,5 Gio à cc1plus — À LUI SEUL.
|
||||
#
|
||||
# Deux causes se cumulent, et il faut les traiter toutes les deux :
|
||||
# 1. la VM est petite (le catalogue démarre à 1 ou 2 Gio) ;
|
||||
# 2. le moteur de build lance nproc compilations EN PARALLÈLE, chacune avec
|
||||
# son propre cc1plus. Six cœurs suffisent à épuiser 8 Gio.
|
||||
#
|
||||
# Réservé aux architectures sans roue PyPI par l'appelant : ailleurs pip pose
|
||||
# des binaires et rien de tout cela ne s'exécute.
|
||||
|
||||
# RAM + swap visés, en Gio. « 0 » désactive entièrement la création de swap,
|
||||
# pour qui préfère dimensionner sa machine lui-même.
|
||||
EL_SWAP_TARGET_GIB="${EL_SWAP_TARGET_GIB:-8}"
|
||||
EL_SWAP_FILE="${EL_SWAP_FILE:-/swapfile.erplibre}"
|
||||
# Mio de mémoire à réserver par compilation simultanée. 2048 vient du pic
|
||||
# mesuré de cc1plus sur matplotlib ; en dessous, le tueur revient.
|
||||
EL_MEM_PER_JOB_MB="${EL_MEM_PER_JOB_MB:-2048}"
|
||||
|
||||
el_mem_mb() {
|
||||
awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo 2> /dev/null || echo 0
|
||||
}
|
||||
|
||||
el_swap_mb() {
|
||||
awk '/^SwapTotal:/ {print int($2 / 1024)}' /proc/meminfo 2> /dev/null || echo 0
|
||||
}
|
||||
|
||||
# Complète la mémoire par un fichier d'échange, si elle manque.
|
||||
#
|
||||
# Volontairement NON persisté dans /etc/fstab : un swap déclaré et disparu
|
||||
# dégrade le démarrage, et cette machine n'en a besoin que le temps de
|
||||
# l'installation. La fonction est idempotente, elle repassera au besoin.
|
||||
el_swap_ensure() {
|
||||
local ram swap missing free_mb size
|
||||
[ "${EL_SWAP_TARGET_GIB}" -gt 0 ] 2> /dev/null || return 0
|
||||
ram="$(el_mem_mb)"
|
||||
swap="$(el_swap_mb)"
|
||||
missing=$((EL_SWAP_TARGET_GIB * 1024 - ram - swap))
|
||||
if [ "${missing}" -le 0 ]; then
|
||||
echo " memoire : ${ram} Mio + ${swap} Mio de swap, suffisant."
|
||||
return 0
|
||||
fi
|
||||
if [ -e "${EL_SWAP_FILE}" ]; then
|
||||
# Déjà là mais pas actif : un « swapon » suffit, on ne le réécrit pas.
|
||||
sudo swapon "${EL_SWAP_FILE}" 2> /dev/null && {
|
||||
echo " swap : ${EL_SWAP_FILE} reactive."
|
||||
return 0
|
||||
}
|
||||
fi
|
||||
# Ne jamais remplir le disque pour gagner de la mémoire : au plus la moitié
|
||||
# de ce qui reste libre. Un disque plein casse l'installation autrement.
|
||||
free_mb="$(df -Pm / | awk 'NR==2 {print $4}')"
|
||||
size="${missing}"
|
||||
[ "${size}" -gt $((free_mb / 2)) ] && size=$((free_mb / 2))
|
||||
if [ "${size}" -lt 512 ]; then
|
||||
echo " swap : pas assez d'espace disque libre (${free_mb} Mio), abandon." >&2
|
||||
return 0
|
||||
fi
|
||||
echo " memoire : ${ram} Mio seulement — ajout de ${size} Mio de swap."
|
||||
# fallocate est instantané ; dd est le repli des systèmes de fichiers qui
|
||||
# ne le supportent pas (et un swap sur trous ne fonctionnerait pas).
|
||||
if ! sudo fallocate -l "${size}M" "${EL_SWAP_FILE}" 2> /dev/null; then
|
||||
sudo dd if=/dev/zero of="${EL_SWAP_FILE}" bs=1M count="${size}" \
|
||||
status=none 2> /dev/null || {
|
||||
echo " swap : creation impossible, on continue sans." >&2
|
||||
return 0
|
||||
}
|
||||
fi
|
||||
sudo chmod 600 "${EL_SWAP_FILE}"
|
||||
if sudo mkswap "${EL_SWAP_FILE}" > /dev/null 2>&1 \
|
||||
&& sudo swapon "${EL_SWAP_FILE}"; then
|
||||
echo " swap : ${size} Mio actifs (${EL_SWAP_FILE})."
|
||||
else
|
||||
echo " swap : activation impossible, on continue sans." >&2
|
||||
sudo rm -f "${EL_SWAP_FILE}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Nombre de compilations simultanées que la mémoire tolère.
|
||||
el_build_jobs() {
|
||||
local total cpus jobs
|
||||
total=$(($(el_mem_mb) + $(el_swap_mb)))
|
||||
cpus="$(nproc 2> /dev/null || echo 1)"
|
||||
jobs=$((total / EL_MEM_PER_JOB_MB))
|
||||
[ "${jobs}" -lt 1 ] && jobs=1
|
||||
[ "${jobs}" -gt "${cpus}" ] && jobs="${cpus}"
|
||||
echo "${jobs}"
|
||||
}
|
||||
|
||||
# Exporte de quoi brider les moteurs de build. À appeler AVANT « poetry
|
||||
# install », dans le shell qui le lance.
|
||||
#
|
||||
# make MAKEFLAGS, lu par tout Makefile.
|
||||
# cmake CMAKE_BUILD_PARALLEL_LEVEL, lu par « cmake --build » (manifold3d).
|
||||
# ninja aucune variable de parallélisme n'existe — mais meson-python lit
|
||||
# « NINJA », le CHEMIN de l'exécutable (vérifié dans mesonpy 0.20 :
|
||||
# « env_ninja = os.environ.get('NINJA') »). On y met une enveloppe
|
||||
# qui ajoute le -j. C'est ce qui sauve matplotlib.
|
||||
# MAX_JOBS convention de PyTorch et de quelques autres.
|
||||
el_build_limit_jobs() {
|
||||
local jobs real wrapper
|
||||
jobs="$(el_build_jobs)"
|
||||
export MAKEFLAGS="-j${jobs}"
|
||||
export CMAKE_BUILD_PARALLEL_LEVEL="${jobs}"
|
||||
export MAX_JOBS="${jobs}"
|
||||
real="$(command -v ninja || command -v ninja-build || true)"
|
||||
if [ -n "${real}" ]; then
|
||||
wrapper="${TMPDIR:-/tmp}/el-ninja-j${jobs}"
|
||||
printf '#!/bin/sh\nexec %s -j%s "$@"\n' "${real}" "${jobs}" > "${wrapper}"
|
||||
chmod +x "${wrapper}"
|
||||
export NINJA="${wrapper}"
|
||||
fi
|
||||
echo " compilation bridee a ${jobs} tache(s) simultanee(s)."
|
||||
}
|
||||
70
script/install/lib_pip_provider.sh
Executable file
70
script/install/lib_pip_provider.sh
Executable file
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env bash
|
||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
#
|
||||
# Bibliothèque SOURÇABLE : installer des paquets Python dans un venv donné.
|
||||
#
|
||||
# Seul endroit du dépôt qui décide COMMENT les paquets sont posés. Deux
|
||||
# fournisseurs, même contrat de sortie :
|
||||
# pip historique, toujours présent dans un venv.
|
||||
# uv nettement plus rapide sur la résolution, le téléchargement et la
|
||||
# pose ; il met aussi en cache les roues qu'il CONSTRUIT, ce qui
|
||||
# compte là où rien n'a de roue publiée.
|
||||
#
|
||||
# EL_PIP_PROVIDER choisit : auto (défaut), uv, ou pip.
|
||||
#
|
||||
# CE QUE ÇA NE FAIT PAS — et il faut le savoir avant d'en attendre trop :
|
||||
# « poetry install --no-root » n'est PAS concerné. uv ne lit pas poetry.lock
|
||||
# (astral-sh/uv#1804, fermé « not planned ») et Poetry 2.1.3 n'a plus la
|
||||
# commande « export ». Or c'est cette étape qui domine : 354 paquets contre
|
||||
# 153 pour le venv d'outils. Le gain porte donc sur le venv d'outils et
|
||||
# l'amorçage de Poetry, pas sur le gros morceau.
|
||||
#
|
||||
# Et sur s390x il est quasi nul : une trentaine de paquets binaires n'ont
|
||||
# aucune roue (numpy, pandas, pillow, lxml, pymupdf…) et se COMPILENT. uv
|
||||
# n'enlève pas une seconde de gcc ; il parallélise seulement entre paquets.
|
||||
|
||||
# uv ne supporte pas officiellement Python 3.7, encore utilisé par Odoo 12
|
||||
# et 13. En dessous de ce seuil on reste sur pip, sans discuter.
|
||||
EL_UV_MIN_PY_MINOR=8
|
||||
|
||||
el_uv_usable() {
|
||||
# $1 = interpréteur du venv visé.
|
||||
local py="$1" minor
|
||||
command -v uv > /dev/null 2>&1 || return 1
|
||||
[ -x "${py}" ] || return 1
|
||||
minor="$("${py}" -c 'import sys;print(sys.version_info[1])' 2> /dev/null)"
|
||||
[ -n "${minor}" ] || return 1
|
||||
[ "${minor}" -ge "${EL_UV_MIN_PY_MINOR}" ] 2> /dev/null || return 1
|
||||
}
|
||||
|
||||
# API publique : installe dans le venv `$1`, avec les arguments qui suivent.
|
||||
#
|
||||
# La CIBLE est toujours explicite, jamais déduite de l'environnement. uv
|
||||
# cherche sinon un « .venv » dans le répertoire courant ou un parent, et
|
||||
# poetry.toml en déclare justement un — le dépôt porte déjà la trace de cette
|
||||
# confusion, des dépendances du pyproject Odoo s'étant retrouvées dans
|
||||
# .venv.erplibre. « --python » ferme la question.
|
||||
#
|
||||
# « uv pip sync » n'est jamais utilisé : il SUPPRIME ce qui n'est pas dans le
|
||||
# fichier d'entrée, donc pip lui-même et tout ce que Poetry a posé.
|
||||
el_pip_install() {
|
||||
local venv="$1"
|
||||
shift
|
||||
local py="${venv}/bin/python"
|
||||
local provider="${EL_PIP_PROVIDER:-auto}"
|
||||
|
||||
if [ "${provider}" != "pip" ] && el_uv_usable "${py}"; then
|
||||
echo " installation par uv ($(uv --version 2>&1 | head -1))"
|
||||
if uv pip install --python "${py}" "$@"; then
|
||||
return 0
|
||||
fi
|
||||
if [ "${provider}" = "uv" ]; then
|
||||
return 1
|
||||
fi
|
||||
# uv est plus strict que pip sur les métadonnées : il refuse des paquets
|
||||
# que pip accepte. Le repli n'est donc pas théorique.
|
||||
echo " uv a echoue : reprise avec pip." >&2
|
||||
fi
|
||||
"${py}" -m pip install "$@"
|
||||
}
|
||||
196
script/install/lib_python_provider.sh
Executable file
196
script/install/lib_python_provider.sh
Executable file
|
|
@ -0,0 +1,196 @@
|
|||
#!/usr/bin/env bash
|
||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
#
|
||||
# Bibliothèque SOURÇABLE : « une version de Python » -> « un interpréteur ».
|
||||
#
|
||||
# C'est le seul endroit du dépôt qui décide COMMENT un interpréteur est
|
||||
# obtenu. Tout le reste ne consomme que des chemins de venv, jamais pyenv ni
|
||||
# mise. Ajouter un fournisseur se fait donc ici, et nulle part ailleurs.
|
||||
#
|
||||
# Deux fournisseurs :
|
||||
# pyenv historique. Compile CPython depuis les sources : 1 à 3 min sur une
|
||||
# machine récente, bien plus sous émulation, et il lui faut une
|
||||
# douzaine de -dev (openssl, zlib, readline, sqlite, bzip2, xz, tk…).
|
||||
# mise pose un CPython PRÉCOMPILÉ (astral python-build-standalone) :
|
||||
# quelques secondes, aucun compilateur requis.
|
||||
#
|
||||
# EL_PYTHON_PROVIDER choisit : auto (défaut), mise, ou pyenv.
|
||||
#
|
||||
# En mode « auto », un interpréteur DÉJÀ présent gagne, quel que soit le
|
||||
# fournisseur qui l'a posé. Une installation pyenv qui marche n'est donc
|
||||
# jamais doublée par un second CPython de 150 Mo.
|
||||
|
||||
# Ne jamais laisser mise compiler en silence : sans build précompilée pour la
|
||||
# plateforme, il retomberait sur python-build — le moteur de pyenv — et on
|
||||
# aurait la lenteur de pyenv sans son outillage. « false » = télécharger ou
|
||||
# échouer ; l'échec nous fait basculer proprement sur le repli.
|
||||
export MISE_PYTHON_COMPILE=false
|
||||
|
||||
el_pyenv_root() {
|
||||
echo "${PYENV_ROOT:-${HOME}/.pyenv}"
|
||||
}
|
||||
|
||||
# Chemin de l'interpréteur pyenv d'une version, qu'il existe ou non.
|
||||
el_pyenv_exec_path() {
|
||||
echo "$(el_pyenv_root)/versions/$1/bin/python"
|
||||
}
|
||||
|
||||
# Interpréteur déjà posé par mise, sans le moindre accès réseau.
|
||||
el_mise_exec_path() {
|
||||
command -v mise > /dev/null 2>&1 || return 1
|
||||
local out
|
||||
out="$(mise where "python@$1" 2> /dev/null)" || return 1
|
||||
[ -n "${out}" ] && [ -x "${out}/bin/python" ] || return 1
|
||||
echo "${out}/bin/python"
|
||||
}
|
||||
|
||||
# Vrai si l'exécutable rend EXACTEMENT la version demandée. Un interpréteur
|
||||
# présent mais d'une autre version casserait Poetry, dont le pyproject exige
|
||||
# le patch au près (>=3.12.10,<3.13).
|
||||
el_python_is_version() {
|
||||
local exe="$1" want="$2" got
|
||||
[ -x "${exe}" ] || return 1
|
||||
got="$("${exe}" -c 'import platform;print(platform.python_version())' 2> /dev/null)"
|
||||
[ "${got}" = "${want}" ]
|
||||
}
|
||||
|
||||
# Vrai si l'exécutable CONVIENT : même majeure.mineure, et patch au moins
|
||||
# égal au demandé. C'est exactement ce qu'exige le pyproject — « >=3.12.10,
|
||||
# <3.13 » — et non l'égalité stricte que testait el_python_is_version.
|
||||
#
|
||||
# La distinction n'est pas théorique. Tumbleweed s390x livre python312 en
|
||||
# 3.12.13 : parfaitement utilisable, mais rejeté par l'égalité, ce qui forçait
|
||||
# pyenv à COMPILER CPython — et gcc 15.2 s'y arrête sur une erreur interne
|
||||
# dans Parser/parser.c, un fichier généré de quarante mille lignes.
|
||||
el_python_is_compatible() {
|
||||
local exe="$1" want="$2" got
|
||||
[ -x "${exe}" ] || return 1
|
||||
got="$("${exe}" -c 'import platform;print(platform.python_version())' \
|
||||
2> /dev/null)"
|
||||
[ -n "${got}" ] || return 1
|
||||
# Même majeure.mineure : 3.13 ne convient pas à un pyproject borné à <3.13.
|
||||
[ "${got%.*}" = "${want%.*}" ] || return 1
|
||||
# Patch au moins égal, comparé en version et non en chaîne (3.12.9 < 3.12.10).
|
||||
[ "$(printf '%s\n%s\n' "${want}" "${got}" | sort -V | head -1)" = "${want}" ]
|
||||
}
|
||||
|
||||
# Interpréteur de la DISTRIBUTION qui conviendrait, s'il y en a un.
|
||||
#
|
||||
# Cherché avant toute compilation : sur une architecture émulée, bâtir CPython
|
||||
# prend des dizaines de minutes quand il aboutit. Le nom suit la convention de
|
||||
# toutes les distributions, « python3.12 ».
|
||||
el_distro_python_exec() {
|
||||
local want="$1" exe
|
||||
# Des chemins SYSTEME, jamais « command -v » : dans un venv activé celui-ci
|
||||
# rend le python DU VENV, et l'on bâtirait un venv depuis un venv. Le PATH
|
||||
# d'une session interactive n'a rien à faire dans cette décision.
|
||||
for exe in "/usr/bin/python${want%.*}" "/usr/local/bin/python${want%.*}"; do
|
||||
if el_python_is_compatible "${exe}" "${want}"; then
|
||||
echo "${exe}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Installe la version via mise. Renvoie le chemin, ou échoue.
|
||||
el_mise_install() {
|
||||
local version="$1" exe
|
||||
command -v mise > /dev/null 2>&1 || return 1
|
||||
echo "---- Python ${version} via mise (precompile) ----" >&2
|
||||
mise install "python@${version}" >&2 || return 1
|
||||
exe="$(el_mise_exec_path "${version}")" || return 1
|
||||
el_python_is_version "${exe}" "${version}" || return 1
|
||||
echo "${exe}"
|
||||
}
|
||||
|
||||
# Installe la version via pyenv, en posant pyenv lui-même au besoin.
|
||||
el_pyenv_install() {
|
||||
local version="$1" root exe
|
||||
root="$(el_pyenv_root)"
|
||||
exe="$(el_pyenv_exec_path "${version}")"
|
||||
if [[ ! -d "${root}" ]]; then
|
||||
echo "---- Installation de pyenv dans ${root} ----" >&2
|
||||
curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer \
|
||||
| bash >&2 || return 1
|
||||
fi
|
||||
export PATH="${root}/bin:$PATH"
|
||||
eval "$(pyenv init - 2> /dev/null)" || true
|
||||
eval "$(pyenv virtualenv-init - 2> /dev/null)" || true
|
||||
if [[ ! -d "${root}/versions/${version}" ]]; then
|
||||
# pyenv COMPILE CPython : sans compilateur C, il télécharge l'archive,
|
||||
# lance configure et s'arrête sur « no acceptable C compiler found », après
|
||||
# avoir gaspillé le téléchargement. On le dit AVANT, et on nomme le paquet.
|
||||
if ! command -v cc > /dev/null 2>&1 \
|
||||
&& ! command -v gcc > /dev/null 2>&1; then
|
||||
echo "Aucun compilateur C : pyenv ne peut pas compiler Python." >&2
|
||||
echo " Installez le necessaire de compilation (build-essential," >&2
|
||||
echo " gcc/gcc-c++, ou le motif devel_basis) puis relancez." >&2
|
||||
return 1
|
||||
fi
|
||||
echo "---- Python ${version} via pyenv (compilation) ----" >&2
|
||||
# La liste des versions connues vient du dépôt git de pyenv : sans ce
|
||||
# « pull », une version récente est « not a known version ».
|
||||
(cd "${root}" && git pull) >&2 || true
|
||||
# Le contrôle d'erreur d'origine testait un $retVal jamais affecté ici :
|
||||
# un échec de compilation passait inaperçu jusqu'au test d'existence.
|
||||
if ! yes n | pyenv install "${version}" >&2; then
|
||||
echo "pyenv install ${version} a echoue." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
el_python_is_version "${exe}" "${version}" || return 1
|
||||
echo "${exe}"
|
||||
}
|
||||
|
||||
# API publique : imprime le chemin absolu d'un interpréteur de cette version,
|
||||
# ou rien (et rend non nul) si aucun fournisseur n'y parvient.
|
||||
el_python_exec() {
|
||||
local version="$1" provider="${EL_PYTHON_PROVIDER:-auto}" exe
|
||||
|
||||
# 1) Déjà présent ? On ne réinstalle rien et on ne touche pas au réseau.
|
||||
# C'est ce qui rend le changement sans effet pour une installation
|
||||
# pyenv existante.
|
||||
if [ "${provider}" != "mise" ]; then
|
||||
exe="$(el_pyenv_exec_path "${version}")"
|
||||
if el_python_is_version "${exe}" "${version}"; then
|
||||
echo "${exe}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [ "${provider}" != "pyenv" ]; then
|
||||
if exe="$(el_mise_exec_path "${version}")" \
|
||||
&& el_python_is_version "${exe}" "${version}"; then
|
||||
echo "${exe}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2) Un interpréteur de la DISTRIBUTION qui convient ? Rien à installer,
|
||||
# rien à compiler. Vérifié avant mise et pyenv : c'est le seul chemin
|
||||
# qui ne coûte rien, et sur s390x le seul qui aboutisse partout — mise
|
||||
# n'y publie aucun binaire, et pyenv doit compiler.
|
||||
# Uniquement en mode « auto » : demander mise ou pyenv explicitement doit
|
||||
# être respecté, sinon le réglage ne veut plus rien dire.
|
||||
if [ "${provider}" = "auto" ] \
|
||||
&& exe="$(el_distro_python_exec "${version}")"; then
|
||||
echo "Python $("${exe}" -V 2>&1 | awk '{print $2}') de la distribution :" \
|
||||
"aucune compilation." >&2
|
||||
echo "${exe}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 3) Rien de posé : il faut provisionner. mise d'abord quand il est là,
|
||||
# parce qu'il télécharge au lieu de compiler.
|
||||
if [ "${provider}" != "pyenv" ]; then
|
||||
if exe="$(el_mise_install "${version}")"; then
|
||||
echo "${exe}"
|
||||
return 0
|
||||
fi
|
||||
[ "${provider}" = "mise" ] && return 1
|
||||
command -v mise > /dev/null 2>&1 \
|
||||
&& echo "mise n'a pas pu fournir Python ${version} : repli sur pyenv." >&2
|
||||
fi
|
||||
el_pyenv_install "${version}"
|
||||
}
|
||||
89
script/install/lib_qpdf.sh
Normal file
89
script/install/lib_qpdf.sh
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env bash
|
||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
#
|
||||
# Bibliothèque SOURÇABLE : garantir un qpdf assez récent pour pikepdf.
|
||||
#
|
||||
# pikepdf 10 déclare QPDF_MIN_VERSION = "12.2.0" et se lie à la bibliothèque
|
||||
# C++ de la distribution. Là où PyPI publie une roue, rien de tout cela ne
|
||||
# s'exécute ; sur s390x il n'y en a aucune, pikepdf compile, et une version
|
||||
# trop ancienne le fait échouer sur des en-têtes qui n'existent pas encore
|
||||
# (« qpdf/QPDFJob.hh », apparu en 10.6 ; « qpdf/QPDFLogger.hh », en 11.0) puis
|
||||
# sur des constantes renommées. Le message ne nomme jamais qpdf : on lit trois
|
||||
# cents lignes de g++ avant de comprendre.
|
||||
#
|
||||
# Aucune famille n'est à jour partout, d'où le seuil plutôt qu'une liste de
|
||||
# distributions : Ubuntu 24.04 livre 11.9, AlmaLinux et Rocky 9 livrent 10.3
|
||||
# (sans même QPDFJob.hh), leurs 10 livrent 11.9 ; Ubuntu 25.10, Fedora 43 et
|
||||
# Tumbleweed dépassent le seuil et ne compilent rien.
|
||||
#
|
||||
# Ce fichier existe parce que le bloc vivait en double : corrigé côté apt, il
|
||||
# ne l'était pas côté dnf, et EL9 est ressorti sur exactement le même mur le
|
||||
# lendemain. Le seuil, la version et la façon de bâtir sont désormais écrits
|
||||
# UNE fois pour les trois familles de paquets.
|
||||
|
||||
EL_QPDF_MIN=12.2.0
|
||||
EL_QPDF_VER=12.3.2
|
||||
|
||||
# Vrai si la version passée en argument atteint le seuil. « sort -V » est le
|
||||
# seul comparateur de versions disponible partout sans dépendance, mais il
|
||||
# classe « 12.2 » AVANT « 12.2.0 » — un paquet numéroté sur deux composantes
|
||||
# aurait donc déclenché une compilation d'une demi-heure pour rien. On complète
|
||||
# les composantes manquantes avant de comparer.
|
||||
el_qpdf_ge_min() {
|
||||
local v="$1" dots
|
||||
[ -n "${v}" ] || return 1
|
||||
dots="${v//[^.]/}"
|
||||
while [ "${#dots}" -lt 2 ]; do
|
||||
v="${v}.0"
|
||||
dots="${dots}."
|
||||
done
|
||||
[ "$(printf '%s\n%s\n' "${EL_QPDF_MIN}" "${v}" | sort -V | head -1)" = "${EL_QPDF_MIN}" ]
|
||||
}
|
||||
|
||||
# Version actuellement visible du compilateur, ou « 0 ».
|
||||
# /usr/local n'est pas dans le chemin par défaut de pkg-config sur toutes les
|
||||
# familles : sans ces deux entrées, un qpdf déjà compilé passerait inaperçu et
|
||||
# on le recompilerait à chaque passage du script.
|
||||
el_qpdf_version() {
|
||||
PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:/usr/local/lib64/pkgconfig:${PKG_CONFIG_PATH}" \
|
||||
pkg-config --modversion libqpdf 2> /dev/null || echo 0
|
||||
}
|
||||
|
||||
# Compile et installe qpdf dans /usr/local si, et seulement si, ce qui est
|
||||
# présent ne suffit pas. Ne rend jamais un code non nul : l'échec est signalé
|
||||
# et l'installation continue, pikepdf dira lui-même ce qui manque.
|
||||
el_qpdf_ensure() {
|
||||
local have build dir
|
||||
have="$(el_qpdf_version)"
|
||||
if el_qpdf_ge_min "${have}"; then
|
||||
echo "qpdf ${have} >= ${EL_QPDF_MIN} : rien a compiler pour pikepdf."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "qpdf ${have} < ${EL_QPDF_MIN} requis par pikepdf : compilation de qpdf ${EL_QPDF_VER} (long en emulation)."
|
||||
build="$(mktemp -d)"
|
||||
if curl -fsSL --max-time 600 -o "${build}/qpdf.tar.gz" \
|
||||
"https://github.com/qpdf/qpdf/releases/download/v${EL_QPDF_VER}/qpdf-${EL_QPDF_VER}.tar.gz" \
|
||||
&& tar -xzf "${build}/qpdf.tar.gz" -C "${build}" \
|
||||
&& cmake -S "${build}/qpdf-${EL_QPDF_VER}" -B "${build}/build" \
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local \
|
||||
-DBUILD_DOC=OFF -DBUILD_STATIC_LIBS=OFF \
|
||||
&& cmake --build "${build}/build" -j"$(nproc)" \
|
||||
&& sudo cmake --install "${build}/build"; then
|
||||
# CMake choisit « lib » ou « lib64 » selon la famille, et /usr/local/lib64
|
||||
# n'est pas dans le chemin de ld.so d'EL : sans cette déclaration, pikepdf
|
||||
# se construirait pour échouer au CHARGEMENT, encore plus loin de la cause.
|
||||
for dir in /usr/local/lib64 /usr/local/lib; do
|
||||
if [ -e "${dir}/libqpdf.so" ]; then
|
||||
echo "${dir}" | sudo tee /etc/ld.so.conf.d/qpdf-local.conf > /dev/null
|
||||
break
|
||||
fi
|
||||
done
|
||||
sudo ldconfig
|
||||
echo "qpdf $(el_qpdf_version) installe dans /usr/local."
|
||||
else
|
||||
echo "Attention : compilation de qpdf echouee, pikepdf ne pourra pas se construire."
|
||||
fi
|
||||
rm -rf "${build}"
|
||||
}
|
||||
|
|
@ -2,12 +2,29 @@
|
|||
Red='\033[0;31m' # Red
|
||||
Color_Off='\033[0m' # Text Reset
|
||||
|
||||
./.venv.$(cat ".erplibre-version" | xargs)/bin/poetry add -vv $(grep -v ";" ./.venv.$(cat ".erplibre-version" | xargs)/build_dependency.txt | grep -v "*" )
|
||||
# poetry add -vv $(grep -v ";" ./.venv/build_dependency.txt | grep -v "*" | sed 's/==/@^/' )
|
||||
# poetry export -f ./.venv/build_dependency.txt --dev | poetry run -- pip install -r /dev/stdin
|
||||
VENV="./.venv.$(cat ".erplibre-version" | xargs)"
|
||||
BUILD="${VENV}/build_dependency.txt"
|
||||
|
||||
# Chaque ligne devient UN argument, jamais plusieurs.
|
||||
#
|
||||
# L'expansion « $(...) » non quotée découpait sur les espaces : une ligne
|
||||
# portant un marqueur — « factur-x>=4.2 ; platform_machine != "s390x" » —
|
||||
# arrivait à poetry en cinq arguments. D'où le « grep -v ';' » historique, qui
|
||||
# écartait purement et simplement ces lignes : la dépendance n'était jamais
|
||||
# ajoutée. Or « poetry add » sait lire une chaîne PEP 508 complète, marqueur
|
||||
# compris — vérifié avec son propre parseur.
|
||||
#
|
||||
# On garde le filtre sur « * » : une contrainte joker n'apporte rien à poetry.
|
||||
mapfile -t DEPS < <(grep -v '\*' "${BUILD}" | grep -v '^[[:space:]]*$')
|
||||
|
||||
if [[ ${#DEPS[@]} -eq 0 ]]; then
|
||||
echo "Aucune dépendance à ajouter depuis ${BUILD}."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"${VENV}/bin/poetry" add -vv "${DEPS[@]}"
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
echo -e "${Red}Error${Color_Off} ./script/poetry/poetry_add_build_dependency.sh"
|
||||
# cat ./.venv/build_dependency.txt
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -191,6 +191,9 @@ def combine_requirements(config):
|
|||
# remove comments at the end of module
|
||||
b = b[: b.index("#")].strip()
|
||||
comment_depend = ""
|
||||
# Défini pour TOUTE ligne : il est relu plus bas, y compris
|
||||
# pour les lignes sans marqueur.
|
||||
keep_marker = ""
|
||||
if except_sign in b:
|
||||
# current_python_version = platform.python_version()
|
||||
# current_platform = sys.platform
|
||||
|
|
@ -205,9 +208,20 @@ def combine_requirements(config):
|
|||
config.set_version_python
|
||||
)
|
||||
req = Requirement(b)
|
||||
is_compatible = req.marker.evaluate(current_platform_2)
|
||||
if not is_compatible:
|
||||
continue
|
||||
# Le nom de l'artefact généré — pyproject.odooXX_pythonYY —
|
||||
# fige la version d'Odoo et celle de Python, PAS
|
||||
# l'architecture. Évaluer « python_version » ici est donc
|
||||
# juste ; évaluer « platform_machine » y graverait
|
||||
# silencieusement l'architecture de la machine qui génère,
|
||||
# et le lock produit sur x86 devient faux sur s390x.
|
||||
# Un tel marqueur est donc CONSERVÉ et transmis à Poetry,
|
||||
# qui sait résoudre les deux branches dans un seul lock.
|
||||
if "platform_machine" in str(req.marker):
|
||||
keep_marker = str(req.marker)
|
||||
else:
|
||||
is_compatible = req.marker.evaluate(current_platform_2)
|
||||
if not is_compatible:
|
||||
continue
|
||||
# this support python_version into comment_depend, check odoo/requirements.txt
|
||||
# b, comment_depend = b.split(except_sign)
|
||||
# b = b.strip()
|
||||
|
|
@ -215,6 +229,26 @@ def combine_requirements(config):
|
|||
# print(comment_depend)
|
||||
# Rewrite dependancy : module==version
|
||||
b = req.name + str(req.specifier)
|
||||
if keep_marker:
|
||||
b += f" ; {keep_marker}"
|
||||
|
||||
# Deux lignes du même module portant des marqueurs DIFFÉRENTS
|
||||
# ne sont pas un conflit : elles s'excluent mutuellement. La
|
||||
# clé inclut donc le marqueur, sans quoi la détection de
|
||||
# versions divergentes les opposerait et en éliminerait une.
|
||||
#
|
||||
# Le nom vient de PEP 508 et non d'un découpage : avec un
|
||||
# marqueur, le premier opérateur de la ligne est souvent celui
|
||||
# du marqueur (« != »), et le découpage rendrait
|
||||
# « factur-x>=4.2 » comme nom de module — que la liste des
|
||||
# ignorés ne retrouverait plus.
|
||||
if keep_marker:
|
||||
module_name = f"{req.name} ; {keep_marker}"
|
||||
dct_requirements[module_name].add(b)
|
||||
dct_requirements_module_filename[b].append(
|
||||
str(requirements_filename)
|
||||
)
|
||||
continue
|
||||
|
||||
# Regroup requirement
|
||||
for sign in lst_sign:
|
||||
|
|
@ -258,6 +292,22 @@ def combine_requirements(config):
|
|||
config, dct_requirements, lst_sign
|
||||
)
|
||||
|
||||
# Un module dont les requirements donnent des variantes par marqueur ne doit
|
||||
# pas recevoir en plus une entrée NUE. Les manifest.py des addons déclarent
|
||||
# « factur-x » sans version ni marqueur : cette entrée s'ajouterait aux deux
|
||||
# branches et poetry recevrait trois contraintes pour le même paquet, dont
|
||||
# une sans condition — ce qui annulerait la distinction par architecture.
|
||||
marked_names = {
|
||||
key.split(";")[0].strip() for key in dct_requirements if ";" in key
|
||||
}
|
||||
for name in marked_names:
|
||||
if name in dct_requirements:
|
||||
del dct_requirements[name]
|
||||
print(
|
||||
f"{Fore.YELLOW}WARNING{Style.RESET_ALL} - '{name}' est déjà"
|
||||
" décliné par marqueur : l'entrée sans condition est ignorée."
|
||||
)
|
||||
|
||||
# Merge all requirement by insensitive
|
||||
dct_requirement_insensitive = {}
|
||||
copy_dct_requirements = dct_requirements.copy()
|
||||
|
|
@ -418,8 +468,13 @@ def combine_requirements(config):
|
|||
lst_ignore, ignore_requirement_file = get_list_ignored()
|
||||
lst_ignored_key = []
|
||||
for key in dct_requirements.keys():
|
||||
# La clé n'est pas toujours un nom nu : selon la branche qui l'a créée
|
||||
# elle porte un marqueur (« paquet ; platform_machine … ») ou une
|
||||
# version (« pymssql<=2.2.5 »). La liste d'ignorés ne nomme que le
|
||||
# module, on ramène donc la clé à son nom avant de comparer.
|
||||
bare_key = re.split(r"[\[<>=!~;]", key)[0].strip()
|
||||
for ignored in lst_ignore:
|
||||
if ignored == key.strip():
|
||||
if ignored == bare_key:
|
||||
lst_ignored_key.append(key)
|
||||
for key in lst_ignored_key:
|
||||
del dct_requirements[key]
|
||||
|
|
@ -514,6 +569,68 @@ def get_manifest_external_dependencies(config, dct_requirements, lst_sign):
|
|||
return dct_requirements
|
||||
|
||||
|
||||
def apply_marker_variants(pyproject_filename, build_dependency_filename):
|
||||
"""Rétablit les paquets déclinés en plusieurs branches par marqueur.
|
||||
|
||||
« poetry add » déduplique par nom : des deux branches de factur-x, seule la
|
||||
dernière survit, et l'architecture qui n'est pas la sienne hérite de la
|
||||
mauvaise version. Poetry sait pourtant exprimer le cas — mais uniquement
|
||||
dans le pyproject, en liste de contraintes, hors de portée du CLI. On la
|
||||
reconstruit depuis build_dependency.txt, qui porte toutes les branches.
|
||||
|
||||
:return: True si le pyproject a été réécrit, donc si le lock est à refaire
|
||||
"""
|
||||
dct_variant = defaultdict(list)
|
||||
with open(build_dependency_filename, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or ";" not in line:
|
||||
continue
|
||||
req = Requirement(line)
|
||||
dct_variant[req.name].append(
|
||||
{"version": str(req.specifier), "markers": str(req.marker)}
|
||||
)
|
||||
dct_variant = {k: v for k, v in dct_variant.items() if len(v) > 1}
|
||||
if not dct_variant:
|
||||
return False
|
||||
|
||||
with open(pyproject_filename, "r") as f:
|
||||
dct_pyproject = toml.load(f)
|
||||
dependencies = (
|
||||
dct_pyproject.get("tool", {}).get("poetry", {}).get("dependencies")
|
||||
)
|
||||
if not dependencies:
|
||||
return False
|
||||
|
||||
has_changed = False
|
||||
for name, lst_branch in dct_variant.items():
|
||||
key = next(
|
||||
(k for k in dependencies if k.lower() == name.lower()), None
|
||||
)
|
||||
if not key:
|
||||
continue
|
||||
dependencies[key] = lst_branch
|
||||
has_changed = True
|
||||
str_branch = ", ".join(
|
||||
f"{d['version']} si {d['markers']}" for d in lst_branch
|
||||
)
|
||||
print(f"Décline '{key}' par marqueur : {str_branch}.")
|
||||
|
||||
if has_changed:
|
||||
with open(pyproject_filename, "w") as f:
|
||||
toml.dump(dct_pyproject, f)
|
||||
return has_changed
|
||||
|
||||
|
||||
def call_poetry_lock(config):
|
||||
"""
|
||||
:return: True if success
|
||||
"""
|
||||
venv_dir = f".venv.{config.set_version_erplibre}"
|
||||
status = os.system(f"./{venv_dir}/bin/poetry lock")
|
||||
return status == 0
|
||||
|
||||
|
||||
def call_poetry_add_build_dependency():
|
||||
"""
|
||||
|
||||
|
|
@ -570,6 +687,15 @@ def main():
|
|||
status = call_poetry_add_build_dependency()
|
||||
if status and pyproject_toml_filename:
|
||||
sorted_dependency_poetry(pyproject_toml_filename)
|
||||
build_dependency_filename = (
|
||||
f"./.venv.{config.set_version_erplibre}/build_dependency.txt"
|
||||
)
|
||||
if apply_marker_variants(
|
||||
pyproject_toml_filename, build_dependency_filename
|
||||
):
|
||||
# Le lock écrit par « poetry add » ignore les branches qu'on
|
||||
# vient de rétablir : il ne décrit plus le pyproject.
|
||||
status = call_poetry_lock(config)
|
||||
if (
|
||||
config.force
|
||||
and not os.path.islink(poetry_default_lock_path)
|
||||
|
|
|
|||
|
|
@ -173,16 +173,73 @@ sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \
|
|||
```
|
||||
|
||||
<!-- [en] -->
|
||||
Supported Ubuntu versions: `20.04`, `22.04`, `24.04` (default), `24.10`,
|
||||
`25.04`, `25.10`. Provide an explicit image path as a positional argument to
|
||||
override the automatic download location.
|
||||
Catalog, per architecture (`deploy_qemu.py` is the source of truth):
|
||||
|
||||
| Distro | Versions | amd64 | arm64 | s390x |
|
||||
|---|---|:-:|:-:|:-:|
|
||||
| ubuntu | `24.04` (default), `25.10`, `26.04` | ✔ | ✔ | ✔ |
|
||||
| debian | `11`, `12` (default), `13` | ✔ | ✔ | — |
|
||||
| fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only |
|
||||
| almalinux | `9` (default), `10` | ✔ | ✔ | ✔ |
|
||||
| rocky | `9`, `10` (default) | ✔ | ✔ | ✔ |
|
||||
| opensuse | `16.0` (default), `tumbleweed` | ✔ | ✔ | ✔ |
|
||||
| arch | `latest` | ✔ | — | — |
|
||||
|
||||
Fedora builds s390x only for the current release, and on a separate tree
|
||||
(`fedora-secondary`) — hence the single version there.
|
||||
|
||||
`opensuse` covers two distinct products, not two versions of one. Leap `16.0`
|
||||
is numbered and stable (SLE base) and is the default. `tumbleweed` is the
|
||||
rolling one, kept as a bellwether for breakage to come: its snapshot drift is
|
||||
real, and it demands a full `zypper dup` before anything can be installed.
|
||||
|
||||
Both ship a qpdf above the pikepdf threshold, so 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.
|
||||
|
||||
Ubuntu `20.04` and `22.04` were **dropped on every architecture**: pikepdf
|
||||
needs qpdf 12.2, whose build requires C++20, and focal ships GCC 9 — it does
|
||||
not even publish `g++-10` for s390x. Python 3.8, node 10, cargo 0.67 and
|
||||
OpenSSL 1.1.1 each had a workaround; the pile of them did not.
|
||||
|
||||
## After deployment
|
||||
|
||||
<!-- [fr] -->
|
||||
Versions Ubuntu supportées : `20.04`, `22.04`, `24.04` (défaut), `24.10`,
|
||||
`25.04`, `25.10`. Fournissez un chemin d'image en argument positionnel pour
|
||||
surcharger l'emplacement de téléchargement automatique.
|
||||
Catalogue, par architecture (`deploy_qemu.py` fait autorité) :
|
||||
|
||||
| Distro | Versions | amd64 | arm64 | s390x |
|
||||
|---|---|:-:|:-:|:-:|
|
||||
| ubuntu | `24.04` (défaut), `25.10`, `26.04` | ✔ | ✔ | ✔ |
|
||||
| debian | `11`, `12` (défaut), `13` | ✔ | ✔ | — |
|
||||
| fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | `43` seule |
|
||||
| almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ |
|
||||
| rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ |
|
||||
| opensuse | `16.0` (défaut), `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` recouvre deux produits distincts, pas deux versions du même. Leap
|
||||
`16.0` est numérotée et stable (base SLE) : c'est le défaut. `tumbleweed` est
|
||||
la rolling, gardée comme banc d'essai des ruptures à venir — sa dérive
|
||||
d'instantanés est réelle, et elle impose un `zypper dup` complet avant toute
|
||||
installation.
|
||||
|
||||
Les deux livrent un qpdf au-dessus du 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.
|
||||
|
||||
Les Ubuntu `20.04` et `22.04` sont **abandonnées sur toutes les
|
||||
architectures** : pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
|
||||
et focal livre GCC 9 — elle ne publie même pas de `g++-10` pour s390x. Python
|
||||
3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur contournement ;
|
||||
leur accumulation, non.
|
||||
|
||||
## Après le déploiement
|
||||
|
||||
|
|
|
|||
|
|
@ -92,9 +92,39 @@ sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \
|
|||
--ssh-key ~/.ssh/id_ed25519.pub -y
|
||||
```
|
||||
|
||||
Versions Ubuntu supportées : `20.04`, `22.04`, `24.04` (défaut), `24.10`,
|
||||
`25.04`, `25.10`. Fournissez un chemin d'image en argument positionnel pour
|
||||
surcharger l'emplacement de téléchargement automatique.
|
||||
Catalogue, par architecture (`deploy_qemu.py` fait autorité) :
|
||||
|
||||
| Distro | Versions | amd64 | arm64 | s390x |
|
||||
|---|---|:-:|:-:|:-:|
|
||||
| ubuntu | `24.04` (défaut), `25.10`, `26.04` | ✔ | ✔ | ✔ |
|
||||
| debian | `11`, `12` (défaut), `13` | ✔ | ✔ | — |
|
||||
| fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | `43` seule |
|
||||
| almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ |
|
||||
| rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ |
|
||||
| opensuse | `16.0` (défaut), `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` recouvre deux produits distincts, pas deux versions du même. Leap
|
||||
`16.0` est numérotée et stable (base SLE) : c'est le défaut. `tumbleweed` est
|
||||
la rolling, gardée comme banc d'essai des ruptures à venir — sa dérive
|
||||
d'instantanés est réelle, et elle impose un `zypper dup` complet avant toute
|
||||
installation.
|
||||
|
||||
Les deux livrent un qpdf au-dessus du 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.
|
||||
|
||||
Les Ubuntu `20.04` et `22.04` sont **abandonnées sur toutes les
|
||||
architectures** : pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
|
||||
et focal livre GCC 9 — elle ne publie même pas de `g++-10` pour s390x. Python
|
||||
3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur contournement ;
|
||||
leur accumulation, non.
|
||||
|
||||
## Après le déploiement
|
||||
|
||||
|
|
|
|||
|
|
@ -86,9 +86,36 @@ sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \
|
|||
--ssh-key ~/.ssh/id_ed25519.pub -y
|
||||
```
|
||||
|
||||
Supported Ubuntu versions: `20.04`, `22.04`, `24.04` (default), `24.10`,
|
||||
`25.04`, `25.10`. Provide an explicit image path as a positional argument to
|
||||
override the automatic download location.
|
||||
Catalog, per architecture (`deploy_qemu.py` is the source of truth):
|
||||
|
||||
| Distro | Versions | amd64 | arm64 | s390x |
|
||||
|---|---|:-:|:-:|:-:|
|
||||
| ubuntu | `24.04` (default), `25.10`, `26.04` | ✔ | ✔ | ✔ |
|
||||
| debian | `11`, `12` (default), `13` | ✔ | ✔ | — |
|
||||
| fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only |
|
||||
| almalinux | `9` (default), `10` | ✔ | ✔ | ✔ |
|
||||
| rocky | `9`, `10` (default) | ✔ | ✔ | ✔ |
|
||||
| opensuse | `16.0` (default), `tumbleweed` | ✔ | ✔ | ✔ |
|
||||
| arch | `latest` | ✔ | — | — |
|
||||
|
||||
Fedora builds s390x only for the current release, and on a separate tree
|
||||
(`fedora-secondary`) — hence the single version there.
|
||||
|
||||
`opensuse` covers two distinct products, not two versions of one. Leap `16.0`
|
||||
is numbered and stable (SLE base) and is the default. `tumbleweed` is the
|
||||
rolling one, kept as a bellwether for breakage to come: its snapshot drift is
|
||||
real, and it demands a full `zypper dup` before anything can be installed.
|
||||
|
||||
Both ship a qpdf above the pikepdf threshold, so 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.
|
||||
|
||||
Ubuntu `20.04` and `22.04` were **dropped on every architecture**: pikepdf
|
||||
needs qpdf 12.2, whose build requires C++20, and focal ships GCC 9 — it does
|
||||
not even publish `g++-10` for s390x. Python 3.8, node 10, cargo 0.67 and
|
||||
OpenSSL 1.1.1 each had a workaround; the pile of them did not.
|
||||
|
||||
## After deployment
|
||||
|
||||
|
|
|
|||
|
|
@ -79,9 +79,13 @@ from pathlib import Path
|
|||
# transitoire. Un VM à 10G tombait « Poetry installation error » (disque
|
||||
# plein). Le qcow2 est CREUX (sparse) : une taille virtuelle plus grande ne
|
||||
# consomme rien tant qu'elle n'est pas remplie -> 20G sans surcoût réel.
|
||||
#
|
||||
# 20.04 et 22.04 ne sont plus proposées : leur chaîne d'outils ne suffit plus à
|
||||
# ERPLibre. Le mur le plus net est pikepdf, qui réclame qpdf >= 12.2, lui-même
|
||||
# en C++20 — focal livre GCC 9. S'y ajoutaient Python 3.8 à l'amorçage, node 10,
|
||||
# cargo 0.67 et OpenSSL 1.1.1. Chacun avait son contournement ; l'accumulation,
|
||||
# non.
|
||||
UBUNTU_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
||||
"20.04": ("focal", "ubuntu20.04", 2048, "20G"),
|
||||
"22.04": ("jammy", "ubuntu22.04", 2048, "20G"),
|
||||
"24.04": ("noble", "ubuntu24.04", 3072, "20G"),
|
||||
"25.10": ("questing", "ubuntu25.10", 3072, "20G"),
|
||||
"26.04": ("resolute", "ubuntu26.04", 3072, "20G"),
|
||||
|
|
@ -97,6 +101,37 @@ FEDORA_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
|||
"43": ("43", "fedora43", 2048, "20G"),
|
||||
"44": ("44", "fedora44", 2048, "20G"),
|
||||
}
|
||||
# Dérivés RHEL. Le second champ est l'identifiant libosinfo : « almalinux9 » et
|
||||
# « almalinux10 » figurent dans osinfo-db, « rocky10 » n'y est pas encore sur
|
||||
# les hôtes 24.04 — osinfo_arg() replie alors sur une valeur connue.
|
||||
# RAM : libosinfo demande 1,5 Gio en x86_64 ; on retient 2048 comme Fedora.
|
||||
ALMALINUX_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
||||
"9": ("9", "almalinux9", 2048, "20G"),
|
||||
"10": ("10", "almalinux10", 2048, "20G"),
|
||||
}
|
||||
ROCKY_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
||||
"9": ("9", "rocky9", 2048, "20G"),
|
||||
"10": ("10", "rocky10", 2048, "20G"),
|
||||
}
|
||||
# openSUSE, deux produits distincts et pas deux versions du même.
|
||||
# Leap NUMÉROTÉ, base SLE, ~2 ans de support. C'est le défaut, et le
|
||||
# seul des deux à proposer pour une installation qui doit durer.
|
||||
# Tumbleweed ROLLING, sans numéro. Gardé comme banc d'essai des ruptures à
|
||||
# venir, pas comme cible. Sa dérive d'instantanés est réelle : deux
|
||||
# VM déployées le même jour ont vu git-daemon 2.54 sur s390x et
|
||||
# 2.55 sur amd64, et l'image livrée est toujours en retard sur les
|
||||
# dépôts, d'où le « zypper dup » obligatoire avant toute install.
|
||||
#
|
||||
# Les deux dépassent le seuil qpdf de pikepdf : aucune compilation de qpdf, ce
|
||||
# qui change tout sous émulation s390x.
|
||||
#
|
||||
# osinfo : « opensuse16.0 » n'est pas encore dans osinfo-db, qui s'arrête à
|
||||
# 15.6. osinfo_arg() replie sur le DERNIER id connu de la table — d'où l'ordre,
|
||||
# Tumbleweed en second servant de repli à Leap.
|
||||
OPENSUSE_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
||||
"16.0": ("16.0", "opensuse16.0", 2048, "20G"),
|
||||
"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"),
|
||||
|
|
@ -107,6 +142,9 @@ DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = {
|
|||
"ubuntu": (UBUNTU_VERSIONS, "24.04"),
|
||||
"debian": (DEBIAN_VERSIONS, "12"),
|
||||
"fedora": (FEDORA_VERSIONS, "42"),
|
||||
"almalinux": (ALMALINUX_VERSIONS, "9"),
|
||||
"rocky": (ROCKY_VERSIONS, "10"),
|
||||
"opensuse": (OPENSUSE_VERSIONS, "16.0"),
|
||||
"arch": (ARCH_VERSIONS, "latest"),
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +152,9 @@ DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = {
|
|||
# s390x s'écrit « s390x » partout (identité), donc aucune entrée n'est requise.
|
||||
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"},
|
||||
}
|
||||
|
||||
|
|
@ -126,8 +167,43 @@ 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",)
|
||||
ARM64_DISTROS: tuple[str, ...] = ("ubuntu", "debian", "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
|
||||
# du catalogue sont réputées disponibles.
|
||||
#
|
||||
# Fedora ne construit s390x que pour la version courante, et sur une
|
||||
# arborescence à part (« fedora-secondary ») : la 41 et la 42 y sont retirées
|
||||
# (404 sur le miroir maître), la 44 n'est pour l'instant que sur certains
|
||||
# miroirs tiers. Seule la 43 est servie par dl.fedoraproject.org — vérifié.
|
||||
ARCH_ONLY_VERSIONS: dict[str, dict[str, tuple[str, ...]]] = {
|
||||
"s390x": {"fedora": ("43",)},
|
||||
}
|
||||
|
||||
|
||||
def arch_versions(distro: str, arch: str, versions) -> list[str]:
|
||||
"""Versions de `distro` réellement publiées pour `arch`."""
|
||||
only = ARCH_ONLY_VERSIONS.get(arch, {}).get(distro)
|
||||
return [v for v in versions if only is None or v in only]
|
||||
|
||||
|
||||
ARM64_DISTROS: tuple[str, ...] = (
|
||||
"ubuntu",
|
||||
"debian",
|
||||
"fedora",
|
||||
"almalinux",
|
||||
"rocky",
|
||||
"opensuse",
|
||||
)
|
||||
|
||||
|
||||
# arch générique -> distros la publiant (pour valider --arch tôt).
|
||||
ARCH_DISTRO_SUPPORT: dict[str, tuple[str, ...]] = {
|
||||
"s390x": S390X_DISTROS,
|
||||
|
|
@ -162,11 +238,23 @@ DEBIAN_CLOUD_BASES: tuple[str, ...] = (
|
|||
"https://gemmei.ftp.acc.umu.se/cdimage/cloud",
|
||||
"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
|
||||
# redirecteur envoie sur un miroir incomplet (fréquent en déploiement
|
||||
# PARALLÈLE : chaque requête peut tomber sur un miroir différent/désynchronisé).
|
||||
FEDORA_BASE_MASTER = "https://dl.fedoraproject.org/pub/fedora/linux/releases"
|
||||
# s390x est une architecture SECONDAIRE chez Fedora : ses images ne sont PAS
|
||||
# sous /pub/fedora/linux/ mais sous /pub/fedora-secondary/, et seule la
|
||||
# version courante y est construite.
|
||||
FEDORA_SECONDARY = (
|
||||
"https://download.fedoraproject.org/pub/fedora-secondary/releases"
|
||||
)
|
||||
FEDORA_SECONDARY_MASTER = (
|
||||
"https://dl.fedoraproject.org/pub/fedora-secondary/releases"
|
||||
)
|
||||
|
||||
# Répertoire de cache par défaut des images cloud (cohérent avec --disk-dir /
|
||||
# --seed-dir). L'écriture y nécessite root : le déploiement tourne de toute
|
||||
|
|
@ -212,6 +300,43 @@ def image_candidates(
|
|||
]
|
||||
if distro == "fedora":
|
||||
return [resolve_fedora_url(version, arch, dry_run)]
|
||||
if distro == "almalinux":
|
||||
# Contrairement à Fedora, AlmaLinux publie un lien « latest » STABLE
|
||||
# par majeure et par architecture : aucun index HTML à analyser. On
|
||||
# vise l'arbre de la MAJEURE (9/, 10/), qui suit la mineure courante —
|
||||
# les répertoires mineurs périmés sont retirés du miroir.
|
||||
return [
|
||||
f"{ALMALINUX_CLOUD_BASE}/{version}/cloud/{a}/images/"
|
||||
f"AlmaLinux-{version}-GenericCloud-latest.{a}.qcow2"
|
||||
]
|
||||
if distro == "rocky":
|
||||
# Même principe : l'alias « .latest » suit le point release courant, et
|
||||
# les précédents partent au vault — une URL figée casserait.
|
||||
return [
|
||||
f"{ROCKY_CLOUD_BASE}/{version}/images/{a}/"
|
||||
f"Rocky-{version}-GenericCloud.latest.{a}.qcow2"
|
||||
]
|
||||
if distro == "opensuse":
|
||||
# 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 ». Elle vaut pour les DEUX produits.
|
||||
tag = f"{a}-{a}" if arch == "s390x" else a
|
||||
if version == "tumbleweed":
|
||||
# Tumbleweed sépare les architectures secondaires sous /ports/.
|
||||
port = {"s390x": "ports/zsystems/", "arm64": "ports/aarch64/"}.get(
|
||||
arch, ""
|
||||
)
|
||||
return [
|
||||
f"{OPENSUSE_BASE}/{port}tumbleweed/appliances/"
|
||||
f"openSUSE-Tumbleweed-Minimal-VM.{tag}-Cloud.qcow2"
|
||||
]
|
||||
# Leap, lui, publie TOUTES les architectures dans un seul répertoire :
|
||||
# les chemins /ports/ équivalents rendent 404. x86_64, aarch64 et s390x
|
||||
# y sont côte à côte (relevé dans l'index de 16.0, les trois en 200).
|
||||
return [
|
||||
f"{OPENSUSE_BASE}/distribution/leap/{version}/appliances/"
|
||||
f"Leap-{version}-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"]
|
||||
|
|
@ -229,12 +354,18 @@ def resolve_fedora_url(version: str, arch: str, dry_run: bool) -> str:
|
|||
pattern = re.compile(
|
||||
rf"Fedora-Cloud-Base-Generic-{version}-[0-9.]+\.{a}\.qcow2"
|
||||
)
|
||||
# Architecture secondaire (s390x, ppc64le) : autre arborescence.
|
||||
primary, master = (
|
||||
(FEDORA_SECONDARY, FEDORA_SECONDARY_MASTER)
|
||||
if arch == "s390x"
|
||||
else (FEDORA_BASE, FEDORA_BASE_MASTER)
|
||||
)
|
||||
if dry_run:
|
||||
index = f"{FEDORA_BASE}/{version}/Cloud/{a}/images/"
|
||||
index = f"{primary}/{version}/Cloud/{a}/images/"
|
||||
return index + f"Fedora-Cloud-Base-Generic-{version}-<build>.{a}.qcow2"
|
||||
# On essaie le redirecteur (2 fois : chaque requête peut viser un miroir
|
||||
# différent) puis le serveur maître (toujours complet).
|
||||
bases = [FEDORA_BASE, FEDORA_BASE, FEDORA_BASE_MASTER]
|
||||
bases = [primary, primary, master]
|
||||
last_err = ""
|
||||
for base in bases:
|
||||
index = f"{base}/{version}/Cloud/{a}/images/"
|
||||
|
|
@ -265,6 +396,16 @@ 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":
|
||||
if version == "tumbleweed":
|
||||
return f"opensuse-tumbleweed-minimal-vm-{a}.qcow2"
|
||||
return f"opensuse-leap-{version}-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
|
||||
# (reproductibilité d'un déploiement à l'autre) ; supprimer le fichier
|
||||
# du cache suffit à repartir sur le build courant.
|
||||
return f"{distro}-{version}-genericcloud-{a}.qcow2"
|
||||
return f"fedora-cloud-{version}-{a}.qcow2"
|
||||
|
||||
|
||||
|
|
@ -1287,6 +1428,27 @@ def host_timezone() -> str:
|
|||
return "UTC"
|
||||
|
||||
|
||||
def user_groups(distro: str) -> str:
|
||||
"""Groupes secondaires du compte créé par cloud-init.
|
||||
|
||||
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(
|
||||
args: argparse.Namespace, pw_hash: str | None, ssh_keys: list[str]
|
||||
) -> str:
|
||||
|
|
@ -1297,10 +1459,15 @@ def build_cloud_config(
|
|||
"users:",
|
||||
f" - name: {args.user}",
|
||||
" sudo: ALL=(ALL) NOPASSWD:ALL",
|
||||
# « sudo » existe sur Debian ET Ubuntu ; « admin » n'existe PAS sur
|
||||
# Debian -> useradd -G ...,admin échoue et l'utilisateur n'est jamais
|
||||
# créé (login/clé SSH KO). C'était la cause du « Debian ne marche pas ».
|
||||
" groups: users, sudo",
|
||||
# Le groupe d'administration N'A PAS le même nom partout, et un nom
|
||||
# inconnu fait échouer « useradd -G » : l'utilisateur n'est alors jamais
|
||||
# créé, donc ni mot de passe ni clé SSH — la VM démarre et reste
|
||||
# inaccessible. C'était déjà la cause du « Debian ne marche pas »
|
||||
# (« admin » n'existe pas sur Debian) ; la famille RHEL et Arch ont le
|
||||
# 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: {user_groups(args.distro)}",
|
||||
" shell: /bin/bash",
|
||||
" lock_passwd: false" if pw_hash else " lock_passwd: true",
|
||||
]
|
||||
|
|
@ -1606,6 +1773,37 @@ def virt_install(
|
|||
# s390x n'a pas de port série ISA : la console est SCLP (ttysclp0), et non
|
||||
# ttyS0. Ailleurs (x86/arm64), console série classique.
|
||||
console_target = "sclp" if args.arch == "s390x" else "serial"
|
||||
# Écran virtuel pour une VM graphique. s390x en est écarté : QEMU y expose
|
||||
# bien « virtio-gpu-ccw », mais rien ne garantit que le noyau s390x de la
|
||||
# distribution embarque le pilote DRM virtio-gpu — la VM démarrerait alors
|
||||
# sur un écran noir. Le bureau distant, lui, marche partout : c'est la voie
|
||||
# retenue pour cette architecture, et le message le dit.
|
||||
graphics = args.graphics
|
||||
video = []
|
||||
if args.desktop and graphics == "none":
|
||||
if args.arch == "s390x":
|
||||
print(
|
||||
"\n s390x : pas d'écran virtuel (pilote DRM non garanti)."
|
||||
"\n Le bureau sera accessible à distance, par le réseau."
|
||||
)
|
||||
else:
|
||||
# VNC sur la boucle locale, PAS « spice,listen=none ».
|
||||
#
|
||||
# « listen=none » est le défaut de virt-install et il n'expose
|
||||
# rien : QEMU crée l'affichage mais n'ouvre AUCUN socket TCP, seul
|
||||
# le canal libvirt y mène. Un virt-manager tournant sur la machine
|
||||
# même y accède ; rien d'autre. Or l'hôte QEMU est lui-même une VM
|
||||
# ici — la console était donc inatteignable par construction, et
|
||||
# aucun « ssh -L » ne pouvait y remédier : il n'y avait pas de port
|
||||
# où aboutir.
|
||||
#
|
||||
# 127.0.0.1 n'expose rien au réseau non plus : le port n'est
|
||||
# joignable que depuis l'hôte, donc à travers un tunnel SSH. VNC
|
||||
# plutôt que SPICE parce qu'il tient en UN port — un seul « -L »
|
||||
# suffit, avec n'importe quel client. Pour revenir au comportement
|
||||
# d'avant : --graphics spice,listen=none
|
||||
graphics = "vnc,listen=127.0.0.1"
|
||||
video = ["--video", "virtio"]
|
||||
cmd = [
|
||||
"virt-install",
|
||||
# Sans --connect, un utilisateur non root vise qemu:///session : le
|
||||
|
|
@ -1635,7 +1833,7 @@ def virt_install(
|
|||
"--network",
|
||||
args.network,
|
||||
"--graphics",
|
||||
args.graphics,
|
||||
graphics,
|
||||
"--console",
|
||||
f"pty,target_type={console_target}",
|
||||
# Canal virtio de l'agent invité (org.qemu.guest_agent.0) : permet à
|
||||
|
|
@ -1644,6 +1842,7 @@ def virt_install(
|
|||
"--channel",
|
||||
"unix,target.type=virtio,target.name=org.qemu.guest_agent.0",
|
||||
]
|
||||
cmd += video
|
||||
if args.arch == "s390x":
|
||||
# s390x (IBM Z) : machine s390-ccw-virtio, amorçage IPL/zipl depuis le
|
||||
# disque (ni BIOS ni UEFI/OVMF -> aucun --boot).
|
||||
|
|
@ -1858,6 +2057,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
default="none",
|
||||
help="Argument --graphics (défaut : none).",
|
||||
)
|
||||
g_vm.add_argument(
|
||||
"--desktop",
|
||||
action="store_true",
|
||||
help="VM graphique : écran virtuel SPICE là où l'architecture le "
|
||||
"permet. Les paquets GNOME sont posés par la commande d'installation, "
|
||||
"pas ici.",
|
||||
)
|
||||
g_vm.add_argument(
|
||||
"--osinfo", help="Force la valeur --osinfo (sinon déduite)."
|
||||
)
|
||||
|
|
@ -2084,6 +2290,14 @@ def main() -> None:
|
|||
f"Architecture {args.arch} indisponible pour {args.distro!r} : "
|
||||
f"images cloud publiées seulement pour {', '.join(supported)}."
|
||||
)
|
||||
# Toutes les versions d'une distro ne sont pas publiées sur toutes les
|
||||
# architectures : le dire ici plutôt que d'échouer au téléchargement.
|
||||
ok_versions = arch_versions(args.distro, args.arch, versions)
|
||||
if args.version not in ok_versions:
|
||||
sys.exit(
|
||||
f"{args.distro} {args.version} n'est pas publié en {args.arch}.\n"
|
||||
f" Versions disponibles : {', '.join(ok_versions) or 'aucune'}."
|
||||
)
|
||||
code, default_osinfo, min_ram, min_disk = versions[args.version]
|
||||
if args.codename:
|
||||
code = args.codename
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
<!---------------------------->
|
||||
|
||||
<!-- [en] -->
|
||||
# Installation Ubuntu 20.04
|
||||
# Installation Ubuntu 24.04
|
||||
|
||||
<!-- [fr] -->
|
||||
# Installation Ubuntu 20.04
|
||||
# Installation Ubuntu 24.04
|
||||
|
||||
<!-- [common] -->
|
||||
sudo apt install libcairo2-dev
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
# Installation Ubuntu 20.04
|
||||
# Installation Ubuntu 24.04
|
||||
|
||||
sudo apt install libcairo2-dev
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
# Installation Ubuntu 20.04
|
||||
# Installation Ubuntu 24.04
|
||||
|
||||
sudo apt install libcairo2-dev
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -37,6 +37,11 @@ SSH_OPTS = (
|
|||
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
|
||||
"-o ConnectTimeout=8"
|
||||
)
|
||||
# Pour les ssh NON INTERACTIFS : « -n » branche leur entrée sur /dev/null.
|
||||
# Ceinture et bretelles avec le stdin du processus détaché — un ssh qui lit le
|
||||
# terminal vole les frappes du shell, et le diagnostic est très difficile.
|
||||
# Jamais pour un ssh interactif (touche « s »), qui doit garder le clavier.
|
||||
SSH_OPTS_BATCH = f"{SSH_OPTS} -n"
|
||||
|
||||
|
||||
def session_dir() -> Path:
|
||||
|
|
@ -98,6 +103,15 @@ def _launch_one(
|
|||
msg_wait = t("Waiting for the VM to start (boot + cloud-init)")
|
||||
msg_slow = t("(an emulated architecture can be slow; this is normal)")
|
||||
msg_ready = t("VM ready - starting the ERPLibre install")
|
||||
msg_giveup = t(
|
||||
"cloud-init still running after 20 min - install starts anyway"
|
||||
" (it waits for cloud-init first)"
|
||||
)
|
||||
msg_novirsh = t(
|
||||
"WARNING libvirt unreachable: the IP will not be refreshed"
|
||||
" (libvirt group? re-login required)"
|
||||
)
|
||||
msg_moved = t("DHCP lease moved:")
|
||||
# L'IP est RÉSOLUE À CHAQUE TOUR, jamais figée. Au 1er boot la VM prend un
|
||||
# bail sous le nom par défaut de l'image, puis cloud-init pose le vrai nom
|
||||
# d'hôte et le client DHCP en redemande un AUTRE. L'adresse connue au
|
||||
|
|
@ -130,17 +144,38 @@ def _launch_one(
|
|||
'vsh() { virsh --connect qemu:///system "$@" 2>/dev/null '
|
||||
'|| sudo -n virsh --connect qemu:///system "$@" 2>/dev/null; }; '
|
||||
)
|
||||
# Deux trous rendaient cette ré-résolution incapable de rattraper une IP
|
||||
# qui bouge — le cas exact où elle sert :
|
||||
#
|
||||
# - « vsh » est muet des deux côtés. Quand libvirt est injoignable (hors du
|
||||
# groupe libvirt, et « sudo -n » refusé faute de tty dans ce processus
|
||||
# détaché), la ré-résolution ne renvoie JAMAIS rien : l'IP de départ est
|
||||
# gardée jusqu'au bout sans qu'une seule ligne du log ne le dise.
|
||||
# - le repli sur les baux exigeait une réponse sur le port 22. Or dnsmasq
|
||||
# ne garde qu'un bail par MAC : quand cloud-init pose le vrai nom d'hôte
|
||||
# et que le client DHCP redemande, le bail DÉPLACE l'adresse. L'ancienne
|
||||
# n'appartient plus à cette VM, mais on l'attendait quand même — et sshd
|
||||
# n'y répondra jamais.
|
||||
refresh = (
|
||||
(
|
||||
f"raw=$(vsh domifaddr {name_q} --source lease); vrc=$?; "
|
||||
'if [ $vrc -ne 0 ] && [ -z "$vwarn" ]; then vwarn=1; '
|
||||
f"echo {shlex.quote(' ' + msg_novirsh)} >> {log_q}; fi; "
|
||||
"cands=$(echo \"$raw\" | grep -oE '([0-9]{1,3}\\.){3}[0-9]{1,3}' "
|
||||
"| grep -v '^127\\.'); "
|
||||
f"n=$(vsh domifaddr {name_q} --source agent "
|
||||
"| grep -oE '([0-9]{1,3}\\.){3}[0-9]{1,3}' "
|
||||
"| grep -v '^127\\.' | head -1); "
|
||||
'if [ -z "$n" ]; then '
|
||||
f"for c in $(vsh domifaddr {name_q} --source lease "
|
||||
"| grep -oE '([0-9]{1,3}\\.){3}[0-9]{1,3}' "
|
||||
"| grep -v '^127\\.'); do "
|
||||
"for c in $cands; do "
|
||||
'timeout 2 bash -c "echo > /dev/tcp/$c/22" 2>/dev/null '
|
||||
'&& n="$c"; done; fi; '
|
||||
# Le bail ne mentionne plus l'adresse courante : elle est périmée,
|
||||
# on suit le bail sans attendre que sshd réponde.
|
||||
'if [ -z "$n" ] && [ -n "$cands" ] && '
|
||||
'! echo "$cands" | grep -Fqx "$ip"; then '
|
||||
'n=$(echo "$cands" | tail -1); '
|
||||
f'echo " {msg_moved} $ip -> $n" >> {log_q}; fi; '
|
||||
'[ -n "$n" ] && ip="$n"; '
|
||||
)
|
||||
if name
|
||||
|
|
@ -151,25 +186,41 @@ def _launch_one(
|
|||
f"{vsh if name else ''}"
|
||||
f"echo {shlex.quote('== ' + msg_wait + ' ==')} >> {log_q}; "
|
||||
f"echo {shlex.quote(' ' + msg_slow)} >> {log_q}; "
|
||||
f"seen=0; "
|
||||
f"for i in $(seq 1 240); do "
|
||||
f"{refresh}"
|
||||
f'st=$(ssh {SSH_OPTS} -o BatchMode=yes "erplibre@$ip" '
|
||||
f'st=$(ssh {SSH_OPTS_BATCH} -o BatchMode=yes "erplibre@$ip" '
|
||||
f"{shlex.quote(ci_probe)} 2>/dev/null); "
|
||||
f'case "$st" in '
|
||||
f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) break;; "
|
||||
f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) seen=1; break;; "
|
||||
f"esac; "
|
||||
f"if [ $((i % 6)) -eq 0 ]; then "
|
||||
f'echo " ... $((i*5))s ($ip)" >> {log_q}; fi; '
|
||||
f"sleep 5; done; "
|
||||
# La boucle peut s'ÉPUISER au lieu de rompre : sous émulation,
|
||||
# cloud-init dépasse volontiers 20 min. Annoncer « VM prête » dans les
|
||||
# deux cas donnait un message faux juste avant le plus long silence du
|
||||
# log — c'est l'installation qui attend alors la fin de cloud-init.
|
||||
f'if [ "$seen" = 1 ]; then '
|
||||
f"echo {shlex.quote('== ' + msg_ready + ' ==')} >> {log_q}; "
|
||||
f"else echo {shlex.quote('== ' + msg_giveup + ' ==')} >> {log_q}; fi; "
|
||||
f'echo " → $ip" >> {log_q}; '
|
||||
f'ssh {SSH_OPTS} "erplibre@$ip" {shlex.quote(remote_cmd)} '
|
||||
f'ssh {SSH_OPTS_BATCH} "erplibre@$ip" {shlex.quote(remote_cmd)} '
|
||||
f">> {log_q} 2>&1; "
|
||||
f'echo "{EXIT_MARKER} $?" >> {log_q}'
|
||||
)
|
||||
# setsid -f : le process survit à la fermeture du menu / du dashboard.
|
||||
# stdin sur /dev/null : SANS lui, le descripteur 0 du processus détaché
|
||||
# reste le TERMINAL. « setsid » lui retire le terminal de contrôle, mais ne
|
||||
# ferme aucun descripteur — et ssh, lui, LIT son entrée pour la transmettre
|
||||
# à la commande distante. Deux lecteurs se partagent alors le clavier : une
|
||||
# frappe sur deux part vers l'installation au lieu du shell, et il faut
|
||||
# appuyer plusieurs fois pour qu'une lettre arrive. Le symptôme survit à
|
||||
# todo.py, puisque l'installation continue une demi-heure après sa
|
||||
# fermeture — d'où un terminal qui « bogue » sans cause visible.
|
||||
subprocess.Popen(
|
||||
["setsid", "-f", "bash", "-c", wrapper],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
|
|
@ -212,7 +263,13 @@ def launch_installs(vms: list[dict], branch: str, remote_cmd: str) -> str:
|
|||
# En-tête d'emblée (date/distro/version/arch) : le log n'est jamais
|
||||
# vide, l'utilisateur voit tout de suite QUOI s'installe.
|
||||
Path(log_path).write_text(_log_header(vm, branch, when))
|
||||
_launch_one(vm["ip"], remote_cmd, log_path, vm["name"])
|
||||
# Une VM peut porter SA commande : depuis que le type de VM (serveur
|
||||
# ou bureau) se choisit machine par machine, le script distant n'est
|
||||
# plus le même pour toutes. `remote_cmd` reste le défaut, ce qui laisse
|
||||
# intacts les appelants qui n'en fournissent qu'une.
|
||||
_launch_one(
|
||||
vm["ip"], vm.get("remote_cmd") or remote_cmd, log_path, vm["name"]
|
||||
)
|
||||
entries.append(
|
||||
{
|
||||
"name": vm["name"],
|
||||
|
|
@ -234,6 +291,20 @@ def launch_installs(vms: list[dict], branch: str, remote_cmd: str) -> str:
|
|||
return manifest_path
|
||||
|
||||
|
||||
def finished_at(log_path: str, fallback: float) -> float:
|
||||
"""Instant où l'installation s'est RÉELLEMENT arrêtée.
|
||||
|
||||
La dernière écriture dans le log, c'est-à-dire le marqueur de sortie. On
|
||||
ne peut pas prendre « maintenant » : le suivi est détachable, et il
|
||||
observe souvent l'état final longtemps après. Rouvrir le tableau de bord
|
||||
une heure plus tard ajoutait cette heure à la durée affichée, comme si
|
||||
l'installation avait tourné pendant tout ce temps."""
|
||||
try:
|
||||
return os.path.getmtime(log_path)
|
||||
except OSError:
|
||||
return fallback
|
||||
|
||||
|
||||
def read_status(log_path: str) -> tuple[str, int | None]:
|
||||
"""(état, code) d'un log : pending / running / done / failed. Ne lit que
|
||||
la FIN du fichier (le marqueur de sortie est sur la dernière ligne) : lire
|
||||
|
|
@ -261,6 +332,46 @@ def read_status(log_path: str) -> tuple[str, int | None]:
|
|||
return "running", None
|
||||
|
||||
|
||||
def run_progress(run: dict) -> dict:
|
||||
"""Avancement d'un run : combien de VM tournent encore, et depuis quand
|
||||
plus rien n'a été écrit. `idle` sert à distinguer une install vivante d'un
|
||||
run laissé pour mort — le marqueur de sortie manque dans les deux cas."""
|
||||
active = final = 0
|
||||
latest = 0.0
|
||||
for vm in run.get("vms", []):
|
||||
log = vm.get("log") or ""
|
||||
state, _code = read_status(log)
|
||||
if state in ("done", "failed"):
|
||||
final += 1
|
||||
else:
|
||||
active += 1
|
||||
try:
|
||||
latest = max(latest, os.path.getmtime(log))
|
||||
except OSError:
|
||||
pass
|
||||
return {
|
||||
"active": active,
|
||||
"final": final,
|
||||
"total": active + final,
|
||||
"idle": (time.time() - latest) if latest else None,
|
||||
}
|
||||
|
||||
|
||||
def active_run():
|
||||
"""Le run le PLUS RÉCENT s'il a encore des VM en cours, sinon None.
|
||||
|
||||
Les installs tournent détachées (`setsid -f`) : fermer le terminal laisse
|
||||
le travail se poursuivre mais fait perdre la seule vue dessus. On ne
|
||||
regarde que le dernier run — un run ancien resté sans marqueur de sortie
|
||||
signalerait éternellement une install fantôme."""
|
||||
runs = list_install_runs()
|
||||
if not runs:
|
||||
return None
|
||||
run = dict(runs[0])
|
||||
run.update(run_progress(run))
|
||||
return run if run["total"] and run["active"] else None
|
||||
|
||||
|
||||
# Listes d'ignore reprises de script/test/run_parallel_test.py (erreurs/
|
||||
# avertissements connus et bénins) : on réutilise la MÊME logique de détection
|
||||
# que la suite de tests ERPLibre pour analyser les logs d'installation.
|
||||
|
|
@ -731,13 +842,134 @@ def virsh_domstates() -> dict:
|
|||
# --------------------------------------------------------------------------- #
|
||||
# Dashboard Textual
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Largeurs de colonnes du tableau de suivi. Une seule source : la
|
||||
# construction du tableau les lit ici, et « 0 » y revient. Les redéclarer à
|
||||
# deux endroits, c'est garantir qu'un jour la remise à zéro rendra autre chose
|
||||
# que ce qui était affiché au départ.
|
||||
COL_DEFAULT_WIDTHS = {
|
||||
"seq": 3,
|
||||
"vm": 22,
|
||||
"arch": 7,
|
||||
"err": 4,
|
||||
"state": 8,
|
||||
"odoo": 6,
|
||||
"elapsed": 7,
|
||||
"disk": 8,
|
||||
}
|
||||
|
||||
|
||||
# Parties d'une remise à jour, dans l'ordre où elles doivent tourner : les
|
||||
# paquets système d'abord (compilateurs et en-têtes), le code ensuite, les
|
||||
# dépendances Python en dernier — elles se compilent contre les deux premiers.
|
||||
UPDATE_PARTS = (
|
||||
("system", "Paquets systeme (apt/dnf/pacman/zypper)"),
|
||||
("git", "Depots git (ERPLibre + addons)"),
|
||||
("python", "Dependances Python (poetry)"),
|
||||
)
|
||||
EL_DIR = "~/git/erplibre"
|
||||
|
||||
|
||||
def update_remote_cmd(parts) -> str:
|
||||
"""Commande exécutée DANS la VM pour remettre à jour ce qui est coché.
|
||||
|
||||
Rien n'est enchaîné par « && » : une partie qui échoue ne doit pas
|
||||
empêcher les suivantes, et son code de retour se lit dans la sortie.
|
||||
Chaque bloc s'annonce, sinon un log de mise à jour est illisible."""
|
||||
chosen = [p for p in parts if p in dict(UPDATE_PARTS)]
|
||||
if not chosen:
|
||||
return ""
|
||||
out = ["set -u"]
|
||||
if "system" in chosen:
|
||||
out.append(
|
||||
'echo "== Paquets systeme =="; '
|
||||
"if command -v apt-get >/dev/null 2>&1; then "
|
||||
"sudo apt-get -o DPkg::Lock::Timeout=600 update -qq "
|
||||
"&& sudo DEBIAN_FRONTEND=noninteractive "
|
||||
"apt-get -o DPkg::Lock::Timeout=600 -y upgrade; "
|
||||
"elif command -v dnf >/dev/null 2>&1; then "
|
||||
"sudo dnf -y upgrade --refresh; "
|
||||
"elif command -v pacman >/dev/null 2>&1; then "
|
||||
# Arch ne supporte pas la mise à jour partielle : -Syu, jamais -S.
|
||||
"pgrep -x pacman >/dev/null 2>&1 "
|
||||
"|| sudo rm -f /var/lib/pacman/db.lck; "
|
||||
"sudo pacman -Syu --noconfirm; "
|
||||
"elif command -v zypper >/dev/null 2>&1; then "
|
||||
". /etc/os-release; "
|
||||
'case "$ID" in *tumbleweed*) '
|
||||
"sudo zypper --non-interactive dup "
|
||||
"--auto-agree-with-licenses --allow-vendor-change;; "
|
||||
"*) sudo zypper --non-interactive up "
|
||||
"--auto-agree-with-licenses;; esac; "
|
||||
'else echo "Gestionnaire de paquets inconnu"; fi; '
|
||||
)
|
||||
if "git" in chosen:
|
||||
out.append(
|
||||
'echo "== Depots git =="; '
|
||||
f"cd {EL_DIR} || exit 1; "
|
||||
# --ff-only : une VM ne doit jamais fusionner toute seule. Un
|
||||
# historique divergent s'arrête ici, visiblement.
|
||||
"git pull --ff-only; "
|
||||
# Les addons viennent de Google Repo, pas de git : c'est le script
|
||||
# de l'installation qui sait les synchroniser.
|
||||
"./script/install/install_git_repo.sh; "
|
||||
)
|
||||
if "python" in chosen:
|
||||
out.append(
|
||||
'echo "== Dependances Python =="; '
|
||||
f"cd {EL_DIR} || exit 1; "
|
||||
# La phase « poetry » seule : ni venvs ni repo, juste les paquets.
|
||||
"EL_PHASE=poetry ./script/install/install_locally.sh; "
|
||||
)
|
||||
out.append('echo "== Mise a jour terminee =="')
|
||||
return "".join(out[:1]) + "; " + "".join(out[1:])
|
||||
|
||||
|
||||
def restart_odoo_cmd() -> str:
|
||||
"""Redémarre le service ERPLibre, ou le lance à la main s'il n'existe pas.
|
||||
|
||||
Une VM installée sans profil de production n'a pas forcément l'unité
|
||||
systemd : le repli dit quoi faire plutôt que d'échouer sans un mot."""
|
||||
return (
|
||||
"if systemctl list-unit-files 2>/dev/null | grep -q '^erplibre'; then "
|
||||
"sudo systemctl restart erplibre.service "
|
||||
"&& sudo systemctl --no-pager --lines=15 status erplibre.service; "
|
||||
"else echo 'Pas de service erplibre : lancez ./run.sh dans "
|
||||
f"{EL_DIR}'; fi"
|
||||
)
|
||||
|
||||
|
||||
def delete_vm_cmd(name: str, with_disks: bool) -> str:
|
||||
"""Efface la VM sur l'HÔTE. Même séquence que « TODO._qemu_delete_vm » :
|
||||
arrêt, retrait de la définition (nvram si UEFI, repli sinon), puis les
|
||||
disques à la demande."""
|
||||
q = shlex.quote(name)
|
||||
cmd = (
|
||||
f"sudo virsh destroy {q} 2>/dev/null; "
|
||||
f"sudo virsh undefine {q} --nvram 2>/dev/null "
|
||||
f"|| sudo virsh undefine {q}"
|
||||
)
|
||||
if with_disks:
|
||||
disk = shlex.quote(f"/var/lib/libvirt/images/{name}.qcow2")
|
||||
seed = shlex.quote(f"/var/lib/libvirt/images/iso/{name}-seed.iso")
|
||||
cmd += f"; sudo rm -f {disk} {seed}"
|
||||
return cmd
|
||||
|
||||
|
||||
def run_monitor(manifest_path: str, run_app: bool = True):
|
||||
"""Ouvre le dashboard Textual sur un manifeste d'installation. `run_app`
|
||||
à False renvoie l'instance sans la lancer (tests headless)."""
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import DataTable, Footer, Header, RichLog, Static
|
||||
from textual.widgets import (
|
||||
Button,
|
||||
Checkbox,
|
||||
DataTable,
|
||||
Footer,
|
||||
Header,
|
||||
RichLog,
|
||||
Static,
|
||||
)
|
||||
|
||||
manifest = json.loads(Path(manifest_path).read_text())
|
||||
started = manifest.get("started", time.time())
|
||||
|
|
@ -786,6 +1018,84 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
def action_dismiss(self) -> None:
|
||||
self.dismiss()
|
||||
|
||||
class ConfirmScreen(ModalScreen):
|
||||
"""Confirmation d'une action irréversible. Le bouton dangereux n'est
|
||||
PAS le défaut : il faut le viser, pas juste appuyer sur Entrée."""
|
||||
|
||||
BINDINGS = [("escape", "cancel", "Annuler")]
|
||||
|
||||
def __init__(self, title, lines, danger_label):
|
||||
super().__init__()
|
||||
self._title = title
|
||||
self._lines = lines
|
||||
self._danger = danger_label
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="confbox"):
|
||||
yield Static(self._title, id="conftitle")
|
||||
for line in self._lines:
|
||||
yield Static(line)
|
||||
with Horizontal(id="confbtns"):
|
||||
yield Button("Annuler", variant="primary", id="c_no")
|
||||
yield Button(self._danger, variant="error", id="c_yes")
|
||||
|
||||
def on_button_pressed(self, event) -> None:
|
||||
self.dismiss(event.button.id == "c_yes")
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.dismiss(False)
|
||||
|
||||
class VmActionsScreen(ModalScreen):
|
||||
"""Les opérations d'une VM, rassemblées en un seul endroit.
|
||||
|
||||
Plutôt que trois touches de plus dans un pied de page qui en compte
|
||||
déjà neuf : on voit ce qu'on va faire, et sur quelle machine."""
|
||||
|
||||
BINDINGS = [("escape", "cancel", "Fermer")]
|
||||
|
||||
def __init__(self, vm):
|
||||
super().__init__()
|
||||
self._vm = vm
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="actbox"):
|
||||
yield Static(
|
||||
f"Actions — {self._vm['name']} ({self._vm.get('ip') or '?'})",
|
||||
id="acttitle",
|
||||
)
|
||||
yield Static("Remettre à jour", classes="actgroup")
|
||||
for key, label in UPDATE_PARTS:
|
||||
yield Checkbox(label, value=True, id=f"u_{key}")
|
||||
yield Button(
|
||||
"Lancer la mise à jour",
|
||||
variant="primary",
|
||||
id="a_update",
|
||||
)
|
||||
yield Static("Service", classes="actgroup")
|
||||
yield Button("Redémarrer Odoo", id="a_restart")
|
||||
yield Static("Irréversible", classes="actdanger")
|
||||
yield Button(
|
||||
"Supprimer la VM et ses disques",
|
||||
variant="error",
|
||||
id="a_delete",
|
||||
)
|
||||
|
||||
def on_button_pressed(self, event) -> None:
|
||||
if event.button.id == "a_update":
|
||||
parts = [
|
||||
k
|
||||
for k, _lbl in UPDATE_PARTS
|
||||
if self.query_one(f"#u_{k}", Checkbox).value
|
||||
]
|
||||
self.dismiss(("update", parts))
|
||||
elif event.button.id == "a_restart":
|
||||
self.dismiss(("restart", []))
|
||||
elif event.button.id == "a_delete":
|
||||
self.dismiss(("delete", []))
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.dismiss(None)
|
||||
|
||||
ICON = {
|
||||
"pending": "⏳",
|
||||
"running": "⏳",
|
||||
|
|
@ -795,7 +1105,17 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
|
||||
class Monitor(App):
|
||||
CSS = """
|
||||
DataTable { width: 74; height: 1fr; overflow-x: auto; border: solid $accent; }
|
||||
/* « width: 74 » figeait la table : au-delà, les colonnes élargies
|
||||
n'étaient plus atteignables, et la barre horizontale ne s'affichait
|
||||
pas faute de place réservée. « scrollbar-size » la rend VISIBLE plutôt
|
||||
que devinable, et « max-width » laisse la table suivre l'élargissement
|
||||
des colonnes sans manger tout l'écran. */
|
||||
DataTable {
|
||||
width: auto; max-width: 60%; height: 1fr;
|
||||
overflow-x: auto; overflow-y: auto;
|
||||
scrollbar-size-horizontal: 1; scrollbar-size-vertical: 1;
|
||||
border: solid $accent;
|
||||
}
|
||||
RichLog { border: solid $accent; }
|
||||
#telemetry { height: 1; color: $text-muted; }
|
||||
#stats { height: 1; color: $accent; }
|
||||
|
|
@ -808,6 +1128,21 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
}
|
||||
#errtitle { height: 1; color: $accent; text-style: bold; }
|
||||
#errlog { height: 1fr; border: solid $accent; }
|
||||
VmActionsScreen { align: center middle; }
|
||||
#actbox {
|
||||
width: 62; height: auto; padding: 1 2;
|
||||
border: thick $accent; background: $surface;
|
||||
}
|
||||
#acttitle { color: $accent; text-style: bold; padding-bottom: 1; }
|
||||
.actgroup { color: $accent; text-style: bold; padding: 1 0 0 0; }
|
||||
.actdanger { color: $error; text-style: bold; padding: 1 0 0 0; }
|
||||
ConfirmScreen { align: center middle; }
|
||||
#confbox {
|
||||
width: 66; height: auto; padding: 1 2;
|
||||
border: thick $error; background: $surface;
|
||||
}
|
||||
#conftitle { color: $error; text-style: bold; padding-bottom: 1; }
|
||||
#confbtns { height: auto; padding-top: 1; }
|
||||
"""
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quitter (détaché)"),
|
||||
|
|
@ -817,6 +1152,14 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
("f", "follow", "Suivre"),
|
||||
("c", "copy_log", "Copier log"),
|
||||
("d", "details", "Détails erreurs"),
|
||||
("a", "vm_actions", "Actions VM"),
|
||||
# Mêmes touches que la TUI mail, qui redimensionne ses volets :
|
||||
# « + » élargit, « - » rétrécit, « 0 » remet tout d'aplomb.
|
||||
("plus", "col_grow", "Colonne +"),
|
||||
("minus", "col_shrink", "Colonne -"),
|
||||
("0", "col_reset", "Colonnes par défaut"),
|
||||
("less_than_sign", "col_prev", "Colonne précédente"),
|
||||
("greater_than_sign", "col_next", "Colonne suivante"),
|
||||
("p", "pause_all", "Pause tout"),
|
||||
("o", "resume_all", "Reprendre tout"),
|
||||
]
|
||||
|
|
@ -824,6 +1167,9 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
def __init__(self):
|
||||
super().__init__()
|
||||
self._offsets = {vm["name"]: 0 for vm in vms}
|
||||
# Colonne visee par « + » / « - » : le nom de VM, celle qu'on a
|
||||
# vraiment besoin d'elargir. « < » et « > » la deplacent.
|
||||
self._col_target = list(COL_DEFAULT_WIDTHS).index("vm")
|
||||
self._selected = vms[0]["name"] if vms else None
|
||||
self._follow = True
|
||||
# Statuts TERMINAUX mémorisés : une VM finie n'est plus relue
|
||||
|
|
@ -873,20 +1219,35 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
# tronqué (« ❌ effacée », « ⏸ en pause » lisibles) ; la table
|
||||
# défile horizontalement (overflow-x) pour les noms de VM longs.
|
||||
# « # » : numéro de séquence de la VM (première colonne).
|
||||
table.add_column("#", key="seq", width=3)
|
||||
table.add_column("VM", key="vm", width=22)
|
||||
table.add_column("⚠", key="err", width=4)
|
||||
table.add_column("#", key="seq", width=COL_DEFAULT_WIDTHS["seq"])
|
||||
table.add_column("VM", key="vm", width=COL_DEFAULT_WIDTHS["vm"])
|
||||
# L'architecture explique à elle seule qu'une installation dure
|
||||
# dix fois plus longtemps : s390x et arm64 sont ÉMULÉES sur un
|
||||
# hôte amd64. La voir évite de chercher la panne ailleurs.
|
||||
table.add_column(
|
||||
"Arch", key="arch", width=COL_DEFAULT_WIDTHS["arch"]
|
||||
)
|
||||
table.add_column("⚠", key="err", width=COL_DEFAULT_WIDTHS["err"])
|
||||
# width=8 : le plus long libellé restant est « ⏸ pause » (7) —
|
||||
# « effacée » (10) est remplacé par l'icône 🗑 (voir plus bas).
|
||||
table.add_column("État", key="state", width=8)
|
||||
table.add_column(
|
||||
"État", key="state", width=COL_DEFAULT_WIDTHS["state"]
|
||||
)
|
||||
# « Odoo » : l'UI web répond-elle sur :8069 ? (🟢 up / — down)
|
||||
table.add_column("Odoo", key="odoo", width=6)
|
||||
table.add_column("Durée", key="elapsed", width=7)
|
||||
table.add_column("Disque", key="disk", width=8)
|
||||
table.add_column(
|
||||
"Odoo", key="odoo", width=COL_DEFAULT_WIDTHS["odoo"]
|
||||
)
|
||||
table.add_column(
|
||||
"Durée", key="elapsed", width=COL_DEFAULT_WIDTHS["elapsed"]
|
||||
)
|
||||
table.add_column(
|
||||
"Disque", key="disk", width=COL_DEFAULT_WIDTHS["disk"]
|
||||
)
|
||||
for i, vm in enumerate(vms, 1):
|
||||
table.add_row(
|
||||
str(i),
|
||||
vm["name"],
|
||||
vm.get("arch") or "?",
|
||||
"",
|
||||
"⏳",
|
||||
"—",
|
||||
|
|
@ -1041,7 +1402,14 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
continue
|
||||
ds = self._domstate.get(name)
|
||||
if ds == "gone":
|
||||
self._final[name] = ("deleted", None, now - started)
|
||||
# Même bornage que les autres états terminaux : une
|
||||
# VM effacée ne « tourne » plus depuis la dernière
|
||||
# ligne de son log.
|
||||
self._final[name] = (
|
||||
"deleted",
|
||||
None,
|
||||
max(0.0, finished_at(vm["log"], now) - started),
|
||||
)
|
||||
# Icône seule (VM effacée) : évite « ❌ effacée » (10
|
||||
# cellules) qui forçait une colonne État large.
|
||||
self._set_cell(table, name, "state", "🗑")
|
||||
|
|
@ -1051,7 +1419,11 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
continue
|
||||
state, code = st
|
||||
if state in ("done", "failed"):
|
||||
elapsed = now - started
|
||||
# Bornée à la dernière écriture du log, pas à l'instant
|
||||
# présent : le suivi peut être rouvert bien plus tard.
|
||||
elapsed = max(
|
||||
0.0, finished_at(vm["log"], now) - started
|
||||
)
|
||||
self._final[name] = (state, code, elapsed)
|
||||
# Les échecs sont enregistrés eux aussi (ok=False) :
|
||||
# sans eux, aucun taux de réussite n'est calculable.
|
||||
|
|
@ -1096,7 +1468,8 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
maxd = f" · max {self._fmt(max_dur)}" if max_dur else ""
|
||||
self.sub_title = (
|
||||
f"{done}/{len(vms)} {t('completed')} · "
|
||||
f"{self._fmt(now - started)}{eta}{maxd}"
|
||||
f"{self._fmt(self._total_elapsed(now, started))}"
|
||||
f"{eta}{maxd}"
|
||||
)
|
||||
if tele:
|
||||
self.query_one("#telemetry", Static).update(tele)
|
||||
|
|
@ -1433,6 +1806,180 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
|
||||
# -- largeur des colonnes ---------------------------------------- #
|
||||
COL_STEP = 2
|
||||
COL_MIN = 3
|
||||
COL_MAX = 60
|
||||
|
||||
def _cursor_column(self):
|
||||
"""Colonne VISEE par le redimensionnement.
|
||||
|
||||
Surtout pas « table.cursor_column » : le curseur est en mode
|
||||
LIGNE, sa colonne reste donc a 0 quoi qu'on fasse. Chaque « + » ou
|
||||
« - » tombait sur « # », large de 3 et deja a sa butee — d'ou un
|
||||
avertissement en boucle et aucune colonne atteignable.
|
||||
|
||||
La cible se deplace avec « < » et « > », et part sur le nom de VM,
|
||||
la seule qu'on ait vraiment besoin d'elargir."""
|
||||
table = self.query_one("#vms", DataTable)
|
||||
keys = list(table.columns)
|
||||
if not keys:
|
||||
return None, None
|
||||
index = max(0, min(self._col_target, len(keys) - 1))
|
||||
return table, keys[index]
|
||||
|
||||
def _apply_column_widths(self, table) -> None:
|
||||
"""Fait PRENDRE les largeurs à l'écran.
|
||||
|
||||
« refresh(layout=True) » ne suffit pas, et c'est le piège :
|
||||
mesuré sur Textual 8.2.8, la largeur de la colonne passe bien de
|
||||
22 à 34, mais la taille virtuelle du tableau reste à 81 — donc
|
||||
rien ne bouge. « clear_cached_dimensions » et « refresh_column »
|
||||
n'y changent rien non plus ; seul le recalcul des dimensions la
|
||||
porte à 93.
|
||||
|
||||
C'est une API privée, d'où le repli : une version future de
|
||||
Textual dégradera l'ajustement au lieu de casser le suivi."""
|
||||
try:
|
||||
table._update_dimensions(list(table.rows))
|
||||
except Exception:
|
||||
table.refresh(layout=True)
|
||||
|
||||
def _resize_column(self, delta) -> None:
|
||||
table, key = self._cursor_column()
|
||||
if key is None:
|
||||
return
|
||||
col = table.columns[key]
|
||||
width = max(
|
||||
self.COL_MIN, min(self.COL_MAX, (col.width or 0) + delta)
|
||||
)
|
||||
if width == col.width:
|
||||
# Butée atteinte : le SEUL cas où il faut le dire, puisque
|
||||
# rien ne bougera à l'écran pour l'expliquer.
|
||||
self.notify(
|
||||
f"{col.label} : {width} (butee {self.COL_MIN}-{self.COL_MAX})",
|
||||
severity="warning",
|
||||
)
|
||||
return
|
||||
col.width = width
|
||||
# auto_width écraserait la largeur au prochain rendu : on la coupe,
|
||||
# sinon le réglage ne survit pas à la première mise à jour.
|
||||
col.auto_width = False
|
||||
# Pas de notification quand ça marche : le changement se VOIT, et
|
||||
# une bulle par frappe rendait l'ajustement pénible.
|
||||
self._apply_column_widths(table)
|
||||
|
||||
def _move_col_target(self, delta) -> None:
|
||||
"""Deplace la cible, en boucle sur les colonnes."""
|
||||
table = self.query_one("#vms", DataTable)
|
||||
keys = list(table.columns)
|
||||
if not keys:
|
||||
return
|
||||
self._col_target = (self._col_target + delta) % len(keys)
|
||||
self.notify(
|
||||
f"Colonne visee : {table.columns[keys[self._col_target]].label}"
|
||||
)
|
||||
|
||||
def action_col_prev(self) -> None:
|
||||
self._move_col_target(-1)
|
||||
|
||||
def action_col_next(self) -> None:
|
||||
self._move_col_target(1)
|
||||
|
||||
def action_col_grow(self) -> None:
|
||||
self._resize_column(self.COL_STEP)
|
||||
|
||||
def action_col_shrink(self) -> None:
|
||||
self._resize_column(-self.COL_STEP)
|
||||
|
||||
def action_col_reset(self) -> None:
|
||||
"""Rend aux colonnes les largeurs déclarées à la construction."""
|
||||
table = self.query_one("#vms", DataTable)
|
||||
for key, width in COL_DEFAULT_WIDTHS.items():
|
||||
if key in table.columns:
|
||||
table.columns[key].width = width
|
||||
table.columns[key].auto_width = False
|
||||
self._apply_column_widths(table)
|
||||
|
||||
def _total_elapsed(self, now, started):
|
||||
"""Durée globale. Elle se FIGE quand plus rien ne tourne : sinon
|
||||
le total continuait de courir sur un parc entièrement terminé,
|
||||
et repartait de plus belle à chaque réouverture."""
|
||||
if len(self._final) < len(vms) or not self._final:
|
||||
return now - started
|
||||
return max(e for _s, _c, e in self._final.values())
|
||||
|
||||
def action_vm_actions(self) -> None:
|
||||
"""Ouvre les actions de la VM sélectionnée."""
|
||||
vm = self._vm_by_name(self._selected)
|
||||
if not vm:
|
||||
return
|
||||
|
||||
def chosen(result):
|
||||
if not result:
|
||||
return
|
||||
kind, parts = result
|
||||
if kind == "update":
|
||||
self._run_update(vm, parts)
|
||||
elif kind == "restart":
|
||||
self._run_in_vm(
|
||||
vm, restart_odoo_cmd(), "Redemarrage d'Odoo"
|
||||
)
|
||||
elif kind == "delete":
|
||||
self._ask_delete(vm)
|
||||
|
||||
self.push_screen(VmActionsScreen(vm), chosen)
|
||||
|
||||
def _run_in_vm(self, vm, cmd, title) -> None:
|
||||
"""Exécute une commande DANS la VM, terminal rendu.
|
||||
|
||||
« suspend() » comme pour SSH et la console : sudo peut demander son
|
||||
mot de passe et la sortie est longue ; la garder derrière Textual
|
||||
la rendrait illisible. La pause finale évite que l'écran reparte
|
||||
avant qu'on ait lu le résultat."""
|
||||
if not vm.get("ip"):
|
||||
self.notify("Pas d'IP pour cette VM.", severity="error")
|
||||
return
|
||||
with self.suspend():
|
||||
print(f"\n=== {title} — {vm['name']} ===")
|
||||
os.system(
|
||||
f"ssh {SSH_OPTS} erplibre@{vm['ip']} "
|
||||
f"{shlex.quote(cmd)} || true"
|
||||
)
|
||||
input("\nEntrée pour revenir au suivi… ")
|
||||
|
||||
def _run_update(self, vm, parts) -> None:
|
||||
cmd = update_remote_cmd(parts)
|
||||
if not cmd:
|
||||
self.notify("Rien de coché.", severity="warning")
|
||||
return
|
||||
self._run_in_vm(vm, cmd, "Mise a jour")
|
||||
|
||||
def _ask_delete(self, vm) -> None:
|
||||
"""Suppression : jamais sans une seconde main."""
|
||||
|
||||
def confirmed(yes):
|
||||
if not yes:
|
||||
return
|
||||
with self.suspend():
|
||||
print(f"\n=== Suppression — {vm['name']} ===")
|
||||
os.system(delete_vm_cmd(vm["name"], True) + " || true")
|
||||
input("\nEntrée pour revenir au suivi… ")
|
||||
|
||||
self.push_screen(
|
||||
ConfirmScreen(
|
||||
f"Supprimer {vm['name']} ?",
|
||||
[
|
||||
"La VM est arrêtée, sa définition retirée,",
|
||||
"et son disque qcow2 EFFACÉ. Rien n'est récupérable.",
|
||||
"",
|
||||
f" /var/lib/libvirt/images/{vm['name']}.qcow2",
|
||||
],
|
||||
"Supprimer définitivement",
|
||||
),
|
||||
confirmed,
|
||||
)
|
||||
|
||||
def action_pause_all(self) -> None:
|
||||
"""Met en PAUSE (virsh suspend) toutes les VM en cours d'exécution.
|
||||
L'install reprend là où elle en était après « Reprendre »."""
|
||||
|
|
|
|||
|
|
@ -7,10 +7,23 @@ DIR_VENV_ERPLIBRE=".venv.erplibre"
|
|||
if [[ ! -d "$DIR_VENV_ERPLIBRE" ]]; then
|
||||
DIR_VENV_ERPLIBRE_EXIST=0
|
||||
echo "$DIR_VENV_ERPLIBRE does not exist."
|
||||
if [[ ! -d "~/.pyenv/versions/$(< ./conf/python-erplibre-version)/bin/python" ]]; then
|
||||
"~/.pyenv/versions/$(< ./conf/python-erplibre-version)/bin/python" -m venv $DIR_VENV_ERPLIBRE
|
||||
# Le test d'origine etait inoperant : le tilde entre guillemets n'est pas
|
||||
# etendu, et « -d » testait un FICHIER comme un repertoire. La condition
|
||||
# etait donc toujours vraie, la commande echouait en « No such file or
|
||||
# directory », et le repli « python -m venv » ne servait jamais.
|
||||
# shellcheck source=script/install/lib_python_provider.sh
|
||||
. ./script/install/lib_python_provider.sh
|
||||
EL_PY_WANT="$(< ./conf/python-erplibre-version)"
|
||||
# Ici on ne PROVISIONNE pas : ce script s'execute au demarrage de TODO, il
|
||||
# doit rester rapide et hors reseau. On prend ce qui est deja pose, sinon
|
||||
# le python du systeme.
|
||||
EL_PY_EXEC="$(el_pyenv_exec_path "${EL_PY_WANT}")"
|
||||
el_python_is_version "${EL_PY_EXEC}" "${EL_PY_WANT}" \
|
||||
|| EL_PY_EXEC="$(el_mise_exec_path "${EL_PY_WANT}" 2> /dev/null)"
|
||||
if [[ -n "${EL_PY_EXEC}" ]] && [[ -x "${EL_PY_EXEC}" ]]; then
|
||||
"${EL_PY_EXEC}" -m venv $DIR_VENV_ERPLIBRE
|
||||
else
|
||||
python -m venv $DIR_VENV_ERPLIBRE
|
||||
python3 -m venv $DIR_VENV_ERPLIBRE
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
|
|||
1273
script/todo/todo.py
1273
script/todo/todo.py
File diff suppressed because it is too large
Load diff
|
|
@ -1973,6 +1973,140 @@ TRANSLATIONS = {
|
|||
"fr": "RAM (Mo)",
|
||||
"en": "RAM (MB)",
|
||||
},
|
||||
"cloud-init still running after 20 min - install starts anyway"
|
||||
" (it waits for cloud-init first)": {
|
||||
"fr": "cloud-init tourne encore apres 20 min - l'installation demarre"
|
||||
" quand meme (elle attend d'abord cloud-init)",
|
||||
"en": "cloud-init still running after 20 min - install starts anyway"
|
||||
" (it waits for cloud-init first)",
|
||||
},
|
||||
"Waiting for cloud-init to finish (up to 15 min)": {
|
||||
"fr": "Attente de la fin de cloud-init (jusqu'a 15 min)",
|
||||
"en": "Waiting for cloud-init to finish (up to 15 min)",
|
||||
},
|
||||
"cloud-init:": {
|
||||
"fr": "cloud-init :",
|
||||
"en": "cloud-init:",
|
||||
},
|
||||
"WARNING libvirt unreachable: the IP will not be refreshed"
|
||||
" (libvirt group? re-login required)": {
|
||||
"fr": "ATTENTION libvirt injoignable : l'IP ne sera pas rafraichie"
|
||||
" (groupe libvirt ? reconnexion necessaire)",
|
||||
"en": "WARNING libvirt unreachable: the IP will not be refreshed"
|
||||
" (libvirt group? re-login required)",
|
||||
},
|
||||
"DHCP lease moved:": {
|
||||
"fr": "bail DHCP deplace :",
|
||||
"en": "DHCP lease moved:",
|
||||
},
|
||||
"An install is still running:": {
|
||||
"fr": "Une installation est encore en cours :",
|
||||
"en": "An install is still running:",
|
||||
},
|
||||
"VM(s) in progress": {
|
||||
"fr": "VM en cours",
|
||||
"en": "VM(s) in progress",
|
||||
},
|
||||
"Last activity:": {
|
||||
"fr": "Dernière activité il y a :",
|
||||
"en": "Last activity:",
|
||||
},
|
||||
"Reopen that monitoring": {
|
||||
"fr": "Rouvrir ce suivi",
|
||||
"en": "Reopen that monitoring",
|
||||
},
|
||||
"Deploy anyway (new run)": {
|
||||
"fr": "Déployer quand même (nouveau run)",
|
||||
"en": "Deploy anyway (new run)",
|
||||
},
|
||||
"Choice (number, blank = reopen): ": {
|
||||
"fr": "Choix (numéro, vide = rouvrir) : ",
|
||||
"en": "Choice (number, blank = reopen): ",
|
||||
},
|
||||
"Python interpreter:": {
|
||||
"fr": "Interpreteur Python :",
|
||||
"en": "Python interpreter:",
|
||||
},
|
||||
"mise (precompiled, faster)": {
|
||||
"fr": "mise (precompile, plus rapide)",
|
||||
"en": "mise (precompiled, faster)",
|
||||
},
|
||||
"pyenv (compiles from source)": {
|
||||
"fr": "pyenv (compile depuis les sources)",
|
||||
"en": "pyenv (compiles from source)",
|
||||
},
|
||||
"Choice (number, blank = mise): ": {
|
||||
"fr": "Choix (numero, vide = mise) : ",
|
||||
"en": "Choice (number, blank = mise): ",
|
||||
},
|
||||
"mise has no binary for:": {
|
||||
"fr": "mise ne publie pas de binaire pour :",
|
||||
"en": "mise has no binary for:",
|
||||
},
|
||||
"those VMs use pyenv": {
|
||||
"fr": "ces VM utiliseront pyenv",
|
||||
"en": "those VMs use pyenv",
|
||||
},
|
||||
"Installing mise (precompiled Python)": {
|
||||
"fr": "Installation de mise (Python precompile)",
|
||||
"en": "Installing mise (precompiled Python)",
|
||||
},
|
||||
"VM type:": {
|
||||
"fr": "Type de VM :",
|
||||
"en": "VM type:",
|
||||
},
|
||||
"Server (no graphical interface)": {
|
||||
"fr": "Serveur (sans interface graphique)",
|
||||
"en": "Server (no graphical interface)",
|
||||
},
|
||||
"Graphical (server + GNOME desktop)": {
|
||||
"fr": "Graphique (serveur + bureau GNOME)",
|
||||
"en": "Graphical (server + GNOME desktop)",
|
||||
},
|
||||
"server": {
|
||||
"fr": "serveur",
|
||||
"en": "server",
|
||||
},
|
||||
"Graphical (server + desktop):": {
|
||||
"fr": "Graphique (serveur + bureau) :",
|
||||
"en": "Graphical (server + desktop):",
|
||||
},
|
||||
"Installing the desktop (long):": {
|
||||
"fr": "Installation du bureau (long) :",
|
||||
"en": "Installing the desktop (long):",
|
||||
},
|
||||
"Choice (number, blank = server): ": {
|
||||
"fr": "Choix (numero, vide = serveur) : ",
|
||||
"en": "Choice (number, blank = server): ",
|
||||
},
|
||||
"Installing the GNOME desktop (long)": {
|
||||
"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:",
|
||||
},
|
||||
"One run per install": {
|
||||
"fr": "une execution par installation",
|
||||
"en": "One run per install",
|
||||
},
|
||||
"limit to host cores": {
|
||||
"fr": "limiter au nombre de coeurs de l'hote",
|
||||
"en": "limit to host cores",
|
||||
},
|
||||
"free value…": {
|
||||
"fr": "valeur libre…",
|
||||
"en": "free value…",
|
||||
},
|
||||
"Disk (e.g. 250G, 1.5T)": {
|
||||
"fr": "Disque (ex. 250G, 1,5T)",
|
||||
"en": "Disk (e.g. 250G, 1.5T)",
|
||||
},
|
||||
"all archs": {
|
||||
"fr": "toutes",
|
||||
"en": "all",
|
||||
|
|
@ -2005,6 +2139,148 @@ TRANSLATIONS = {
|
|||
"fr": "Éditer la VM",
|
||||
"en": "Edit VM",
|
||||
},
|
||||
"Server": {
|
||||
"fr": "Serveur",
|
||||
"en": "Server",
|
||||
},
|
||||
"VM type (default):": {
|
||||
"fr": "Type de VM (défaut) :",
|
||||
"en": "VM type (default):",
|
||||
},
|
||||
"These values are the default for every VM;": {
|
||||
"fr": "Ces valeurs sont le défaut de chaque VM ;",
|
||||
"en": "These values are the default for every VM;",
|
||||
},
|
||||
"adjust any of them per VM on the right.": {
|
||||
"fr": "ajustez-les VM par VM dans la vue de droite.",
|
||||
"en": "adjust any of them per VM on the right.",
|
||||
},
|
||||
"Application store:": {
|
||||
"fr": "Magasin d'applications :",
|
||||
"en": "Application store:",
|
||||
},
|
||||
"Application store (graphical Ubuntu VMs):": {
|
||||
"fr": "Magasin d'applications (VM Ubuntu graphiques) :",
|
||||
"en": "Application store (graphical Ubuntu VMs):",
|
||||
},
|
||||
"deb only (epiphany-browser)": {
|
||||
"fr": "deb uniquement (epiphany-browser)",
|
||||
"en": "deb only (epiphany-browser)",
|
||||
},
|
||||
"Flatpak tooling, no Flathub": {
|
||||
"fr": "outillage Flatpak, sans Flathub",
|
||||
"en": "Flatpak tooling, no Flathub",
|
||||
},
|
||||
"snap (Ubuntu default, Firefox)": {
|
||||
"fr": "snap (défaut Ubuntu, Firefox)",
|
||||
"en": "snap (Ubuntu default, Firefox)",
|
||||
},
|
||||
"snap needs the store; slow under emulation.": {
|
||||
"fr": "snap exige le store ; lent sous émulation.",
|
||||
"en": "snap needs the store; slow under emulation.",
|
||||
},
|
||||
"No graphical VM on a snap-based distro.": {
|
||||
"fr": "Aucune VM graphique sur une distribution à snap.",
|
||||
"en": "No graphical VM on a snap-based distro.",
|
||||
},
|
||||
"Resources — applied to ALL VMs": {
|
||||
"fr": "Ressources — appliquées à TOUTES les VM",
|
||||
"en": "Resources — applied to ALL VMs",
|
||||
},
|
||||
"The profile and these fields change EVERY VM.": {
|
||||
"fr": "Le profil et ces champs changent TOUTES les VM.",
|
||||
"en": "The profile and these fields change EVERY VM.",
|
||||
},
|
||||
"A VM edited on the right (marked) keeps its own.": {
|
||||
"fr": "Une VM modifiée à droite (marquée ✎) garde les siennes.",
|
||||
"en": "A VM edited on the right (marked) keeps its own.",
|
||||
},
|
||||
"Rename the VM": {
|
||||
"fr": "Renommer la VM",
|
||||
"en": "Rename the VM",
|
||||
},
|
||||
"Rename": {
|
||||
"fr": "Renommer",
|
||||
"en": "Rename",
|
||||
},
|
||||
"Empty = back to the automatic name:": {
|
||||
"fr": "Vide = revenir au nom automatique :",
|
||||
"en": "Empty = back to the automatic name:",
|
||||
},
|
||||
"Invalid name: letters, digits, hyphens.": {
|
||||
"fr": "Nom invalide : lettres, chiffres et traits d'union.",
|
||||
"en": "Invalid name: letters, digits, hyphens.",
|
||||
},
|
||||
"default; a VM may differ, see its line": {
|
||||
"fr": "défaut ; une VM peut s'en écarter, voir sa ligne",
|
||||
"en": "default; a VM may differ, see its line",
|
||||
},
|
||||
"varies, see each line": {
|
||||
"fr": "varie, voir chaque ligne",
|
||||
"en": "varies, see each line",
|
||||
},
|
||||
"From your workstation:": {
|
||||
"fr": "Depuis votre poste :",
|
||||
"en": "From your workstation:",
|
||||
},
|
||||
"then point your client at": {
|
||||
"fr": "puis pointez votre client sur",
|
||||
"en": "then point your client at",
|
||||
},
|
||||
"Remote desktop tunnel (VNC/RDP through SSH)": {
|
||||
"fr": "🖥 Tunnel bureau distant (VNC/RDP par SSH)",
|
||||
"en": "🖥 Remote desktop tunnel (VNC/RDP through SSH)",
|
||||
},
|
||||
"Remote desktop tunnel": {
|
||||
"fr": "Tunnel vers le bureau distant",
|
||||
"en": "Remote desktop tunnel",
|
||||
},
|
||||
"Which VM?": {"fr": "Quelle VM ?", "en": "Which VM?"},
|
||||
"No IP for this VM; is it running?": {
|
||||
"fr": "Pas d'IP pour cette VM ; tourne-t-elle ?",
|
||||
"en": "No IP for this VM; is it running?",
|
||||
},
|
||||
"Run this on YOUR workstation:": {
|
||||
"fr": "À lancer sur VOTRE poste :",
|
||||
"en": "Run this on YOUR workstation:",
|
||||
},
|
||||
"(through the ProxyJump already in ~/.ssh/config)": {
|
||||
"fr": "(par le ProxyJump déjà dans ~/.ssh/config)",
|
||||
"en": "(through the ProxyJump already in ~/.ssh/config)",
|
||||
},
|
||||
"No ~/.ssh/config entry; see SSH configuration.": {
|
||||
"fr": "Aucune entrée ~/.ssh/config ; voir Configuration SSH.",
|
||||
"en": "No ~/.ssh/config entry; see SSH configuration.",
|
||||
},
|
||||
"Not in an SSH session: check the host address.": {
|
||||
"fr": "Hors session SSH : vérifiez l'adresse de l'hôte.",
|
||||
"en": "Not in an SSH session: check the host address.",
|
||||
},
|
||||
"The tunnel stays open as long as that ssh runs.": {
|
||||
"fr": "Le tunnel reste ouvert tant que ce ssh tourne.",
|
||||
"en": "The tunnel stays open as long as that ssh runs.",
|
||||
},
|
||||
"No VM defined.": {"fr": "Aucune VM définie.", "en": "No VM defined."},
|
||||
"No host in ~/.ssh/config and no local VM.": {
|
||||
"fr": "Aucun hôte dans ~/.ssh/config et aucune VM locale.",
|
||||
"en": "No host in ~/.ssh/config and no local VM.",
|
||||
},
|
||||
"local VM": {"fr": "VM locale", "en": "local VM"},
|
||||
"Remote desktop kind:": {
|
||||
"fr": "Type de bureau distant :",
|
||||
"en": "Remote desktop kind:",
|
||||
},
|
||||
"already defined, not counted": {
|
||||
"fr": "déjà définie(s), non comptée(s)",
|
||||
"en": "already defined, not counted",
|
||||
},
|
||||
"RAM: 2048 or 8G": {"fr": "RAM : 2048 ou 8G", "en": "RAM: 2048 or 8G"},
|
||||
"Diagnostic dump": {"fr": "Diagnostic", "en": "Diagnostic dump"},
|
||||
"State written to": {"fr": "État écrit dans", "en": "State written to"},
|
||||
"Reset VM": {
|
||||
"fr": "Réinit. VM",
|
||||
"en": "Reset VM",
|
||||
},
|
||||
"Preview": {
|
||||
"fr": "Aperçu",
|
||||
"en": "Preview",
|
||||
|
|
@ -2786,8 +3062,8 @@ TRANSLATIONS = {
|
|||
"en": "select all",
|
||||
},
|
||||
"Delete VM(s)": {
|
||||
"fr": "🗑 Effacer une ou plusieurs VM",
|
||||
"en": "🗑 Delete VM(s)",
|
||||
"fr": "🗑 Effacer une ou plusieurs VM",
|
||||
"en": "🗑 Delete VM(s)",
|
||||
},
|
||||
"No VM found.": {
|
||||
"fr": "Aucune VM trouvée.",
|
||||
|
|
@ -3414,8 +3690,8 @@ TRANSLATIONS = {
|
|||
"en": "Verify SHA256 after download? (y/N, default: no): ",
|
||||
},
|
||||
"QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)": {
|
||||
"fr": "QEMU - Exemple dry-run (demo-vm, Ubuntu 24.04)",
|
||||
"en": "QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)",
|
||||
"fr": "🧰 QEMU - Exemple dry-run (demo-vm, Ubuntu 24.04)",
|
||||
"en": "🧰 QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)",
|
||||
},
|
||||
# QEMU - statistics screen
|
||||
"Statistics (installs, durations, VMs)": {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,16 @@ This file tracks what is installed in the current ERPLibre workspace:
|
|||
- Which Odoo versions are installed, and with which options (extra, etc.)
|
||||
- Whether the mobile project is active
|
||||
- The currently active Odoo version
|
||||
|
||||
`update_env_version.py` imports this module as the FIRST thing `make
|
||||
install_os` runs, donc avant pyenv : il s'exécute sur le Python DU SYSTÈME,
|
||||
3.8 sur Ubuntu 20.04. Les annotations doivent rester différées — sans quoi
|
||||
« str | None » (PEP 604) ou « list[str] » (PEP 585) sont évaluées à
|
||||
l'import et lèvent un TypeError avant même que l'installation commence.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
|
|
|||
155
test/test_tooling_requirements.py
Executable file
155
test/test_tooling_requirements.py
Executable file
|
|
@ -0,0 +1,155 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
"""Ce que l'outillage importe doit être déclaré, pas seulement installé.
|
||||
|
||||
Un paquet peut vivre dans `.venv.erplibre` sans figurer nulle part : arrivé
|
||||
par la dépendance d'un autre, ou posé à la main un jour. Rien ne le réclame,
|
||||
donc rien ne le réinstalle — il disparaît au prochain venv neuf, et l'outil
|
||||
qui s'en servait tombe chez le suivant, pas chez celui qui l'a écrit.
|
||||
|
||||
Le fichier porte déjà ce raisonnement en commentaire pour lxml. Ce test le
|
||||
rend vérifiable au lieu de le laisser à la vigilance.
|
||||
|
||||
Portée : le venv d'OUTILS. Les venvs Odoo ont leurs propres requirements, et
|
||||
psycopg2 y est déclaré parce qu'Odoo en a besoin — l'outillage, lui, parle à
|
||||
PostgreSQL par psql en sous-processus.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
REQUIREMENTS = os.path.join(REPO, "requirement", "erplibre_require-ments.txt")
|
||||
|
||||
PATTERNS = (
|
||||
"script/analyse/*.py",
|
||||
"script/analyse/*/*.py",
|
||||
"script/odoo/migration/*.py",
|
||||
"script/todo/*.py",
|
||||
)
|
||||
|
||||
# Ce que l'on écrit pour importer n'est pas ce que l'on écrit pour installer.
|
||||
# La table est explicite : un nouveau décalage doit être ajouté ici en
|
||||
# connaissance de cause, plutôt que d'être toléré par une correspondance
|
||||
# approximative qui laisserait passer une vraie absence.
|
||||
PACKAGE_OF_MODULE = {
|
||||
"dotenv": "python-dotenv",
|
||||
}
|
||||
|
||||
# Modules fournis par l'environnement, jamais par ce fichier.
|
||||
PROVIDED_ELSEWHERE = {
|
||||
# Le venv Odoo, pas celui des outils : ces scripts tournent sous
|
||||
# `odoo-bin shell`, qui apporte son propre interpréteur.
|
||||
"odoo",
|
||||
}
|
||||
|
||||
|
||||
def tooling_files():
|
||||
return sorted(
|
||||
{p for m in PATTERNS for p in glob.glob(os.path.join(REPO, m))}
|
||||
)
|
||||
|
||||
|
||||
def third_party_imports():
|
||||
"""{module: {fichiers qui l'importent}}, hors stdlib et hors modules locaux."""
|
||||
files = tooling_files()
|
||||
local = {os.path.basename(p)[:-3] for p in files}
|
||||
found = {}
|
||||
for path in files:
|
||||
with open(path) as handle:
|
||||
tree = ast.parse(handle.read())
|
||||
for node in ast.walk(tree):
|
||||
names = []
|
||||
if isinstance(node, ast.Import):
|
||||
names = [alias.name.split(".")[0] for alias in node.names]
|
||||
elif (
|
||||
isinstance(node, ast.ImportFrom)
|
||||
and node.module
|
||||
and not node.level
|
||||
):
|
||||
names = [node.module.split(".")[0]]
|
||||
for name in names:
|
||||
if (
|
||||
name in sys.stdlib_module_names
|
||||
or name in local
|
||||
or name in PROVIDED_ELSEWHERE
|
||||
or name == "script"
|
||||
):
|
||||
continue
|
||||
found.setdefault(name, set()).add(os.path.relpath(path, REPO))
|
||||
return found
|
||||
|
||||
|
||||
def declared():
|
||||
with open(REQUIREMENTS) as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
class TestEveryImportIsDeclared(unittest.TestCase):
|
||||
def test_the_scan_finds_something(self):
|
||||
# Un test qui n'inspecte rien passe toujours.
|
||||
self.assertGreater(len(tooling_files()), 20)
|
||||
self.assertGreater(len(third_party_imports()), 5)
|
||||
|
||||
def test_no_third_party_import_is_undeclared(self):
|
||||
text = declared()
|
||||
missing = []
|
||||
for module, users in sorted(third_party_imports().items()):
|
||||
package = PACKAGE_OF_MODULE.get(module, module)
|
||||
# En début de ligne : « lxml » ne doit pas être satisfait par
|
||||
# une mention en commentaire ni par « python-lxml-stubs ».
|
||||
if not re.search(
|
||||
rf"^\s*{re.escape(package)}\b", text, re.M | re.I
|
||||
):
|
||||
missing.append(f"{module} ({package}) <- {sorted(users)[0]}")
|
||||
self.assertEqual(missing, [])
|
||||
|
||||
def test_the_alias_table_has_no_dead_entry(self):
|
||||
# Une correspondance qui ne sert plus masque le jour où le module
|
||||
# revient sous un autre nom.
|
||||
imported = set(third_party_imports())
|
||||
for module in PACKAGE_OF_MODULE:
|
||||
with self.subTest(module=module):
|
||||
self.assertIn(module, imported)
|
||||
|
||||
def test_the_check_would_notice_a_removal(self):
|
||||
# La preuve que la recherche mord : un paquet réellement déclaré,
|
||||
# retiré du texte, doit être vu comme manquant.
|
||||
# `assertRegex` cherche SANS re.M : « ^ » n'y vaut qu'au tout début
|
||||
# de la chaîne, et la vérification ci-dessus, elle, lit ligne à ligne.
|
||||
# Un motif compilé fait dire au test la même chose qu'au code.
|
||||
motif = re.compile(r"^\s*lxml\b", re.M)
|
||||
text = declared()
|
||||
self.assertRegex(text, motif)
|
||||
without = motif.sub("", text)
|
||||
self.assertNotRegex(without, motif)
|
||||
|
||||
|
||||
class TestPsycopg2StaysOutOfTheToolingVenv(unittest.TestCase):
|
||||
"""Décision prise, et la raison avec — sinon elle se reprend à l'aveugle.
|
||||
|
||||
Les outils d'analyse interrogent PostgreSQL par `psql` en sous-processus :
|
||||
un choix délibéré, qui porte la garantie de lecture seule côté serveur
|
||||
(PGOPTIONS). Déclarer psycopg2 ici ferait compiler une extension C à
|
||||
chaque installation neuve, sans que rien ne l'importe.
|
||||
"""
|
||||
|
||||
def test_nothing_in_the_tooling_imports_psycopg2(self):
|
||||
self.assertNotIn("psycopg2", third_party_imports())
|
||||
|
||||
def test_the_analysis_tools_query_through_psql(self):
|
||||
path = os.path.join(REPO, "script", "analyse", "lib_analyse.py")
|
||||
with open(path) as handle:
|
||||
source = handle.read()
|
||||
self.assertIn('"psql"', source)
|
||||
self.assertIn("default_transaction_read_only=on", source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue