erplibre/script/install/lib_python_provider.sh
Mathieu Benoit 01c5290e30 [FIX] qemu install : poser mise, pyenv et starship, verdict par outil
Without pipefail, « curl … | sh » returns the status of sh, 0 on empty input:
a failed download passed for an install, its fallback never ran, and the
failure surfaced later as « command not found ». mise and pyenv are downloaded
into a mktemp file, then run, and a check refuses any fallback behind a pipe
without pipefail. starship's installer runs as root under a root timeout, never
reaching the « sudo -v » that sudo-rs refuses, and its shell hook is guarded.
Each tool then reports the version found or « not installed », without
tripping « set -e ».

--- FR ---

Sans pipefail, « curl … | sh » rend le statut de sh, 0 sur une entrée vide :
un téléchargement raté passait pour une pose, son repli ne tournait jamais, et
l'échec se lisait plus loin sous « command not found ». mise et pyenv sont
téléchargés dans un fichier tiré par mktemp, puis exécutés, et un contrôle
refuse tout repli derrière un tube sans pipefail. L'installateur de starship
tourne en root sous un délai root, sans atteindre le « sudo -v » que sudo-rs
refuse, et son crochet de shell est gardé. Chaque outil dit ensuite la version
trouvée ou « non installé », sans faire tomber « set -e ».

Assisted-by: Claude Opus 5
2026-09-14 16:46:15 -04:00

219 lines
9.2 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 : même majeure.mineure, et patch au moins
# égal au demandé. C'est exactement ce qu'exige le pyproject — « >=3.12.10,
# <3.13 » — et non l'égalité stricte que testait el_python_is_version.
#
# La distinction n'est pas théorique. Tumbleweed s390x livre python312 en
# 3.12.13 : parfaitement utilisable, mais rejeté par l'égalité, ce qui forçait
# pyenv à COMPILER CPython — et gcc 15.2 s'y arrête sur une erreur interne
# dans Parser/parser.c, un fichier généré de quarante mille lignes.
el_python_is_compatible() {
local exe="$1" want="$2" got
[ -x "${exe}" ] || return 1
got="$("${exe}" -c 'import platform;print(platform.python_version())' \
2> /dev/null)"
[ -n "${got}" ] || return 1
# Même majeure.mineure : 3.13 ne convient pas à un pyproject borné à <3.13.
[ "${got%.*}" = "${want%.*}" ] || return 1
# Patch au moins égal, comparé en version et non en chaîne (3.12.9 < 3.12.10).
[ "$(printf '%s\n%s\n' "${want}" "${got}" | sort -V | head -1)" = "${want}" ]
}
# Interpréteur de la DISTRIBUTION qui conviendrait, s'il y en a un.
#
# Cherché avant toute compilation : sur une architecture émulée, bâtir CPython
# prend des dizaines de minutes quand il aboutit. Le nom suit la convention de
# toutes les distributions, « python3.12 ».
el_distro_python_exec() {
local want="$1" exe
# Des chemins SYSTEME, jamais « command -v » : dans un venv activé celui-ci
# rend le python DU VENV, et l'on bâtirait un venv depuis un venv. Le PATH
# d'une session interactive n'a rien à faire dans cette décision.
for exe in "/usr/bin/python${want%.*}" "/usr/local/bin/python${want%.*}"; do
if el_python_is_compatible "${exe}" "${want}"; then
echo "${exe}"
return 0
fi
done
return 1
}
# Installe la version via mise. Renvoie le chemin, ou échoue.
el_mise_install() {
local version="$1" exe
command -v mise > /dev/null 2>&1 || return 1
echo "---- Python ${version} via mise (precompile) ----" >&2
mise install "python@${version}" >&2 || return 1
exe="$(el_mise_exec_path "${version}")" || return 1
el_python_is_version "${exe}" "${version}" || return 1
echo "${exe}"
}
# Installe la version via pyenv, en posant pyenv lui-même au besoin.
el_pyenv_install() {
local version="$1" root exe 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}"
}
# API publique : imprime le chemin absolu d'un interpréteur de cette version,
# ou rien (et rend non nul) si aucun fournisseur n'y parvient.
el_python_exec() {
local version="$1" provider="${EL_PYTHON_PROVIDER:-auto}" exe
# 1) Déjà présent ? On ne réinstalle rien et on ne touche pas au réseau.
# C'est ce qui rend le changement sans effet pour une installation
# pyenv existante.
if [ "${provider}" != "mise" ]; then
exe="$(el_pyenv_exec_path "${version}")"
if el_python_is_version "${exe}" "${version}"; then
echo "${exe}"
return 0
fi
fi
if [ "${provider}" != "pyenv" ]; then
if exe="$(el_mise_exec_path "${version}")" \
&& el_python_is_version "${exe}" "${version}"; then
echo "${exe}"
return 0
fi
fi
# 2) Un interpréteur de la DISTRIBUTION qui convient ? Rien à installer,
# rien à compiler. Vérifié avant mise et pyenv : c'est le seul chemin
# qui ne coûte rien, et sur s390x le seul qui aboutisse partout — mise
# n'y publie aucun binaire, et pyenv doit compiler.
# Uniquement en mode « auto » : demander mise ou pyenv explicitement doit
# être respecté, sinon le réglage ne veut plus rien dire.
if [ "${provider}" = "auto" ] \
&& exe="$(el_distro_python_exec "${version}")"; then
echo "Python $("${exe}" -V 2>&1 | awk '{print $2}') de la distribution :" \
"aucune compilation." >&2
echo "${exe}"
return 0
fi
# 3) Rien de posé : il faut provisionner. mise d'abord quand il est là,
# parce qu'il télécharge au lieu de compiler.
if [ "${provider}" != "pyenv" ]; then
if exe="$(el_mise_install "${version}")"; then
echo "${exe}"
return 0
fi
[ "${provider}" = "mise" ] && return 1
command -v mise > /dev/null 2>&1 \
&& echo "mise n'a pas pu fournir Python ${version} : repli sur pyenv." >&2
fi
el_pyenv_install "${version}"
}