diff --git a/.claude/rules/08-deployment.md b/.claude/rules/08-deployment.md index 354d4ce..a928e1a 100644 --- a/.claude/rules/08-deployment.md +++ b/.claude/rules/08-deployment.md @@ -24,3 +24,16 @@ 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. diff --git a/Makefile b/Makefile index 59b417e..6b7e893 100644 --- a/Makefile +++ b/Makefile @@ -105,6 +105,25 @@ pyenv_update: # # 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 \ diff --git a/env_var.sh b/env_var.sh index d338d3e..179fafc 100755 --- a/env_var.sh +++ b/env_var.sh @@ -19,6 +19,12 @@ EL_ERPLIBRE_VERSION=$(cat ".erplibre-version" | xargs) # 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" diff --git a/script/install/install_locally.sh b/script/install/install_locally.sh index 38609e4..fe71f79 100755 --- a/script/install/install_locally.sh +++ b/script/install/install_locally.sh @@ -19,15 +19,27 @@ 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 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 @@ -72,7 +84,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 @@ -121,28 +134,50 @@ if [[ "${EL_PHASE}" != "setup" ]]; then esac fi - # Delete artifacts created by pip, cause error in next "poetry install" - if [[ ! -f "${POETRY_ODOO_PATH}" ]]; then + # 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 ${PIP_CONSTRAINT_CRYPTO} 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 + "${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 diff --git a/script/install/lib_pip_provider.sh b/script/install/lib_pip_provider.sh new file mode 100755 index 0000000..c4c3b39 --- /dev/null +++ b/script/install/lib_pip_provider.sh @@ -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 "$@" +}