From 42e10bcced0bc70f34c855846e4d0321f0202516 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 24 Sep 2026 13:31:32 -0400 Subject: [PATCH] [UPD] install : le venv d'outillage passe en Python 3.14.7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install_erplibre.sh bâtissait .venv.erplibre avec le python3 du système, quelle que soit sa version : un 3.10 y donnait un venv que l'outillage ne sait pas charger. Il délègue à install_venv.sh, qui obtient la version par mise, pyenv ou la distribution, et rebâtit un venv hors service. Le PATCH ne borne que le venv d'Odoo, dont le pyproject exige « >=3.12.10,<3.13 » : l'exiger ailleurs écartait un Python de distribution d'un cran en retard — NixOS 25.11 livre 3.14.2 — et faisait compiler CPython pour rien. Le module NixOS déclare désormais les DEUX Python, substitués depuis les fichiers de version. Vérifié : 15 tests sur des venvs factices, conservés, rebâtis ou refusés selon leur état, et un chemin sans pyvenv.cfg garde son contenu. --- EN --- install_erplibre.sh built .venv.erplibre with the system python3, whatever its version: a 3.10 gave a venv the tooling cannot load. It delegates to install_venv.sh, which obtains the version through mise, pyenv or the distribution, and rebuilds a venv out of service. The PATCH bounds only Odoo's venv, whose pyproject requires ">=3.12.10,<3.13": requiring it elsewhere turned away a distribution Python one step behind — NixOS 25.11 ships 3.14.2 — and compiled CPython for nothing. The NixOS module now declares BOTH Pythons, substituted from the version files. Checked: 15 tests on stub venvs, kept, rebuilt or refused by their state, and a path without pyvenv.cfg keeps its content. Assisted-by: Claude Opus 5 --- .claude/skills/erplibre-deployment/SKILL.md | 22 ++ CHANGELOG.base.md | 8 + CHANGELOG.fr.md | 4 + CHANGELOG.md | 4 + Makefile | 16 +- README.base.md | 11 + README.fr.md | 6 + README.md | 5 + conf/nixos/erplibre.nix | 23 +- conf/python-erplibre-version | 2 +- doc/WINDOWS_INSTALLATION.base.md | 8 + doc/WINDOWS_INSTALLATION.fr.md | 4 + doc/WINDOWS_INSTALLATION.md | 4 + docker/Dockerfile.prod.pkg | 4 + docker/README.base.md | 10 + docker/README.fr.md | 5 + docker/README.md | 5 + script/install/install_erplibre.sh | 62 ++++- script/install/install_git_repo.sh | 42 ++- script/install/install_nixos_dependency.sh | 18 ++ script/install/install_venv.sh | 145 +++++++++- script/install/lib_python_provider.sh | 55 +++- script/todo/source_todo.sh | 45 +--- test/test_install_nixos.py | 31 ++- test/test_install_venv_rebuild.py | 278 ++++++++++++++++++++ 25 files changed, 739 insertions(+), 78 deletions(-) create mode 100644 test/test_install_venv_rebuild.py diff --git a/.claude/skills/erplibre-deployment/SKILL.md b/.claude/skills/erplibre-deployment/SKILL.md index b2f6688..c72f257 100644 --- a/.claude/skills/erplibre-deployment/SKILL.md +++ b/.claude/skills/erplibre-deployment/SKILL.md @@ -36,9 +36,31 @@ 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. +## Le Python de l'outillage + +`conf/python-erplibre-version` (3.14.7) donne l'interpréteur de +`.venv.erplibre`, le venv d'outillage, distinct du venv Odoo (3.12.10 pour +Odoo 18.0). C'est la seule version que `script/` vise : là où une distribution +ne la porte pas, pyenv la compile. + +Le PATCH ne borne que le venv d'Odoo, dont le pyproject exige +`>=3.12.10,<3.13`. Pour l'outillage, la majeure.mineure suffit : exiger le +patch écarterait le Python d'une distribution d'un cran en retard — NixOS 25.11 +livre 3.14.2 — et ferait compiler CPython pour rien. + +`install_erplibre.sh` passe par `install_venv.sh`, donc par +`EL_PYTHON_PROVIDER`. Un venv dont `bin/python` n'est pas compatible (même +majeure.mineure, patch au moins égal) est DÉTRUIT puis rebâti : ce qui y avait +été posé à la main part avec lui. Le venv d'Odoo, lui, n'est rebâti que s'il +est HORS SERVICE : une simple différence de version le laisse en place, parce +que le rebâtir refait une installation Poetry entière. Un répertoire sans +`pyvenv.cfg` n'est jamais effacé. + Le hook `pre-commit` relaie `script/analyse/check_python_version.py` : il signale le source qui ne parse pas sous cette version, sans bloquer, et dit quand aucun interpréteur de cette version n'était là pour vérifier. +L'image Docker de production bâtit `.venv.erplibre` sur le Python d'Odoo de son +image de base, et s'arrête si ce Python ne sait pas lire `script/`. ## Paquets Python diff --git a/CHANGELOG.base.md b/CHANGELOG.base.md index 371aedb..c214a8b 100644 --- a/CHANGELOG.base.md +++ b/CHANGELOG.base.md @@ -156,6 +156,10 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - A VM deployed with the cache upstream cut — QEMU form, Proxmox VE, or `deploy_qemu.py --offline` — has npm's security audit turned off (`NPM_CONFIG_AUDIT=false`): it queries a remote service no cache can replay, and failed on every offline install. An online VM keeps its audit - Verifying a downloaded image no longer needs `--verify`: it runs by default for every distribution that publishes a sum, and `--no-verify` is what skips it — to be kept for offline runs, where a substituted image would otherwise pass unremarked - `--bios` is refused on an image with no BIOS boot sector, and says why. Forced there, it gave a VM reported « running » with a silent console — the very failure that flag exists to avoid elsewhere +- The tooling virtual environment `.venv.erplibre` runs Python 3.14.7, independently of the Odoo one (3.12.10 for Odoo 18.0). `install_erplibre.sh` builds it through `install_venv.sh` and `EL_PYTHON_PROVIDER` instead of the system `python3` +- `.venv.erplibre` on an incompatible Python is DELETED and rebuilt; whatever was installed in it by hand goes with it. Odoo's venv is kept when it merely differs in version, and rebuilt only when it is unusable: rebuilding it redoes a whole Poetry install. A directory without `pyvenv.cfg` is never deleted +- The production Docker image builds `.venv.erplibre` on Odoo's Python and stops when that Python cannot parse `script/` +- The PATCH bounds only Odoo's venv, whose pyproject requires `>=3.12.10,<3.13`. For the tooling one, the major.minor is enough: requiring the patch turned away a distribution's Python one step behind — NixOS 25.11 ships 3.14.2 where conf asks 3.14.7 — and made pyenv COMPILE CPython for a difference nothing needs - `make format` picks the formatter from each file's context: an Odoo module keeps isort and black on `py37`, the series still supported going down that far, while this repository's own tooling goes through ruff, configured once in `.ruff.toml`. ruff follows CPython's versions, where black 24.8.0 stops at `py313`, and its import sorting replaces isort; it is also what the OCA standard uses since it left black - The repositories that Google Repo checks out under `script/` are excluded from that formatting, a named path included: reformatting them would write in someone else's history. `target-version` stays at `py310` there, because the git hooks carry `#!/usr/bin/env python3` and a distribution still ships 3.10 — from 3.14 on, ruff would write `except A, B:` without parentheses @@ -174,6 +178,10 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Une VM déployée l'amont du cache coupé — formulaire QEMU, Proxmox VE, ou `deploy_qemu.py --offline` — a l'audit de sécurité de npm désactivé (`NPM_CONFIG_AUDIT=false`) : il interroge un service qu'aucun cache ne rejoue, et échouait à chaque installation hors ligne. Une VM en ligne garde son audit - Vérifier une image téléchargée ne demande plus `--verify` : c'est le défaut pour toute distribution qui publie une somme, et `--no-verify` est ce qui la saute — à réserver aux essais hors ligne, où une image substituée passerait autrement sans un mot - `--bios` est refusé sur une image sans secteur d'amorçage BIOS, et dit pourquoi. Forcé là, il donnait une VM « running » à console muette — la panne même que ce drapeau évite ailleurs +- L'environnement virtuel d'outillage `.venv.erplibre` tourne en Python 3.14.7, indépendamment de celui d'Odoo (3.12.10 pour Odoo 18.0). `install_erplibre.sh` le bâtit par `install_venv.sh` et `EL_PYTHON_PROVIDER` plutôt qu'avec le `python3` du système +- `.venv.erplibre` sur un Python incompatible est SUPPRIMÉ puis rebâti ; ce qu'on y avait posé à la main part avec lui. Le venv d'Odoo est conservé quand seule sa version diffère, et rebâti seulement s'il est hors service : le rebâtir refait une installation Poetry entière. Un répertoire sans `pyvenv.cfg` n'est jamais effacé +- L'image Docker de production bâtit `.venv.erplibre` sur le Python d'Odoo et s'arrête quand ce Python ne sait pas lire `script/` +- Le PATCH ne borne que le venv d'Odoo, dont le pyproject exige `>=3.12.10,<3.13`. Pour celui de l'outillage, la majeure.mineure suffit : exiger le patch écartait le Python d'une distribution d'un cran en retard — NixOS 25.11 livre 3.14.2 quand conf demande 3.14.7 — et faisait COMPILER CPython à pyenv pour une différence que rien ne réclame - `make format` choisit le formateur d'après le contexte de chaque fichier : un module Odoo garde isort et black en `py37`, la série la plus ancienne encore supportée, quand l'outillage de ce dépôt passe par ruff, réglé une fois dans `.ruff.toml`. ruff suit les versions de CPython, là où black 24.8.0 s'arrête à `py313`, et son tri d'imports remplace isort ; c'est aussi ce qu'emploie la norme OCA depuis qu'elle a quitté black - Les dépôts que Google Repo rapatrie sous `script/` sont écartés de ce formatage, chemin nommé compris : les reformater écrirait dans l'historique d'autrui. `target-version` y reste à `py310`, parce que les hooks git portent `#!/usr/bin/env python3` et qu'une distribution livre encore 3.10 — à partir de 3.14, ruff écrirait `except A, B:` sans parenthèses diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 6bc6481..d1a9e56 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -78,6 +78,10 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Une VM déployée l'amont du cache coupé — formulaire QEMU, Proxmox VE, ou `deploy_qemu.py --offline` — a l'audit de sécurité de npm désactivé (`NPM_CONFIG_AUDIT=false`) : il interroge un service qu'aucun cache ne rejoue, et échouait à chaque installation hors ligne. Une VM en ligne garde son audit - Vérifier une image téléchargée ne demande plus `--verify` : c'est le défaut pour toute distribution qui publie une somme, et `--no-verify` est ce qui la saute — à réserver aux essais hors ligne, où une image substituée passerait autrement sans un mot - `--bios` est refusé sur une image sans secteur d'amorçage BIOS, et dit pourquoi. Forcé là, il donnait une VM « running » à console muette — la panne même que ce drapeau évite ailleurs +- L'environnement virtuel d'outillage `.venv.erplibre` tourne en Python 3.14.7, indépendamment de celui d'Odoo (3.12.10 pour Odoo 18.0). `install_erplibre.sh` le bâtit par `install_venv.sh` et `EL_PYTHON_PROVIDER` plutôt qu'avec le `python3` du système +- `.venv.erplibre` sur un Python incompatible est SUPPRIMÉ puis rebâti ; ce qu'on y avait posé à la main part avec lui. Le venv d'Odoo est conservé quand seule sa version diffère, et rebâti seulement s'il est hors service : le rebâtir refait une installation Poetry entière. Un répertoire sans `pyvenv.cfg` n'est jamais effacé +- L'image Docker de production bâtit `.venv.erplibre` sur le Python d'Odoo et s'arrête quand ce Python ne sait pas lire `script/` +- Le PATCH ne borne que le venv d'Odoo, dont le pyproject exige `>=3.12.10,<3.13`. Pour celui de l'outillage, la majeure.mineure suffit : exiger le patch écartait le Python d'une distribution d'un cran en retard — NixOS 25.11 livre 3.14.2 quand conf demande 3.14.7 — et faisait COMPILER CPython à pyenv pour une différence que rien ne réclame - `make format` choisit le formateur d'après le contexte de chaque fichier : un module Odoo garde isort et black en `py37`, la série la plus ancienne encore supportée, quand l'outillage de ce dépôt passe par ruff, réglé une fois dans `.ruff.toml`. ruff suit les versions de CPython, là où black 24.8.0 s'arrête à `py313`, et son tri d'imports remplace isort ; c'est aussi ce qu'emploie la norme OCA depuis qu'elle a quitté black - Les dépôts que Google Repo rapatrie sous `script/` sont écartés de ce formatage, chemin nommé compris : les reformater écrirait dans l'historique d'autrui. `target-version` y reste à `py310`, parce que les hooks git portent `#!/usr/bin/env python3` et qu'une distribution livre encore 3.10 — à partir de 3.14, ruff écrirait `except A, B:` sans parenthèses diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fd79b3..eb715ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - A VM deployed with the cache upstream cut — QEMU form, Proxmox VE, or `deploy_qemu.py --offline` — has npm's security audit turned off (`NPM_CONFIG_AUDIT=false`): it queries a remote service no cache can replay, and failed on every offline install. An online VM keeps its audit - Verifying a downloaded image no longer needs `--verify`: it runs by default for every distribution that publishes a sum, and `--no-verify` is what skips it — to be kept for offline runs, where a substituted image would otherwise pass unremarked - `--bios` is refused on an image with no BIOS boot sector, and says why. Forced there, it gave a VM reported « running » with a silent console — the very failure that flag exists to avoid elsewhere +- The tooling virtual environment `.venv.erplibre` runs Python 3.14.7, independently of the Odoo one (3.12.10 for Odoo 18.0). `install_erplibre.sh` builds it through `install_venv.sh` and `EL_PYTHON_PROVIDER` instead of the system `python3` +- `.venv.erplibre` on an incompatible Python is DELETED and rebuilt; whatever was installed in it by hand goes with it. Odoo's venv is kept when it merely differs in version, and rebuilt only when it is unusable: rebuilding it redoes a whole Poetry install. A directory without `pyvenv.cfg` is never deleted +- The production Docker image builds `.venv.erplibre` on Odoo's Python and stops when that Python cannot parse `script/` +- The PATCH bounds only Odoo's venv, whose pyproject requires `>=3.12.10,<3.13`. For the tooling one, the major.minor is enough: requiring the patch turned away a distribution's Python one step behind — NixOS 25.11 ships 3.14.2 where conf asks 3.14.7 — and made pyenv COMPILE CPython for a difference nothing needs - `make format` picks the formatter from each file's context: an Odoo module keeps isort and black on `py37`, the series still supported going down that far, while this repository's own tooling goes through ruff, configured once in `.ruff.toml`. ruff follows CPython's versions, where black 24.8.0 stops at `py313`, and its import sorting replaces isort; it is also what the OCA standard uses since it left black - The repositories that Google Repo checks out under `script/` are excluded from that formatting, a named path included: reformatting them would write in someone else's history. `target-version` stays at `py310` there, because the git hooks carry `#!/usr/bin/env python3` and a distribution still ships 3.10 — from 3.14 on, ruff would write `except A, B:` without parentheses diff --git a/Makefile b/Makefile index a5c0aa6..7a4992e 100644 --- a/Makefile +++ b/Makefile @@ -129,6 +129,8 @@ install_uv: echo "Ajoutez ce repertoire au PATH, ou installez uv globalement."; \ fi +# Telecharge puis execute : dans « curl | sh », le statut est celui de sh, +# qui rend 0 sur une entree vide et masque un telechargement rate. .PHONY: install_mise install_mise: @if command -v mise >/dev/null 2>&1; then \ @@ -139,7 +141,19 @@ install_mise: 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; \ + installateur="$$(mktemp)" || exit 1; \ + if ! curl -fsSL -o "$$installateur" https://mise.run; then \ + rm -f "$$installateur"; \ + echo "Telechargement de l installateur mise impossible" \ + "(reseau ou cache) : mise n est pas pose." >&2; \ + exit 1; \ + fi; \ + if ! sh "$$installateur"; then \ + rm -f "$$installateur"; \ + echo "L installateur de mise a echoue (voir ci-dessus)." >&2; \ + exit 1; \ + fi; \ + rm -f "$$installateur"; \ echo "Ajoutez ~/.local/bin a votre PATH, puis relancez l installation."; \ fi diff --git a/README.base.md b/README.base.md index fad2699..70a9ad6 100644 --- a/README.base.md +++ b/README.base.md @@ -86,10 +86,21 @@ Suivez-nous sur Mastodon : https://fosstodon.org/@erplibre Switch between versions with `make switch_odoo_18`, `make switch_odoo_16`, etc. +The Python in that table is the one of the Odoo virtual environment. The +tooling virtual environment `.venv.erplibre` (TODO, `repo`, formatters) runs +its own interpreter, **3.14.7**, set by `conf/python-erplibre-version`. Where a +distribution does not carry it, pyenv compiles it. + Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc. +Le Python de ce tableau est celui de l'environnement virtuel Odoo. +L'environnement virtuel d'outillage `.venv.erplibre` (TODO, `repo`, formateurs) +tourne sur son propre interpréteur, **3.14.7**, fixé par +`conf/python-erplibre-version`. Là où une distribution ne le porte pas, pyenv +le compile. + # Supported platforms diff --git a/README.fr.md b/README.fr.md index 0b0e333..e942d48 100644 --- a/README.fr.md +++ b/README.fr.md @@ -44,6 +44,12 @@ Suivez-nous sur Mastodon : https://fosstodon.org/@erplibre Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc. +Le Python de ce tableau est celui de l'environnement virtuel Odoo. +L'environnement virtuel d'outillage `.venv.erplibre` (TODO, `repo`, formateurs) +tourne sur son propre interpréteur, **3.14.7**, fixé par +`conf/python-erplibre-version`. Là où une distribution ne le porte pas, pyenv +le compile. + # Plateformes supportées diff --git a/README.md b/README.md index 037695a..2a7232d 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,11 @@ Follow us on Mastodon : https://fosstodon.org/@erplibre Switch between versions with `make switch_odoo_18`, `make switch_odoo_16`, etc. +The Python in that table is the one of the Odoo virtual environment. The +tooling virtual environment `.venv.erplibre` (TODO, `repo`, formatters) runs +its own interpreter, **3.14.7**, set by `conf/python-erplibre-version`. Where a +distribution does not carry it, pyenv compiles it. + # Supported platforms diff --git a/conf/nixos/erplibre.nix b/conf/nixos/erplibre.nix index f0aac4a..345ed75 100644 --- a/conf/nixos/erplibre.nix +++ b/conf/nixos/erplibre.nix @@ -83,17 +83,26 @@ }; # ── Les outils ─────────────────────────────────────────────────────────── - # python3.12 : la version qu'attend ERPLibre (.python-odoo-version). envfs - # la rend visible en /usr/bin/python3.12, où lib_python_provider.sh la - # cherche — ni mise ni pyenv n'ont alors à télécharger quoi que ce soit. + # Les DEUX Python du dépôt, substitués depuis ses fichiers de version : + # celui d'Odoo (.python-odoo-version) et celui de l'outillage + # (conf/python-erplibre-version), qui ne sont plus la même version. envfs + # les rend visibles en /usr/bin/python3.X, où lib_python_provider.sh les + # cherche — ni mise ni pyenv n'ont alors rien à compiler. Déclarer le seul + # Python d'Odoo laissait pyenv bâtir l'autre, et la compilation de CPython + # s'arrête ici sur « Modules/_cursesmodule.o ». + # + # Le second marqueur est VIDE quand les deux versions coïncident : nommer + # deux fois le même paquet ferait entrer en collision deux chemins + # identiques dans le profil. # # Les sorties « .dev » portent les en-têtes : sans elles, une roue absente # du dépôt amont devrait se compiler et ne trouverait ni libpq-fe.h ni # openssl/ssl.h. Elles ne servent qu'à ce cas, et ne coûtent que du disque. environment.systemPackages = with pkgs; [ - python312 - python312Packages.pip - python312Packages.virtualenv + @EL_PY_ODOO_PKG@ + @EL_PY_ODOO_PKG@Packages.pip + @EL_PY_ODOO_PKG@Packages.virtualenv + @EL_PY_TOOLS_PKG@ uv nodejs_22 postgresql @@ -336,7 +345,7 @@ # venv, donc avec le python du système. Son échec est silencieux # (« 2>/dev/null ») : sans elle, la première page ouverte attendrait le # chargement du registre sans que rien ne le dise. - path = with pkgs; [ bash python312 ]; + path = with pkgs; [ bash @EL_PY_ODOO_PKG@ ]; # L'unité est déclarée par le module, donc démarrée par la # reconstruction — qui a lieu PENDANT « make install_os », alors que la # source d'Odoo n'arrive qu'à « make install_odoo_18 ». Sans condition, diff --git a/conf/python-erplibre-version b/conf/python-erplibre-version index d367df0..38f27a2 100644 --- a/conf/python-erplibre-version +++ b/conf/python-erplibre-version @@ -1 +1 @@ -3.12.10 \ No newline at end of file +3.14.7 \ No newline at end of file diff --git a/doc/WINDOWS_INSTALLATION.base.md b/doc/WINDOWS_INSTALLATION.base.md index 44201e4..27de3d9 100644 --- a/doc/WINDOWS_INSTALLATION.base.md +++ b/doc/WINDOWS_INSTALLATION.base.md @@ -439,6 +439,10 @@ If these last steps to set up your development environment were unsuccessful, fo ## Manual Installation +This system interpreter only needs to reach `make` (Python 3.10 or later). +`.venv.erplibre` is built on its own version, `conf/python-erplibre-version`, +through `EL_PYTHON_PROVIDER` (mise or pyenv). + ### Install Python 3.10.14 You can delete the files that are left over in your home directory regarding the python installation when the steps have been completed succesfully. @@ -447,6 +451,10 @@ Si ces dernières étapes pour configurer votre environnement de développement ## Installation manuelle +Cet interpréteur système ne sert qu'à atteindre `make` (Python 3.10 ou plus). +`.venv.erplibre` est bâti sur sa propre version, `conf/python-erplibre-version`, +par `EL_PYTHON_PROVIDER` (mise ou pyenv). + ### Installer Python 3.10.14 Vous pouvez supprimer les fichiers restants dans votre répertoire personnel concernant l'installation de Python une fois les étapes complétées avec succès. diff --git a/doc/WINDOWS_INSTALLATION.fr.md b/doc/WINDOWS_INSTALLATION.fr.md index a6a2b49..03abb25 100644 --- a/doc/WINDOWS_INSTALLATION.fr.md +++ b/doc/WINDOWS_INSTALLATION.fr.md @@ -236,6 +236,10 @@ Si ces dernières étapes pour configurer votre environnement de développement ## Installation manuelle +Cet interpréteur système ne sert qu'à atteindre `make` (Python 3.10 ou plus). +`.venv.erplibre` est bâti sur sa propre version, `conf/python-erplibre-version`, +par `EL_PYTHON_PROVIDER` (mise ou pyenv). + ### Installer Python 3.10.14 Vous pouvez supprimer les fichiers restants dans votre répertoire personnel concernant l'installation de Python une fois les étapes complétées avec succès. diff --git a/doc/WINDOWS_INSTALLATION.md b/doc/WINDOWS_INSTALLATION.md index 912ef51..440ec76 100644 --- a/doc/WINDOWS_INSTALLATION.md +++ b/doc/WINDOWS_INSTALLATION.md @@ -236,6 +236,10 @@ If these last steps to set up your development environment were unsuccessful, fo ## Manual Installation +This system interpreter only needs to reach `make` (Python 3.10 or later). +`.venv.erplibre` is built on its own version, `conf/python-erplibre-version`, +through `EL_PYTHON_PROVIDER` (mise or pyenv). + ### Install Python 3.10.14 You can delete the files that are left over in your home directory regarding the python installation when the steps have been completed succesfully. diff --git a/docker/Dockerfile.prod.pkg b/docker/Dockerfile.prod.pkg index 5d848f3..d560b18 100644 --- a/docker/Dockerfile.prod.pkg +++ b/docker/Dockerfile.prod.pkg @@ -37,8 +37,12 @@ RUN cd $ODOO_PREFIX && \ ls -lha /ERPLibre && \ python -m venv .venv.$ERPLIBRE_VERSION +# Le venv des outils prend le python de l'image, celui d'Odoo : le RUN +# s'arrete ici si ce python ne sait pas lire la syntaxe de script/. RUN cd $ODOO_PREFIX && \ ls -lha /ERPLibre && \ + { python -m compileall -q script/ || \ + { echo "Le python de l'image ($(python -V 2>&1)) ne lit pas script/ : prendre une version Odoo plus recente." ; exit 1 ; } ; } && \ python -m venv .venv.erplibre && \ source ./.venv.erplibre/bin/activate && \ pip3 install -r requirement/erplibre_require-ments.txt diff --git a/docker/README.base.md b/docker/README.base.md index 3df3924..cab75f0 100644 --- a/docker/README.base.md +++ b/docker/README.base.md @@ -70,11 +70,21 @@ docker build -f Dockerfile.prod.pkg -t technolibre/erplibre:12.0-pkg . ``` + +`.venv.erplibre` is built on the image's own Python, Odoo's, without mise or +pyenv. The build stops when that Python cannot parse `script/`, which happens +with a deprecated Odoo version. + ### Running ERPLibre using Docker-Compose Go at the root of this git project. + +`.venv.erplibre` est bâti sur le Python de l'image, celui d'Odoo, sans mise ni +pyenv. La construction s'arrête quand ce Python ne sait pas lire `script/`, ce +qui arrive avec une version d'Odoo dépréciée. + ### Exécuter ERPLibre avec Docker-Compose Allez à la racine de ce projet git. diff --git a/docker/README.fr.md b/docker/README.fr.md index f2f3e47..23c7597 100644 --- a/docker/README.fr.md +++ b/docker/README.fr.md @@ -34,6 +34,11 @@ docker build -f Dockerfile.base -t technolibre/erplibre-base:12.0 . docker build -f Dockerfile.prod.pkg -t technolibre/erplibre:12.0-pkg . ``` + +`.venv.erplibre` est bâti sur le Python de l'image, celui d'Odoo, sans mise ni +pyenv. La construction s'arrête quand ce Python ne sait pas lire `script/`, ce +qui arrive avec une version d'Odoo dépréciée. + ### Exécuter ERPLibre avec Docker-Compose Allez à la racine de ce projet git. diff --git a/docker/README.md b/docker/README.md index 5535ec7..e411725 100644 --- a/docker/README.md +++ b/docker/README.md @@ -34,6 +34,11 @@ docker build -f Dockerfile.base -t technolibre/erplibre-base:12.0 . docker build -f Dockerfile.prod.pkg -t technolibre/erplibre:12.0-pkg . ``` + +`.venv.erplibre` is built on the image's own Python, Odoo's, without mise or +pyenv. The build stops when that Python cannot parse `script/`, which happens +with a deprecated Odoo version. + ### Running ERPLibre using Docker-Compose Go at the root of this git project. diff --git a/script/install/install_erplibre.sh b/script/install/install_erplibre.sh index d74178a..2025ee1 100755 --- a/script/install/install_erplibre.sh +++ b/script/install/install_erplibre.sh @@ -1,6 +1,58 @@ #!/usr/bin/env bash -# TODO deprecated, this is moved into install_locally.sh to be sure venv is installed and force take specified supported version -python3 -m venv .venv.erplibre -source .venv.erplibre/bin/activate -pip3 install -r requirement/erplibre_require-ments.txt -npm install +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +# +# Pose le venv d'OUTILLAGE d'ERPLibre, sans qu'une version d'Odoo soit +# choisie : chemin et version se lisent dans conf/, versionné, et non par +# env_var.sh, qui exige .python-odoo-version. Se lance depuis la racine du +# dépôt. + +CONF_VENV="conf/python-erplibre-venv" +CONF_PYTHON_VERSION="conf/python-erplibre-version" + +for conf_file in "${CONF_VENV}" "${CONF_PYTHON_VERSION}"; do + if [[ ! -f "${conf_file}" ]]; then + echo "Configuration introuvable : ${conf_file}" + echo " Ce script se lance depuis la racine du depot :" + echo " cd && ./script/install/install_erplibre.sh" + exit 1 + fi +done + +# « xargs » retire espaces et retour de ligne autour de la valeur. +VENV_ERPLIBRE_PATH=$(xargs < "${CONF_VENV}") +PYTHON_ERPLIBRE_VERSION=$(xargs < "${CONF_PYTHON_VERSION}") + +echo -e "Install ${VENV_ERPLIBRE_PATH} with ${PYTHON_ERPLIBRE_VERSION}" +if ! ./script/install/install_venv.sh \ + "ERPLibre" "${VENV_ERPLIBRE_PATH}" "${PYTHON_ERPLIBRE_VERSION}"; then + echo "Echec de creation de ${VENV_ERPLIBRE_PATH}, arret." + exit 1 +fi + +# Un venv rebâti n'a plus bin/repo ; install_git_repo.sh ne le pose que s'il +# manque. TODO s'en passe : seules les synchronisations de dépôts l'exigent, +# donc un échec (hors ligne) est signalé sans arrêter l'installation. +if ! ./script/install/install_git_repo.sh; then + echo "git-repo n'est pas pose dans ${VENV_ERPLIBRE_PATH} : 'repo sync' en a besoin." + echo " Rejouez quand le reseau le permet : ./script/install/install_git_repo.sh" +fi + +# el_pip_install vise le venv par son chemin : aucune activation n'est requise. +# shellcheck source=script/install/lib_pip_provider.sh +. ./script/install/lib_pip_provider.sh +if ! el_pip_install "${VENV_ERPLIBRE_PATH}" \ + -r requirement/erplibre_require-ments.txt; then + echo "Echec d'installation des dependances Python dans ${VENV_ERPLIBRE_PATH}." + echo " Relancez : ./script/install/install_erplibre.sh" + exit 1 +fi + +# Prettier ne sert qu'au formatage XML (« make format ») : son échec est +# signalé sans faire échouer une installation Python complète. +if ! npm install; then + echo "npm install a echoue : prettier et son greffon XML ne sont pas poses." + echo " ${VENV_ERPLIBRE_PATH} est utilisable ; seul 'make format' les reclame." + echo " Rejouez quand le reseau le permet : npm install" +fi +exit 0 diff --git a/script/install/install_git_repo.sh b/script/install/install_git_repo.sh index 54c7214..41304a6 100755 --- a/script/install/install_git_repo.sh +++ b/script/install/install_git_repo.sh @@ -1,14 +1,44 @@ #!/usr/bin/env bash -VENV_ERPLIBRE_PATH=$(cat "conf/python-erplibre-venv" | xargs) +# Se lance depuis la racine du dépôt : ailleurs, le chemin de venv serait vide +# et la cible deviendrait « /bin/repo ». +CONF_VENV="conf/python-erplibre-venv" +if [[ ! -f "${CONF_VENV}" ]]; then + echo "Configuration introuvable : ${CONF_VENV}" + echo " cd && ./script/install/install_git_repo.sh" + exit 1 +fi +VENV_ERPLIBRE_PATH=$(xargs < "${CONF_VENV}") VENV_REPO_PATH=${VENV_ERPLIBRE_PATH}/bin/repo # Install git-repo if missing if [[ ! -f ${VENV_REPO_PATH} ]]; then - echo "\n---- Install git-repo from Google APIS ----" - curl https://storage.googleapis.com/git-repo-downloads/repo > ${VENV_REPO_PATH} - chmod +x ${VENV_REPO_PATH} - sed -i 1d ${VENV_REPO_PATH} + if [[ ! -d "${VENV_ERPLIBRE_PATH}/bin" ]]; then + echo "Venv ${VENV_ERPLIBRE_PATH} absent : posez-le avant git-repo." + echo " ./script/install/install_erplibre.sh" + exit 1 + fi + echo -e "\n---- Install git-repo from Google APIS ----" + # Téléchargé à côté, contrôlé, puis déplacé : sans « -f », curl écrit la + # page d'une erreur HTTP et rend 0 ; un corps vide passe le statut. + REPO_TMP="$(mktemp)" || exit 1 + if ! curl -fsSL -o "${REPO_TMP}" \ + https://storage.googleapis.com/git-repo-downloads/repo \ + || [[ ! -s "${REPO_TMP}" ]]; then + echo "Telechargement de git-repo impossible ou vide (reseau ou cache) :" + echo " ${VENV_REPO_PATH} n'est pas pose." + rm -f "${REPO_TMP}" + exit 1 + fi + # Le hashbang du venv remplace celui du python3 du système. + sed -i 1d "${REPO_TMP}" PYTHON_HASHBANG="#!./${VENV_ERPLIBRE_PATH}/bin/python" - sed -i "1 i ${PYTHON_HASHBANG}" ${VENV_REPO_PATH} + sed -i "1 i ${PYTHON_HASHBANG}" "${REPO_TMP}" + if ! mv "${REPO_TMP}" "${VENV_REPO_PATH}"; then + echo "Mise en place de ${VENV_REPO_PATH} impossible." + rm -f "${REPO_TMP}" + exit 1 + fi + # mktemp crée en 0600. + chmod 755 "${VENV_REPO_PATH}" fi diff --git a/script/install/install_nixos_dependency.sh b/script/install/install_nixos_dependency.sh index 36a3b1c..af37ff4 100755 --- a/script/install/install_nixos_dependency.sh +++ b/script/install/install_nixos_dependency.sh @@ -37,6 +37,21 @@ EL_CA_BUNDLE=/var/lib/erplibre/ca-bundle.crt [ -r "${EL_CA_BUNDLE}" ] || EL_CA_BUNDLE="" EL_LOCALE=${EL_LOCALE:-$(lire_seed locale)} EL_TZ=${EL_TZ:-$(lire_seed timezone)} +# « 3.12.10 » donne « python312 », le nom du paquet nixpkgs. Les deux versions +# du dépôt sont déclarées : celle d'Odoo et celle de l'outillage. Le second est +# VIDE quand elles coïncident, un même paquet nommé deux fois entrant en +# collision dans le profil. +el_nix_python_pkg() { + local v + v="$(xargs < "$1" 2> /dev/null)" + [ -n "${v}" ] || return 0 + echo "python$(echo "${v}" | cut -d. -f1)$(echo "${v}" | cut -d. -f2)" +} +EL_PY_ODOO_PKG="$(el_nix_python_pkg .python-odoo-version)" +EL_PY_ODOO_PKG="${EL_PY_ODOO_PKG:-python312}" +EL_PY_TOOLS_PKG="$(el_nix_python_pkg conf/python-erplibre-version)" +[ "${EL_PY_TOOLS_PKG}" = "${EL_PY_ODOO_PKG}" ] && EL_PY_TOOLS_PKG="" + MODULE_SRC="conf/nixos/erplibre.nix" MODULE_DST="/etc/nixos/erplibre.nix" CONFIG="/etc/nixos/configuration.nix" @@ -57,9 +72,12 @@ echo -e "\n---- Module ERPLibre pour NixOS ----" sed -e "s/@EL_USER@/${EL_USER}/g" -e "s#@EL_DIR@#${EL_DIR}#g" \ -e "s/@EL_LOCALE@/${EL_LOCALE}/g" -e "s#@EL_TZ@#${EL_TZ}#g" \ -e "s#@EL_CA_BUNDLE@#${EL_CA_BUNDLE}#g" \ + -e "s/@EL_PY_ODOO_PKG@/${EL_PY_ODOO_PKG}/g" \ + -e "s/@EL_PY_TOOLS_PKG@/${EL_PY_TOOLS_PKG}/g" \ "${MODULE_SRC}" | sudo tee "${MODULE_DST}" > /dev/null echo " posé : ${MODULE_DST} (compte ${EL_USER}, dépôt ${EL_DIR})" echo " autorité du cache : ${EL_CA_BUNDLE:-aucune}" +echo " Python : ${EL_PY_ODOO_PKG} (Odoo)${EL_PY_TOOLS_PKG:+, ${EL_PY_TOOLS_PKG} (outillage)}" echo " régional : locale « ${EL_LOCALE:-non demandée} »," \ "fuseau « ${EL_TZ:-non demandé} »" diff --git a/script/install/install_venv.sh b/script/install/install_venv.sh index 4f4dc8f..3785d11 100755 --- a/script/install/install_venv.sh +++ b/script/install/install_venv.sh @@ -9,8 +9,13 @@ fi # Assign arguments to variables CONTEXT="$1" -VENV_PATH="$2" +# Sans « / » final : « rm -rf lien/ » viderait la cible d'un lien. +VENV_PATH="${2%"${2##*[!/]}"}" PYTHON_VERSION="$3" +if [ -z "${VENV_PATH}" ]; then + echo "Error: Venv_Path '$2' designates the root." + exit 1 +fi # Display variables (for verification) echo "Context: $CONTEXT" @@ -18,20 +23,140 @@ echo "Venv Path: $VENV_PATH" echo "Python Version: $PYTHON_VERSION" # 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. +# ne connaît qu'un chemin d'interpréteur. # shellcheck source=script/install/lib_python_provider.sh . ./script/install/lib_python_provider.sh -PYTHON_EXEC="$(el_python_exec "${PYTHON_VERSION}")" +# Ce qui borne le PATCH, et pour qui. +# +# Le venv d'Odoo est contraint par son pyproject (« >=3.12.10,<3.13 ») : un +# patch inférieur y serait refusé par Poetry. Rien n'épingle celui de +# l'outillage — exiger le patch y écarte le Python des distributions dès +# qu'il est d'un cran en retard, et force pyenv à compiler CPython pour rien. +EXIGENCE="patch" +[ "${CONTEXT}" = "ERPLibre" ] && EXIGENCE="mineure" + +# Version RÉELLE d'un venv : celle que rend son interpréteur, jamais celle +# qu'annonce pyvenv.cfg, figée à la création et que rien ne revalide. Rien +# n'est rendu quand bin/python ne s'exécute plus — un venv bâti sur un CPython +# depuis retiré garde un lien mort, et c'est justement un venv à rebâtir. +el_venv_python_version() { + local py="$1/bin/python" + [ -x "${py}" ] || return 1 + "${py}" -c 'import platform;print(platform.python_version())' 2> /dev/null +} + +# Le venv est-il HORS SERVICE — non pas d'une autre version, mais inutilisable +# tel quel ? Ce qui suit ne se répare pas : on rebâtit, quel que soit l'appelant. +el_venv_hors_service() { + local py="$1/bin/python" + if [[ ! -f "$1/pyvenv.cfg" ]]; then + echo "aucun pyvenv.cfg" + elif [[ -z "$(el_venv_python_version "$1")" ]]; then + echo "son interpreteur ne s'execute plus" + elif [[ ! -f "$1/bin/activate" ]] || ! "${py}" -m pip --version > /dev/null 2>&1; then + # Ce que laisse un « python -m venv » sans ensurepip. + echo "bin/activate ou pip manquant" + fi +} + +# Le venv tourne, mais pas sur la version demandée. La version est celle que +# rend bin/python, pas celle de pyvenv.cfg, figée à la création. Compatible et +# non identique : un python de distribution d'un patch plus récent convient, et +# l'égalité stricte rebâtirait à chaque appel. +el_venv_version_autre() { + if ! el_python_is_compatible \ + "$1/bin/python" "${PYTHON_VERSION}" "${EXIGENCE}"; then + echo "bin/python en $(el_venv_python_version "$1"), ${PYTHON_VERSION} exige" + fi +} + +# Ce qui oblige à rebâtir, ou rien. +# +# Une version qui diffère ne vaut destruction que pour le venv d'OUTILLAGE : +# le rebâtir coûte une installation pip, quand celui d'Odoo représente une +# installation Poetry entière — des centaines de paquets, dont certains se +# compilent. Un écart de patch ne l'empêche pas de servir ; on le dit, et on le +# laisse. Hors service, en revanche, il ne sert plus personne. +el_venv_a_rebatir() { + local cause + cause="$(el_venv_hors_service "$1")" + if [[ -n "${cause}" ]]; then + echo "${cause}" + return + fi + cause="$(el_venv_version_autre "$1")" + if [[ -n "${cause}" ]] && [[ "${CONTEXT}" == "ERPLibre" ]]; then + echo "${cause}" + fi +} + +# Un checkout MONTÉ depuis une autre machine porte le venv de cette machine. +# Son interpréteur ne démarre pas ici — un CPython précompilé cherche sa +# bibliothèque standard sous le préfixe de sa construction —, ce qui le fait +# passer pour défectueux. Le détruire effacerait, à travers le réseau, +# l'installation de la machine à qui il appartient. +# findmnt vient d'util-linux : absent ailleurs, le contrôle s'abstient plutôt +# que de refuser à tort. +el_venv_est_distant() { + local fs + command -v findmnt > /dev/null 2>&1 || return 1 + fs="$(findmnt -n -o FSTYPE --target "$1" 2> /dev/null)" + case "${fs}" in + fuse* | sshfs | nfs* | cifs | smb*) return 0 ;; + *) return 1 ;; + esac +} + +PYTHON_EXEC="$(el_python_exec "${PYTHON_VERSION}" "${EXIGENCE}")" 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." + if command -v mise > /dev/null 2>&1; then + echo " Voir : mise install python@${PYTHON_VERSION} (reseau requis)" + else + echo " Voir 'make install_mise', ou installez pyenv." + fi exit 1 fi echo "Interpreteur retenu : ${PYTHON_EXEC}" +# L'interpréteur est obtenu AVANT toute destruction : un provisionnement en +# échec laisse le venv en place. +if [[ -d ${VENV_PATH} ]]; then + REASON="$(el_venv_a_rebatir "${VENV_PATH}")" + CONSERVE="$(el_venv_version_autre "${VENV_PATH}")" + if [[ -z "${REASON}" ]] && [[ -n "${CONSERVE}" ]]; then + echo "Virtual environment ${VENV_PATH} conserve : ${CONSERVE}." + echo " Le rebatir refait toute son installation. Si vous le voulez :" + echo " rm -rf ${VENV_PATH} puis relancez." + fi + # rmdir n'écarte qu'un répertoire vide, ce que laisse une création + # interrompue ; seul un pyvenv.cfg autorise le « rm -rf ». + if [[ -n "${REASON}" ]] && ! rmdir "${VENV_PATH}" 2> /dev/null; then + if [[ ! -f "${VENV_PATH}/pyvenv.cfg" ]]; then + echo "Refus de detruire ${VENV_PATH} : ce n'est pas un venv (${REASON})." + echo " Ecartez-le vous-meme, puis relancez." + exit 1 + fi + if el_venv_est_distant "${VENV_PATH}"; then + echo "Refus de detruire ${VENV_PATH} : il est sur un systeme de" + echo " fichiers monte a distance, donc il appartient a une autre" + echo " machine (${REASON} vu d'ici)." + echo " Installez SUR cette machine-la, pas au travers du montage." + exit 1 + fi + echo "DESTRUCTION de ${VENV_PATH} : ${REASON}." + echo " Ce qui y a ete pose a la main part avec lui ; il est rebati." + if ! rm -rf "${VENV_PATH}"; then + echo "Destruction de ${VENV_PATH} en echec, probablement faute de droits :" + echo " le venv est peut-etre partiellement detruit. Rien n'est rebati." + echo " Corrigez les droits, puis : rm -rf ${VENV_PATH} et relancez." + exit 1 + fi + fi +fi + if [[ ! -d ${VENV_PATH} ]]; then echo -e "\n---- Create Virtual environment Python ----" if ! "${PYTHON_EXEC}" -m venv "${VENV_PATH}"; then @@ -39,3 +164,13 @@ if [[ ! -d ${VENV_PATH} ]]; then exit 1 fi fi + +# Contrôle final : l'appelant enchaîne sur ce venv, un 0 doit le garantir. +# Hors service seulement : un venv d'une autre version qu'on vient de conserver +# sciemment reste utilisable. +REASON="$(el_venv_hors_service "${VENV_PATH}")" +if [[ ! -d ${VENV_PATH} ]] || [[ -n "${REASON}" ]]; then + echo "Virtual environment ${VENV_PATH} inutilisable : ${REASON:-absent}." + exit 1 +fi +echo "Virtual environment ${VENV_PATH} pret." diff --git a/script/install/lib_python_provider.sh b/script/install/lib_python_provider.sh index 7865e7a..fea7a58 100755 --- a/script/install/lib_python_provider.sh +++ b/script/install/lib_python_provider.sh @@ -55,22 +55,29 @@ el_python_is_version() { [ "${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. +# Vrai si l'exécutable CONVIENT. Deux exigences, selon ce qui borne l'appelant. # -# 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. +# patch même majeure.mineure, et patch au moins égal. C'est ce que le +# pyproject d'Odoo demande — « >=3.12.10,<3.13 » —, et Poetry +# refuserait un patch inférieur. +# mineure même majeure.mineure, quel que soit le patch. RIEN n'épingle le +# patch du venv d'OUTILLAGE : l'exiger écarte le Python des +# distributions dès qu'il est d'un patch en retard — NixOS 25.11 +# livre 3.14.2 quand conf/ demande 3.14.7 — et force pyenv à +# COMPILER CPython pour une différence qui ne gêne personne. +# +# L'égalité stricte, elle, rejetterait aussi un patch plus RÉCENT : c'est +# el_python_is_version, et elle ne sert qu'aux fournisseurs qui posent une +# version nommée. el_python_is_compatible() { - local exe="$1" want="$2" got + local exe="$1" want="$2" exigence="${3:-patch}" 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 + [ "${exigence}" = "mineure" ] && return 0 # 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}" ] } @@ -81,7 +88,7 @@ el_python_is_compatible() { # 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 + local want="$1" exigence="${2:-patch}" 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. @@ -94,7 +101,7 @@ el_distro_python_exec() { # pas, et la boucle passe a la suite sans rien couter. for exe in "/run/current-system/sw/bin/python${want%.*}" \ "/usr/bin/python${want%.*}" "/usr/local/bin/python${want%.*}"; do - if el_python_is_compatible "${exe}" "${want}"; then + if el_python_is_compatible "${exe}" "${want}" "${exigence}"; then echo "${exe}" return 0 fi @@ -175,10 +182,31 @@ el_pyenv_install() { echo "${exe}" } +# Annonce la compilation pyenv AVANT de la subir, et le geste qui l'évite : +# poser mise, ou lire ce que mise reproche quand il est déjà là. Muet si pyenv +# porte déjà la version, puisque rien ne sera compilé. +el_warn_pyenv_fallback() { + local version="$1" + [ -d "$(el_pyenv_root)/versions/${version}" ] && return 0 + if command -v mise > /dev/null 2>&1; then + echo "mise n'a pas pu fournir Python ${version} : repli sur pyenv," >&2 + echo " qui COMPILE CPython. Pour lire ce que mise reproche :" >&2 + echo " mise install python@${version} (reseau requis)" >&2 + else + echo "mise est absent : pyenv va etre pose, puis COMPILER CPython" >&2 + echo " ${version} -- quelques minutes, bien plus sous emulation, et il" >&2 + echo " lui faut une douzaine de -dev (openssl, zlib, readline, sqlite," >&2 + echo " bzip2, xz, tk). Pour l'eviter : Ctrl+C, puis" >&2 + echo " make install_mise" >&2 + echo " qui pose un CPython precompile en quelques secondes, et relancez." >&2 + fi +} + # 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 + local version="$1" exigence="${2:-patch}" + local 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 @@ -205,7 +233,7 @@ el_python_exec() { # 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 + && exe="$(el_distro_python_exec "${version}" "${exigence}")"; then echo "Python $("${exe}" -V 2>&1 | awk '{print $2}') de la distribution :" \ "aucune compilation." >&2 echo "${exe}" @@ -220,8 +248,7 @@ el_python_exec() { 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 + el_warn_pyenv_fallback "${version}" fi el_pyenv_install "${version}" } diff --git a/script/todo/source_todo.sh b/script/todo/source_todo.sh index 4a31863..f922e2c 100755 --- a/script/todo/source_todo.sh +++ b/script/todo/source_todo.sh @@ -1,36 +1,13 @@ -#!/bin/bash -DIR_VENV_ERPLIBRE_EXIST=1 -DIR_VENV_ERPLIBRE=".venv.erplibre" -# If not exist, create it and do installation -# Can be in conflict with ./script/install_locally.sh +#!/usr/bin/env bash +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +# +# Auto-installation de TODO : install_erplibre.sh pose ou rebâtit le venv +# d'outillage, puis TODO repart sur l'interpréteur de ce venv. +# Se lance depuis la racine du dépôt. -if [[ ! -d "$DIR_VENV_ERPLIBRE" ]]; then - DIR_VENV_ERPLIBRE_EXIST=0 - echo "$DIR_VENV_ERPLIBRE does not exist." - # 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 - python3 -m venv $DIR_VENV_ERPLIBRE - fi +if ! ./script/install/install_erplibre.sh; then + echo "Installation d'ERPLibre en echec : TODO ne demarre pas." + exit 1 fi - -# If exist, source it and start installation -source ./.venv.erplibre/bin/activate -if [[ $DIR_VENV_ERPLIBRE_EXIST -eq 0 ]]; then - pip install -r requirement/erplibre_require-ments.txt -fi - -./script/todo/todo.py +exec "./$(xargs < conf/python-erplibre-venv)/bin/python" ./script/todo/todo.py "$@" diff --git a/test/test_install_nixos.py b/test/test_install_nixos.py index a74f4bd..5094733 100644 --- a/test/test_install_nixos.py +++ b/test/test_install_nixos.py @@ -153,10 +153,31 @@ class LeModule(unittest.TestCase): with self.subTest(lib=lib): self.assertIn(lib, self.src) - def test_the_python_matches_what_the_repository_wants(self): - voulu = (RACINE / ".python-odoo-version").read_text().strip() - majeur, mineur = voulu.split(".")[:2] - self.assertIn(f"python{majeur}{mineur}", self.src) + def test_no_python_version_is_written_by_hand(self): + """Une version écrite ici dériverait des fichiers du dépôt au premier + changement — et déclarer le seul Python d'Odoo laissait pyenv bâtir + celui de l'outillage, dont la compilation s'arrête sur + « Modules/_cursesmodule.o ».""" + self.assertNotRegex(self.src, r"python3\d\d") + self.assertIn("@EL_PY_ODOO_PKG@", self.src) + self.assertIn("@EL_PY_TOOLS_PKG@", self.src) + + def test_both_pythons_of_the_repository_are_declared(self): + """Le module les reçoit du script, qui les lit dans les deux fichiers + de version.""" + script = SCRIPT.read_text(encoding="utf-8") + self.assertIn(".python-odoo-version", script) + self.assertIn("conf/python-erplibre-version", script) + + def test_the_second_package_is_empty_when_both_agree(self): + """Nommer deux fois le même paquet ferait entrer en collision deux + chemins identiques dans le profil.""" + script = SCRIPT.read_text(encoding="utf-8") + self.assertRegex( + script, + r'EL_PY_TOOLS_PKG\}"?\s*=\s*"?\$\{EL_PY_ODOO_PKG' + r'|\[ "\$\{EL_PY_TOOLS_PKG\}" = "\$\{EL_PY_ODOO_PKG\}" \]', + ) def test_the_database_role_is_substituted(self): """Le nom du compte varie d'un déploiement à l'autre ; l'écrire en dur @@ -335,7 +356,7 @@ class LeServiceEstDeclare(unittest.TestCase): """Une unité ne reçoit pas le PATH d'une session. run.sh lance des scripts dont le shebang est « env bash » : env est dans le PATH par défaut, bash non, et run.sh s'arrête avant Odoo.""" - self.assertIn("path = with pkgs; [ bash python312 ];", self.src) + self.assertIn("path = with pkgs; [ bash @EL_PY_ODOO_PKG@ ];", self.src) def test_the_repository_is_not_guessed(self): """Le service lance le dépôt QUI A POSÉ le module. Écrire diff --git a/test/test_install_venv_rebuild.py b/test/test_install_venv_rebuild.py new file mode 100644 index 0000000..58de48a --- /dev/null +++ b/test/test_install_venv_rebuild.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +# © 2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +"""install_venv.sh détruit un venv : sous quelles conditions, exactement ? + +C'est le seul endroit du dépôt qui efface le travail de quelqu'un. Un venv +porte ce qu'on y a posé à la main — bin/repo, uv, des paquets — et le nom +.venv.erplibre ne dit pas sa version : rien ne distingue de l'extérieur celui +qu'il faut rebâtir de celui qu'il faut garder. + +Ces tests pèsent donc les REFUS autant que les destructions : + +- un répertoire sans pyvenv.cfg n'est pas un venv et garde son contenu ; +- un venv sur un système de fichiers monté appartient à une autre machine, et + l'effacer le ferait à travers le réseau ; +- un venv utilisable est conservé, sans quoi chaque installation rebâtirait + ce qui marche. + +L'interpréteur est un bouchon annoncé par PYENV_ROOT : les tests restent hors +réseau, et rien ne dépend de ce que cette machine a installé. +""" + +import os +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + +RACINE = Path(__file__).resolve().parents[1] +SCRIPT = RACINE / "script/install/install_venv.sh" + +VOULUE = "9.99.0" +AUTRE = "9.98.0" + +# Un python qui suffit à ce que le script lui demande : sa version, la création +# d'un venv, et la présence de pip. +BOUCHON = """#!/bin/sh +VERSION=__VERSION__ +case "$1" in + -V|--version) echo "Python ${VERSION}" ;; + -c) echo "${VERSION}" ;; + -m) + case "$2" in + venv) + mkdir -p "$3/bin" || exit 1 + printf 'version = %s\\n' "${VERSION}" > "$3/pyvenv.cfg" + sed "s/^VERSION=.*/VERSION=${VERSION}/" "$0" > "$3/bin/python" + chmod +x "$3/bin/python" + : > "$3/bin/activate" + ;; + pip) echo "pip 99.0 from nowhere" ;; + *) exit 1 ;; + esac + ;; + *) exit 1 ;; +esac +exit 0 +""" + + +def pose_bouchon(chemin, version): + chemin.parent.mkdir(parents=True, exist_ok=True) + chemin.write_text( + BOUCHON.replace("__VERSION__", version), encoding="utf-8" + ) + chemin.chmod(0o755) + + +class BancInstallVenv(unittest.TestCase): + """Un PYENV_ROOT bouchonné, et un répertoire de travail jetable.""" + + def setUp(self): + self.coin = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.coin, ignore_errors=True) + pose_bouchon( + self.coin / "pyenv" / "versions" / VOULUE / "bin" / "python", + VOULUE, + ) + self.bin = self.coin / "bin" + self.bin.mkdir() + + def env(self): + return dict( + os.environ, + PYENV_ROOT=str(self.coin / "pyenv"), + PATH=f"{self.bin}:{os.environ['PATH']}", + EL_PYTHON_PROVIDER="pyenv", + ) + + def lance(self, chemin_venv, contexte="ERPLibre"): + return subprocess.run( + [str(SCRIPT), contexte, str(chemin_venv), VOULUE], + cwd=RACINE, + capture_output=True, + text=True, + env=self.env(), + ) + + def venv_factice(self, nom, version): + """Un venv complet, comme « python -m venv » le laisse.""" + chemin = self.coin / nom + (chemin / "bin").mkdir(parents=True) + (chemin / "pyvenv.cfg").write_text(f"version = {version}\n") + pose_bouchon(chemin / "bin" / "python", version) + (chemin / "bin" / "activate").touch() + (chemin / "TEMOIN").write_text("pose a la main\n") + return chemin + + +class TestCeQuIlConserve(BancInstallVenv): + def test_un_venv_a_la_bonne_version_est_garde(self): + venv = self.venv_factice("bon", VOULUE) + fin = self.lance(venv) + self.assertEqual(0, fin.returncode, fin.stdout + fin.stderr) + self.assertTrue( + (venv / "TEMOIN").exists(), "il a été rebâti pour rien" + ) + + def test_un_chemin_neuf_est_simplement_cree(self): + venv = self.coin / "neuf" + fin = self.lance(venv) + self.assertEqual(0, fin.returncode, fin.stdout + fin.stderr) + self.assertTrue((venv / "pyvenv.cfg").exists()) + + +class TestCeQuIlDetruit(BancInstallVenv): + def test_un_venv_d_une_autre_version_est_rebati(self): + venv = self.venv_factice("perime", AUTRE) + fin = self.lance(venv) + self.assertEqual(0, fin.returncode, fin.stdout + fin.stderr) + self.assertIn("DESTRUCTION", fin.stdout) + self.assertFalse((venv / "TEMOIN").exists()) + self.assertIn(VOULUE, (venv / "pyvenv.cfg").read_text()) + + def test_la_destruction_est_annoncee_avant_d_avoir_lieu(self): + """Dans un journal de milliers de lignes, l'avis doit précéder.""" + venv = self.venv_factice("perime", AUTRE) + fin = self.lance(venv) + self.assertLess( + fin.stdout.index("DESTRUCTION"), + fin.stdout.index("Create Virtual environment"), + ) + + def test_un_venv_sans_pip_est_rebati(self): + """Ce que laisse « python -m venv » quand ensurepip manque.""" + venv = self.venv_factice("sans_pip", VOULUE) + (venv / "bin" / "activate").unlink() + fin = self.lance(venv) + self.assertEqual(0, fin.returncode, fin.stdout + fin.stderr) + self.assertFalse((venv / "TEMOIN").exists()) + + +class TestLExigenceDePatch(BancInstallVenv): + """Le patch ne borne que le venv d'Odoo, contraint par son pyproject. + + Sur le venv d'outillage, l'exiger écarterait le Python des distributions + dès qu'il est d'un cran en retard — et ferait compiler CPython pour une + différence que rien ne réclame.""" + + PATCH_PLUS_BAS = "9.99.0" + DEMANDE = "9.99.4" + + def setUp(self): + super().setUp() + pose_bouchon( + self.coin / "pyenv" / "versions" / self.DEMANDE / "bin" / "python", + self.DEMANDE, + ) + + def joue(self, contexte, venv): + return subprocess.run( + [str(SCRIPT), contexte, str(venv), self.DEMANDE], + cwd=RACINE, + capture_output=True, + text=True, + env=self.env(), + ) + + def test_l_outillage_garde_un_patch_plus_ancien(self): + venv = self.venv_factice("outils", self.PATCH_PLUS_BAS) + fin = self.joue("ERPLibre", venv) + self.assertEqual(0, fin.returncode, fin.stdout + fin.stderr) + self.assertTrue((venv / "TEMOIN").exists(), fin.stdout) + + def test_odoo_refuse_un_patch_plus_ancien(self): + """Poetry refuserait un patch inférieur à ce que borne le pyproject.""" + venv = self.venv_factice("odoo", self.PATCH_PLUS_BAS) + fin = self.joue("Odoo", venv) + self.assertIn("conserve", fin.stdout + fin.stderr) + + def test_une_autre_mineure_ne_passe_jamais(self): + venv = self.venv_factice("mineure", "9.98.0") + fin = self.joue("ERPLibre", venv) + self.assertIn("DESTRUCTION", fin.stdout) + + +class TestCeQueLeContexteDecide(BancInstallVenv): + """Rebâtir le venv d'outillage coûte une installation pip ; rebâtir celui + d'Odoo en refait une de Poetry entière. L'écart de version ne vaut donc + destruction que pour le premier.""" + + def test_le_venv_odoo_d_une_autre_version_est_conserve(self): + venv = self.venv_factice("odoo", AUTRE) + fin = self.lance(venv, contexte="Odoo") + self.assertEqual(0, fin.returncode, fin.stdout + fin.stderr) + self.assertIn("conserve", fin.stdout) + self.assertTrue((venv / "TEMOIN").exists()) + + def test_le_venv_odoo_hors_service_est_rebati_quand_meme(self): + """Une autre version se tolère ; un interpréteur mort, non.""" + venv = self.venv_factice("odoo_casse", VOULUE) + (venv / "bin" / "python").write_text("#!/bin/sh\nexit 1\n") + fin = self.lance(venv, contexte="Odoo") + self.assertEqual(0, fin.returncode, fin.stdout + fin.stderr) + self.assertIn("DESTRUCTION", fin.stdout) + self.assertFalse((venv / "TEMOIN").exists()) + + +class TestCeQuIlRefuse(BancInstallVenv): + def test_un_repertoire_sans_pyvenv_cfg_garde_son_contenu(self): + pas_un_venv = self.coin / "donnees" + (pas_un_venv / "sous").mkdir(parents=True) + (pas_un_venv / "sous" / "PRECIEUX").write_text("ne pas effacer\n") + fin = self.lance(pas_un_venv) + self.assertEqual(1, fin.returncode) + self.assertIn("Refus de detruire", fin.stdout) + self.assertTrue((pas_un_venv / "sous" / "PRECIEUX").exists()) + + def test_un_venv_monte_a_distance_est_epargne(self): + """Il appartient à une autre machine : l'effacer traverse le réseau.""" + faux_findmnt = self.bin / "findmnt" + faux_findmnt.write_text("#!/bin/sh\necho fuse.sshfs\n") + faux_findmnt.chmod(0o755) + venv = self.venv_factice("monte", AUTRE) + fin = self.lance(venv) + self.assertEqual(1, fin.returncode) + self.assertIn("monte a distance", fin.stdout) + self.assertTrue((venv / "TEMOIN").exists()) + + def test_un_repertoire_vide_ne_demande_pas_d_intervention(self): + """Rien à perdre : rmdir suffit, et l'installation continue.""" + vide = self.coin / "vide" + vide.mkdir() + fin = self.lance(vide) + self.assertEqual(0, fin.returncode, fin.stdout + fin.stderr) + self.assertNotIn("Refus", fin.stdout) + + def test_la_racine_est_refusee(self): + fin = subprocess.run( + [str(SCRIPT), "ERPLibre", "/", VOULUE], + cwd=RACINE, + capture_output=True, + text=True, + env=self.env(), + ) + self.assertEqual(1, fin.returncode) + self.assertNotIn("DESTRUCTION", fin.stdout) + + +class TestSansInterpreteur(BancInstallVenv): + def test_aucune_destruction_quand_l_interpreteur_manque(self): + """L'interpréteur est obtenu AVANT : sinon on reste sans venv.""" + venv = self.venv_factice("perime", AUTRE) + env = dict(self.env(), PYENV_ROOT=str(self.coin / "nulle_part")) + fin = subprocess.run( + [str(SCRIPT), "ERPLibre", str(venv), VOULUE], + cwd=RACINE, + capture_output=True, + text=True, + env=env, + ) + self.assertEqual(1, fin.returncode) + self.assertTrue((venv / "TEMOIN").exists()) + + +if __name__ == "__main__": + unittest.main()