diff --git a/.claude/skills/erplibre-deployment/SKILL.md b/.claude/skills/erplibre-deployment/SKILL.md index c72f257..8499cd6 100644 --- a/.claude/skills/erplibre-deployment/SKILL.md +++ b/.claude/skills/erplibre-deployment/SKILL.md @@ -56,9 +56,14 @@ 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é. +`make` / `make todo` hors venv : TODO se relance dans `.venv.erplibre` ; si le +venv manque, il propose `install_erplibre.sh` en terminal, ou nomme la +commande. + 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/`. diff --git a/CHANGELOG.base.md b/CHANGELOG.base.md index c214a8b..6aec6a6 100644 --- a/CHANGELOG.base.md +++ b/CHANGELOG.base.md @@ -158,6 +158,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `--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 +- `make` and `make todo` go through `install.sh`, which picks an interpreter able to READ the code before running it: `.venv.erplibre` on the right version, else a recent enough system `python3`, else the install. A system older than `conf/python-erplibre-version` would otherwise stop on a syntax error raised before any guard could name the command to type. TODO then relaunches itself in `.venv.erplibre`, or offers to run `install_erplibre.sh` in a terminal - 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 @@ -180,6 +181,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `--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é +- `make` et `make todo` passent par `install.sh`, qui choisit un interpréteur capable de LIRE le code avant de le lancer : `.venv.erplibre` s'il porte la bonne version, sinon le `python3` du système s'il est assez récent, sinon l'installation. Un système plus ancien que `conf/python-erplibre-version` s'arrêterait autrement sur une erreur de syntaxe levée avant qu'aucun garde puisse nommer la commande à taper. TODO se relance ensuite dans `.venv.erplibre`, ou propose de lancer `install_erplibre.sh` en terminal - 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 diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index d1a9e56..8f7c503 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -80,6 +80,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `--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é +- `make` et `make todo` passent par `install.sh`, qui choisit un interpréteur capable de LIRE le code avant de le lancer : `.venv.erplibre` s'il porte la bonne version, sinon le `python3` du système s'il est assez récent, sinon l'installation. Un système plus ancien que `conf/python-erplibre-version` s'arrêterait autrement sur une erreur de syntaxe levée avant qu'aucun garde puisse nommer la commande à taper. TODO se relance ensuite dans `.venv.erplibre`, ou propose de lancer `install_erplibre.sh` en terminal - 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index eb715ea..8973cd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `--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 +- `make` and `make todo` go through `install.sh`, which picks an interpreter able to READ the code before running it: `.venv.erplibre` on the right version, else a recent enough system `python3`, else the install. A system older than `conf/python-erplibre-version` would otherwise stop on a syntax error raised before any guard could name the command to type. TODO then relaunches itself in `.venv.erplibre`, or offers to run `install_erplibre.sh` in a terminal - 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 diff --git a/README.base.md b/README.base.md index 70a9ad6..6f1e80e 100644 --- a/README.base.md +++ b/README.base.md @@ -254,6 +254,33 @@ make +`make` and `./install.sh` start TODO through the interpreter that can read it: +`.venv.erplibre` when it carries the right version, otherwise the system +`python3` when it is recent enough, otherwise the install itself — a system +older than `conf/python-erplibre-version` cannot parse the code, so it goes +straight to the install rather than stopping on a syntax error. When the +environment is missing but the system Python suffices, TODO offers to run +`./script/install/install_erplibre.sh` (in a terminal) or prints that command. The install builds the environment +through `EL_PYTHON_PROVIDER` (mise or pyenv); an existing `.venv.erplibre` on +another Python version is DELETED and rebuilt, and whatever was installed in it +by hand goes with it. + + + +`make` et `./install.sh` lancent TODO par l'interpréteur capable de le lire : +`.venv.erplibre` quand il porte la bonne version, sinon le `python3` du système +s'il est assez récent, sinon l'installation elle-même — un système plus ancien +que `conf/python-erplibre-version` ne sait pas analyser le code, donc on va +droit à l'installation plutôt que de s'arrêter sur une erreur de syntaxe. Quand +l'environnement manque mais que le Python du système suffit, TODO propose de +lancer `./script/install/install_erplibre.sh` (dans un terminal) ou affiche +cette commande. L'installation bâtit l'environnement par +`EL_PYTHON_PROVIDER` (mise ou pyenv) ; un `.venv.erplibre` existant sur une +autre version de Python est SUPPRIMÉ puis rebâti, et ce qu'on y avait posé à la +main part avec lui. + + + ### Manually Into Ubuntu, minimal dependency: diff --git a/README.fr.md b/README.fr.md index e942d48..0bdb9d3 100644 --- a/README.fr.md +++ b/README.fr.md @@ -126,6 +126,19 @@ make ``` +`make` et `./install.sh` lancent TODO par l'interpréteur capable de le lire : +`.venv.erplibre` quand il porte la bonne version, sinon le `python3` du système +s'il est assez récent, sinon l'installation elle-même — un système plus ancien +que `conf/python-erplibre-version` ne sait pas analyser le code, donc on va +droit à l'installation plutôt que de s'arrêter sur une erreur de syntaxe. Quand +l'environnement manque mais que le Python du système suffit, TODO propose de +lancer `./script/install/install_erplibre.sh` (dans un terminal) ou affiche +cette commande. L'installation bâtit l'environnement par +`EL_PYTHON_PROVIDER` (mise ou pyenv) ; un `.venv.erplibre` existant sur une +autre version de Python est SUPPRIMÉ puis rebâti, et ce qu'on y avait posé à la +main part avec lui. + + ### Manuellement Sous Ubuntu, dépendance minimale : diff --git a/README.md b/README.md index 2a7232d..1b63d4b 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,18 @@ make ``` +`make` and `./install.sh` start TODO through the interpreter that can read it: +`.venv.erplibre` when it carries the right version, otherwise the system +`python3` when it is recent enough, otherwise the install itself — a system +older than `conf/python-erplibre-version` cannot parse the code, so it goes +straight to the install rather than stopping on a syntax error. When the +environment is missing but the system Python suffices, TODO offers to run +`./script/install/install_erplibre.sh` (in a terminal) or prints that command. The install builds the environment +through `EL_PYTHON_PROVIDER` (mise or pyenv); an existing `.venv.erplibre` on +another Python version is DELETED and rebuilt, and whatever was installed in it +by hand goes with it. + + ### Manually Into Ubuntu, minimal dependency: diff --git a/conf/make.todo.Makefile b/conf/make.todo.Makefile index bd88161..5902887 100644 --- a/conf/make.todo.Makefile +++ b/conf/make.todo.Makefile @@ -2,6 +2,8 @@ # TODO # ######## +# Par install.sh, et non todo.py directement : lui seul choisit un interpréteur +# capable de LIRE le code avant de le lancer, et pose le venv s'il manque. .PHONY: todo todo: - ./script/todo/todo.py + ./install.sh diff --git a/install.sh b/install.sh index ff04bef..f98789a 100755 --- a/install.sh +++ b/install.sh @@ -1,3 +1,69 @@ #!/usr/bin/env bash +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +# +# Point d'entrée de TODO : choisir un interpréteur capable de LIRE le code +# avant de le lui donner. +# +# todo.py porte le hashbang « python3 », donc le Python du système. Celui-ci +# est libre d'être plus vieux que conf/python-erplibre-version, qui est la +# seule version que le dépôt vise : une syntaxe qu'il ne connaît pas l'arrête +# en SyntaxError au chargement, AVANT le garde de todo.py qui aurait su nommer +# le geste. Un système hors d'âge n'a alors plus aucun moyen de lancer +# l'installation qui le tirerait de là. +# +# D'où l'ordre : le venv d'outillage, qui porte la bonne version ; sinon le +# Python du système s'il est assez récent pour lire le code, et le garde de +# todo.py prend le relais ; sinon l'installation, seule issue. -./script/todo/todo.py +cd "$(dirname "$0")" || exit 1 + +VENV="$(xargs < conf/python-erplibre-venv 2> /dev/null)" +VOULUE="$(xargs < conf/python-erplibre-version 2> /dev/null)" +ATTENDUE="${VOULUE%.*}" + +# Majeure.mineure de l'exécutable : elle seule décide de la grammaire acceptée. +# Rien n'est rendu quand la sortie n'a pas la forme « Python X.Y… » : un +# interpréteur en panne écrit son diagnostic, et le premier mot venu passerait +# sinon pour un numéro de version que « sort -V » jugerait assez récent. +el_mineure() { + "$1" -V 2>&1 | awk '$1 == "Python" { + split($2, v, ".") + if (v[1] ~ /^[0-9]+$/ && v[2] ~ /^[0-9]+$/) print v[1] "." v[2] + }' +} + +# Vrai si la version lue atteint au moins celle attendue, comparée en version +# et non en chaîne : « 3.9 » est une chaîne plus grande que « 3.14 ». +el_assez_recent() { + [ -n "$1" ] && [ -n "${ATTENDUE}" ] \ + && [ "$(printf '%s\n%s\n' "${ATTENDUE}" "$1" | sort -V | head -1)" \ + = "${ATTENDUE}" ] +} + +PYTHON_VENV="./${VENV}/bin/python" +if [ -n "${VENV}" ] && [ -x "${PYTHON_VENV}" ] \ + && el_assez_recent "$(el_mineure "${PYTHON_VENV}")"; then + exec "${PYTHON_VENV}" ./script/todo/todo.py "$@" +fi + +# Un venv dont l'interpréteur ne rend pas sa version ne démarrera pas +# davantage TODO : c'est le cas d'un checkout monté depuis une autre machine, +# dont le venv cherche sa bibliothèque standard là où elle n'est pas. Le dire, +# et couper la relance de todo.py, qui sinon se remplacerait par ce python mort +# et rendrait son pavé d'initialisation au lieu d'un message. +if [ -x "${PYTHON_VENV}" ] && [ -z "$(el_mineure "${PYTHON_VENV}")" ]; then + echo "${PYTHON_VENV} ne demarre pas : ce venv a ete bati ailleurs." + echo " Rebatissez-le ici : ./script/install/install_erplibre.sh" + export EL_TODO_VENV_RELAUNCHED=1 +fi + +if command -v python3 > /dev/null 2>&1 \ + && el_assez_recent "$(el_mineure python3)"; then + exec ./script/todo/todo.py "$@" +fi + +echo "Python ${VOULUE:-du depot} est requis pour lire le code de TODO ;" +echo " ce systeme livre $(el_mineure python3 2> /dev/null || echo 'aucun python3')." +echo " Installation du venv d'outillage, qui le pose :" +exec ./script/todo/source_todo.sh "$@" diff --git a/script/todo/todo.py b/script/todo/todo.py index fde32b5..a9a872c 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -5,6 +5,7 @@ import ast import configparser import datetime +import importlib.util import inspect import json import logging @@ -24,6 +25,98 @@ new_path = os.path.normpath( ) sys.path.append(new_path) + +# Garde d'amorçage, joué seulement quand todo.py est le programme lancé : il +# relance dans le venv d'outillage, ou refuse avec le geste qui le bâtit, au +# lieu d'une trace brute à l'import. Il parse sous 3.7 pour parler à un vieux +# python. Messages en anglais hors t() : todo_i18n n'est pas encore importé. +RELAUNCH_ENV = "EL_TODO_VENV_RELAUNCHED" +# Modules tiers que charge l'import de ce fichier et de ses mixins : un seul +# absent fait lever l'import, ou boucler crash_diagnostic. Ceux qu'un menu +# n'importe qu'à l'usage n'y figurent pas, ils ne bloquent pas le démarrage. +REQUIRED_MODULES = ( + "click colorama dotenv humanize openai pykeepass urwid".split() +) +INSTALL_CMD = "./script/install/install_erplibre.sh" + + +def _read_conf(name, fallback): + """Rend la première ligne non vide et non commentée de conf/.""" + try: + with open(os.path.join(new_path, "conf", name), encoding="utf-8") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + return line + except (OSError, UnicodeDecodeError): + pass + sys.stderr.write("conf/%s unreadable, assuming %s\n" % (name, fallback)) + return fallback + + +VENV_ERPLIBRE = _read_conf("python-erplibre-venv", ".venv.erplibre") +VENV_DIR = os.path.join(new_path, VENV_ERPLIBRE) +VENV_PYTHON = os.path.join(VENV_DIR, "bin", "python") + + +def _in_venv(): + # sys.prefix et non le binaire : /bin/python est un lien vers sa base. + return os.path.realpath(sys.prefix) == os.path.realpath(VENV_DIR) + + +def _relaunch(marker): + """Remplace le processus par le python du venv ; OSError si impossible.""" + env = dict(os.environ) + env[RELAUNCH_ENV] = marker + argv = [VENV_PYTHON, os.path.abspath(__file__)] + sys.argv[1:] + os.execve(VENV_PYTHON, argv, env) + + +def _bootstrap(): + # La relance passe avant la version : un python trop vieux avec un venv + # sain doit relancer, pas refuser. + relaunched = os.environ.get(RELAUNCH_ENV) + if not relaunched and not _in_venv() and os.access(VENV_PYTHON, os.X_OK): + try: + _relaunch("1") + except OSError: + pass + voulue = _read_conf("python-erplibre-version", "3.14") + missing = [m for m in REQUIRED_MODULES if not importlib.util.find_spec(m)] + if sys.version_info[:2] >= tuple(map(int, voulue.split(".")[:2])): + if not missing: + os.environ.pop(RELAUNCH_ENV, None) + return + reason = "missing modules: %s" % ", ".join(missing) + else: + reason = "Python %s is older than %s" % ( + sys.version.split()[0], + voulue, + ) + print("TODO cannot start from %s: %s." % (sys.executable, reason)) + print("Install the tools virtualenv %s with:" % VENV_ERPLIBRE) + print("\n %s\n" % INSTALL_CMD) + # Amorçage de « make » sur un clone neuf : proposer l'installation, une fois. + if relaunched != "installed" and os.isatty(0) and os.isatty(1): + try: + answer = input("Run it now? [y/o/N] ") + except (EOFError, KeyboardInterrupt): + answer = "" + if answer.strip().lower() in ("y", "yes", "o", "oui"): + if subprocess.call([INSTALL_CMD], cwd=new_path) != 0: + print("Installation failed, see above.") + else: + try: + _relaunch("installed") + except OSError: + print("Installed, but %s does not start." % VENV_PYTHON) + sys.exit(1) + + +if __name__ == "__main__": + _bootstrap() + + from script.config import config_file from script.execute import execute from script.todo import dev_tools, ssh_config, todo_install, todo_prefs @@ -5601,6 +5694,8 @@ class TODO( from pykeepass import PyKeePass except ImportError: print("Rerun and exit") + if _in_venv(): # la relance tournerait dans ce même venv + sys.exit(1) self.execute.exec_command_live(cmd, source_erplibre=True) sys.exit(1) print("No error")