erplibre/script/install/lib_python_provider.sh
Mathieu Benoit 42e10bcced [UPD] install : le venv d'outillage passe en Python 3.14.7
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
2026-09-24 13:56:57 -04:00

254 lines
11 KiB
Bash
Executable file

#!/usr/bin/env bash
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
#
# Bibliothèque SOURÇABLE : « une version de Python » -> « un interpréteur ».
#
# C'est le seul endroit du dépôt qui décide COMMENT un interpréteur est
# obtenu. Tout le reste ne consomme que des chemins de venv, jamais pyenv ni
# mise. Ajouter un fournisseur se fait donc ici, et nulle part ailleurs.
#
# Deux fournisseurs :
# pyenv historique. Compile CPython depuis les sources : 1 à 3 min sur une
# machine récente, bien plus sous émulation, et il lui faut une
# douzaine de -dev (openssl, zlib, readline, sqlite, bzip2, xz, tk…).
# mise pose un CPython PRÉCOMPILÉ (astral python-build-standalone) :
# quelques secondes, aucun compilateur requis.
#
# EL_PYTHON_PROVIDER choisit : auto (défaut), mise, ou pyenv.
#
# En mode « auto », un interpréteur DÉJÀ présent gagne, quel que soit le
# fournisseur qui l'a posé. Une installation pyenv qui marche n'est donc
# jamais doublée par un second CPython de 150 Mo.
# Ne jamais laisser mise compiler en silence : sans build précompilée pour la
# plateforme, il retomberait sur python-build — le moteur de pyenv — et on
# aurait la lenteur de pyenv sans son outillage. « false » = télécharger ou
# échouer ; l'échec nous fait basculer proprement sur le repli.
export MISE_PYTHON_COMPILE=false
el_pyenv_root() {
echo "${PYENV_ROOT:-${HOME}/.pyenv}"
}
# Chemin de l'interpréteur pyenv d'une version, qu'il existe ou non.
el_pyenv_exec_path() {
echo "$(el_pyenv_root)/versions/$1/bin/python"
}
# Interpréteur déjà posé par mise, sans le moindre accès réseau.
el_mise_exec_path() {
command -v mise > /dev/null 2>&1 || return 1
local out
out="$(mise where "python@$1" 2> /dev/null)" || return 1
[ -n "${out}" ] && [ -x "${out}/bin/python" ] || return 1
echo "${out}/bin/python"
}
# Vrai si l'exécutable rend EXACTEMENT la version demandée. Un interpréteur
# présent mais d'une autre version casserait Poetry, dont le pyproject exige
# le patch au près (>=3.12.10,<3.13).
el_python_is_version() {
local exe="$1" want="$2" got
[ -x "${exe}" ] || return 1
got="$("${exe}" -c 'import platform;print(platform.python_version())' 2> /dev/null)"
[ "${got}" = "${want}" ]
}
# Vrai si l'exécutable CONVIENT. Deux exigences, selon ce qui borne l'appelant.
#
# 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" 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}" ]
}
# Interpréteur de la DISTRIBUTION qui conviendrait, s'il y en a un.
#
# Cherché avant toute compilation : sur une architecture émulée, bâtir CPython
# prend des dizaines de minutes quand il aboutit. Le nom suit la convention de
# toutes les distributions, « python3.12 ».
el_distro_python_exec() {
local want="$1" 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.
# Le profil du systeme AVANT /usr/bin, et c'est NixOS qui l'impose : la-bas
# /usr/bin est servi par envfs, qui repond a open() et a execve() mais PAS a
# stat(). Le test « -x » y reussit donc, l'interpreteur s'execute, et
# « python -m venv » echoue quand meme -- il stat son propre interpreteur
# pour le recopier : « Error: [Errno 2] No such file or directory ». Le
# chemin du profil, lui, est un vrai lien symbolique. Ailleurs il n'existe
# 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}" "${exigence}"; then
echo "${exe}"
return 0
fi
done
return 1
}
# Installe la version via mise. Renvoie le chemin, ou échoue.
el_mise_install() {
local version="$1" exe
command -v mise > /dev/null 2>&1 || return 1
echo "---- Python ${version} via mise (precompile) ----" >&2
mise install "python@${version}" >&2 || return 1
exe="$(el_mise_exec_path "${version}")" || return 1
el_python_is_version "${exe}" "${version}" || return 1
echo "${exe}"
}
# Installe la version via pyenv, en posant pyenv lui-même au besoin.
el_pyenv_install() {
local version="$1" root exe installer
root="$(el_pyenv_root)"
exe="$(el_pyenv_exec_path "${version}")"
if [[ ! -d "${root}" ]]; then
echo "---- Installation de pyenv dans ${root} ----" >&2
# Téléchargé dans un fichier, PUIS exécuté. Quand curl alimente bash par
# un tube, le statut du tube est celui de bash, qui rend 0 sur une entrée
# vide : sans pipefail — que rien ne pose dans cette chaîne —, un
# « || return 1 » placé après le tube ne se déclencherait jamais. Un
# téléchargement raté mènerait alors à « pyenv: command not found », puis
# à un échec de compilation, deux messages qui accusent la mauvaise
# étape. Lu seul, le statut de curl nomme la vraie cause.
#
# « -f » : sans lui, curl écrit le CORPS d'une erreur HTTP — page d'erreur
# de miroir, portail captif, 504 d'un cache hors ligne —, que bash
# exécuterait ensuite comme un script.
installer="$(mktemp)" || return 1
if ! curl -fsSL -o "${installer}" \
https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer; then
echo "Telechargement de l'installateur pyenv impossible" \
"(reseau ou cache) : pyenv n'est pas pose." >&2
rm -f "${installer}"
return 1
fi
if ! bash "${installer}" >&2; then
echo "L'installateur de pyenv a echoue (voir ci-dessus)." >&2
rm -f "${installer}"
return 1
fi
rm -f "${installer}"
fi
export PATH="${root}/bin:$PATH"
eval "$(pyenv init - 2> /dev/null)" || true
eval "$(pyenv virtualenv-init - 2> /dev/null)" || true
if [[ ! -d "${root}/versions/${version}" ]]; then
# pyenv COMPILE CPython : sans compilateur C, il télécharge l'archive,
# lance configure et s'arrête sur « no acceptable C compiler found », après
# avoir gaspillé le téléchargement. On le dit AVANT, et on nomme le paquet.
if ! command -v cc > /dev/null 2>&1 \
&& ! command -v gcc > /dev/null 2>&1; then
echo "Aucun compilateur C : pyenv ne peut pas compiler Python." >&2
echo " Installez le necessaire de compilation (build-essential," >&2
echo " gcc/gcc-c++, ou le motif devel_basis) puis relancez." >&2
return 1
fi
echo "---- Python ${version} via pyenv (compilation) ----" >&2
# La liste des versions connues vient du dépôt git de pyenv : sans ce
# « pull », une version récente est « not a known version ».
(cd "${root}" && git pull) >&2 || true
# Le contrôle d'erreur d'origine testait un $retVal jamais affecté ici :
# un échec de compilation passait inaperçu jusqu'au test d'existence.
if ! yes n | pyenv install "${version}" >&2; then
echo "pyenv install ${version} a echoue." >&2
return 1
fi
fi
el_python_is_version "${exe}" "${version}" || return 1
echo "${exe}"
}
# 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" 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
# pyenv existante.
if [ "${provider}" != "mise" ]; then
exe="$(el_pyenv_exec_path "${version}")"
if el_python_is_version "${exe}" "${version}"; then
echo "${exe}"
return 0
fi
fi
if [ "${provider}" != "pyenv" ]; then
if exe="$(el_mise_exec_path "${version}")" \
&& el_python_is_version "${exe}" "${version}"; then
echo "${exe}"
return 0
fi
fi
# 2) Un interpréteur de la DISTRIBUTION qui convient ? Rien à installer,
# rien à compiler. Vérifié avant mise et pyenv : c'est le seul chemin
# qui ne coûte rien, et sur s390x le seul qui aboutisse partout — mise
# n'y publie aucun binaire, et pyenv doit compiler.
# Uniquement en mode « auto » : demander mise ou pyenv explicitement doit
# être respecté, sinon le réglage ne veut plus rien dire.
if [ "${provider}" = "auto" ] \
&& exe="$(el_distro_python_exec "${version}" "${exigence}")"; then
echo "Python $("${exe}" -V 2>&1 | awk '{print $2}') de la distribution :" \
"aucune compilation." >&2
echo "${exe}"
return 0
fi
# 3) Rien de posé : il faut provisionner. mise d'abord quand il est là,
# parce qu'il télécharge au lieu de compiler.
if [ "${provider}" != "pyenv" ]; then
if exe="$(el_mise_install "${version}")"; then
echo "${exe}"
return 0
fi
[ "${provider}" = "mise" ] && return 1
el_warn_pyenv_fallback "${version}"
fi
el_pyenv_install "${version}"
}