Compare commits

..

No commits in common. "5a408f10612dafa24437ff9a1342a85864250143" and "2b7e20493a9e707ebabe2b9cdd30ef26bca423d1" have entirely different histories.

17 changed files with 194 additions and 2221 deletions

View file

@ -33,7 +33,3 @@ EL_GITHUB_TOKEN=""
EL_MANIFEST_PROD="./default.xml" EL_MANIFEST_PROD="./default.xml"
EL_MANIFEST_DEV="./manifest/git_manifest_erplibre.xml" EL_MANIFEST_DEV="./manifest/git_manifest_erplibre.xml"
EL_LANG="fr" EL_LANG="fr"
# Verbosité de l'installation (poetry install, repo sync, git daemon).
# 0 = silencieux (défaut), 1 = logs détaillés. On respecte une valeur déjà
# passée en environnement : EL_VERBOSE=1 make install_odoo_18
EL_VERBOSE="${EL_VERBOSE:-0}"

View file

@ -24,7 +24,6 @@ openai
humanize humanize
requests requests
urwid urwid
textual
python-dotenv python-dotenv
python-dateutil python-dateutil
unidecode unidecode

View file

@ -1,126 +1,68 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
#
# Dépendances système ERPLibre pour Arch Linux (pacman). Réécrit pour les
# images cloud Arch : l'ancien script utilisait « yay » (helper AUR ABSENT
# d'une image cloud et qui refuse de tourner en root) et n'installait jamais
# « base-devel » -> pas de compilateur C -> échec de la compilation de Python
# par pyenv. On passe tout par pacman (dépôts officiels) ; l'AUR (wkhtmltopdf)
# est best-effort et ne bloque pas.
. ./env_var.sh install_package() {
local package_name=$1
EL_USER=${USER} # Check package is already installed
if pacman -Qs "$package_name" >/dev/null; then
# pacman résilient : --needed saute ce qui est déjà là, --noconfirm en non echo "$package_name is already installed."
# interactif. else
PAC="sudo pacman -S --needed --noconfirm" echo "Installation of package $package_name..."
# Mise à jour COMPLÈTE obligatoire : Arch est en rolling release et NE SUPPORTE yes | yay -S "$package_name"
# PAS les mises à jour partielles. Sur une image cloud dont la glibc est
# ancienne, un « pacman -S <paquet récent> » lie le binaire à une glibc plus
# récente que celle installée -> « /usr/bin/postgres: GLIBC_2.44 not found ».
# « -Syu » met à jour glibc (et tout le système) pour rester cohérent.
sudo pacman -Syu --noconfirm || true
#--------------------------------------------------
# Outils de compilation — CRITIQUE (build Python via pyenv, extensions Python)
# base-devel fournit gcc, make, patch, pkgconf, fakeroot, etc.
#--------------------------------------------------
echo -e "\n---- Groupe base-devel (compilateur C, make…) ----"
${PAC} base-devel
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "pacman base-devel installation error."
exit 1
fi fi
}
#-------------------------------------------------- # Odoo installation
# Dépendances de build pyenv (compilation de CPython) + outils de base install_package postgis
#-------------------------------------------------- install_package postgresql
echo -e "\n---- Dépendances pyenv (compilation Python) + outils ----" install_package mariadb
${PAC} git wget curl openssl zlib xz tk bzip2 readline sqlite libffi install_package libev
retVal=$? install_package wkhtmltopdf
if [[ $retVal -ne 0 ]]; then install_package freetds
echo "pacman pyenv dependencies installation error."
exit 1
fi
#-------------------------------------------------- echo "Need password to create symbolic link, create postgres user and install npm :"
# PostgreSQL (+ PostGIS) — Arch n'initialise pas le cluster automatiquement sudo ln -fs /usr/lib/libldap.so /usr/lib/libldap_r.so
#--------------------------------------------------
echo -e "\n---- Install PostgreSQL Server ----"
${PAC} postgresql
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "pacman postgresql installation error."
exit 1
fi
# Initialisation du cluster (chemin Arch : /var/lib/postgres/data).
if [ ! -f /var/lib/postgres/data/PG_VERSION ]; then
echo -e "\n---- Initialisation du cluster PostgreSQL ----"
sudo -u postgres initdb --locale=C.UTF-8 --encoding=UTF8 \
-D /var/lib/postgres/data || true
fi
sudo systemctl enable --now postgresql 2>/dev/null || true
# PostGIS : optionnel (géospatial), ne bloque pas.
${PAC} postgis || echo "PostGIS non installé (optionnel)."
echo -e "\n---- Creating the ERPLibre PostgreSQL User ----"
sudo su - postgres -c "createuser -s ${EL_USER}" 2>/dev/null || true sudo su - postgres -c "createuser -s ${EL_USER}" 2>/dev/null || true
#-------------------------------------------------- echo -e "\n---- Update NPM ----"
# Dépendances Odoo / ERPLibre (extensions Python, bindings) install_package npm
#--------------------------------------------------
echo -e "\n---- Installing arch dependency ----"
# best-effort paquet par paquet : pacman refuse TOUTE la transaction si UN
# seul nom est inconnu (pas de --skip-unavailable). Ces deps ne sont pas
# critiques pour le build Python -> on ne bloque pas sur un nom absent.
for _pkg in libxslt libzip libldap libsasl cmake parallel swig portaudio \
cups xmlsec freetds libev mariadb-libs shfmt; do
${PAC} "${_pkg}" || echo " ${_pkg} : non installé (optionnel), on continue."
done
# libldap_r : Odoo/python-ldap cherche parfois libldap_r.so (supprimé des
# versions récentes d'OpenLDAP) -> lien vers libldap.so.
sudo ln -fs /usr/lib/libldap.so /usr/lib/libldap_r.so 2>/dev/null || true
#-------------------------------------------------- sudo npm install npm@latest -g
# Node.js + npm (rtlcss, less)
#--------------------------------------------------
echo -e "\n---- Installing nodeJS NPM and rtlcss ----"
${PAC} nodejs npm
retVal=$? retVal=$?
if [[ $retVal -ne 0 ]]; then if [[ $retVal -ne 0 ]]; then
echo "pacman nodejs installation error." echo "npm install npm lastest installation error."
exit 1
fi
sudo npm install -g rtlcss
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "npm install rtlcss installation error."
exit 1
fi
sudo npm install -g less
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "npm install less installation error."
exit 1 exit 1
fi fi
sudo npm install -g rtlcss less || echo "npm rtlcss/less: erreur (optionnel)."
echo -e "\n---- Test tool ----" echo -e "\n---- Test tool ----"
npm install || echo "npm install (prettier/plugin-xml): erreur (optionnel)." npm install
retVal=$?
# Pour erplibre_devops. if [[ $retVal -ne 0 ]]; then
${PAC} sshpass || echo "sshpass non installé (optionnel)." echo "npm install prettier + plugin-xml installation error."
exit 1
#--------------------------------------------------
# nginx (optionnel)
#--------------------------------------------------
if [ "${EL_INSTALL_NGINX}" = "True" ]; then
echo -e "\n---- Installing nginx ----"
${PAC} nginx || echo "nginx: erreur (optionnel)."
fi fi
#-------------------------------------------------- # TODO install nginx
# wkhtmltopdf (optionnel) — uniquement dans l'AUR sur Arch (pas de paquet
# officiel). Sur une image cloud sans helper AUR, on saute proprement. # ERPLibre installation
#-------------------------------------------------- install_package cmake
if [ "${EL_INSTALL_WKHTMLTOPDF}" = "True" ]; then install_package parallel
if ! command -v wkhtmltopdf >/dev/null 2>&1; then install_package tk
echo "wkhtmltopdf : disponible seulement via l'AUR sur Arch, ignoré" install_package shfmt
echo " (installez-le manuellement avec un helper AUR si nécessaire)."
else # For erplibre_devops
echo -e "\n---- Already installed wkhtml ----" install_package sshpass
fi
fi
echo -e "\n---- Arch Linux dependency installation done ----"

View file

@ -5,11 +5,6 @@
EL_USER=${USER} EL_USER=${USER}
#EL_INSTALL_WKHTMLTOPDF="True" #EL_INSTALL_WKHTMLTOPDF="True"
# apt-get qui ATTEND le verrou (jusqu'à 10 min) : sur une image cloud fraîche,
# cloud-init / unattended-upgrades tiennent souvent le verrou apt au 1er boot
# (« Could not get lock » -> échec de l'install). DPkg::Lock::Timeout patiente.
APT_GET="sudo apt-get -o DPkg::Lock::Timeout=600"
## ##
### WKHTMLTOPDF download links ### WKHTMLTOPDF download links
## === Ubuntu Focal x64 === (for other distributions please replace these two links, ## === Ubuntu Focal x64 === (for other distributions please replace these two links,
@ -60,30 +55,25 @@ echo -e "\n---- Update Server ----"
if [ "18.04" == "${UBUNTU_VERSION}" ]; then if [ "18.04" == "${UBUNTU_VERSION}" ]; then
# add-apt-repository can install add-apt-repository Ubuntu 18.x # add-apt-repository can install add-apt-repository Ubuntu 18.x
${APT_GET} install software-properties-common curl -y sudo apt-get install software-properties-common curl -y
# universe package is for Ubuntu 18.x # universe package is for Ubuntu 18.x
sudo add-apt-repository universe sudo add-apt-repository universe
# libpng12-0 dependency for wkhtmltopdf # libpng12-0 dependency for wkhtmltopdf
sudo add-apt-repository "deb http://mirrors.kernel.org/ubuntu/ xenial main" sudo add-apt-repository "deb http://mirrors.kernel.org/ubuntu/ xenial main"
${APT_GET} update sudo apt-get update
${APT_GET} upgrade -y sudo apt-get upgrade -y
fi fi
#-------------------------------------------------- #--------------------------------------------------
# Install PostgreSQL Server # Install PostgreSQL Server
#-------------------------------------------------- #--------------------------------------------------
echo -e "\n---- Install PostgreSQL Server ----" echo -e "\n---- Install PostgreSQL Server ----"
${APT_GET} install postgresql postgresql-contrib libpq-dev -y sudo apt-get install postgresql libpq-dev postgis -y
retVal=$? retVal=$?
if [[ $retVal -ne 0 ]]; then if [[ $retVal -ne 0 ]]; then
echo "apt-get install postgresql installation error." echo "apt-get install postgresql installation error."
exit 1 exit 1
fi fi
# PostGIS : optionnel (géospatial). Le nom du paquet « postgis » n'existe pas
# sur toutes les versions (Ubuntu 24.04) -> best-effort, ne bloque pas.
${APT_GET} install postgis -y \
|| ${APT_GET} install postgresql-postgis -y \
|| echo "PostGIS non installé (optionnel)."
echo -e "\n---- Creating the ERPLibre PostgreSQL User ----" echo -e "\n---- Creating the ERPLibre PostgreSQL User ----"
sudo su - postgres -c "createuser -s ${EL_USER}" 2>/dev/null || true sudo su - postgres -c "createuser -s ${EL_USER}" 2>/dev/null || true
@ -92,20 +82,20 @@ sudo su - postgres -c "createuser -s ${EL_USER}" 2>/dev/null || true
# Install Dependencies # Install Dependencies
#-------------------------------------------------- #--------------------------------------------------
echo -e "\n--- Installing debian dependency --" echo -e "\n--- Installing debian dependency --"
${APT_GET} install git build-essential wget libxslt-dev libzip-dev libldap2-dev libsasl2-dev gdebi-core libffi-dev libbz2-dev parallel pysassc swig cmake portaudio19-dev libcups2-dev shfmt xmlsec1 -y sudo apt-get install git build-essential wget libxslt-dev libzip-dev libldap2-dev libsasl2-dev gdebi-core libffi-dev libbz2-dev parallel pysassc swig cmake portaudio19-dev libcups2-dev shfmt xmlsec1 -y
retVal=$? retVal=$?
if [[ $retVal -ne 0 ]]; then if [[ $retVal -ne 0 ]]; then
echo "apt-get debian tool installation error." echo "apt-get debian tool installation error."
exit 1 exit 1
fi fi
${APT_GET} install libmariadbd-dev freetds-dev -y sudo apt-get install libmariadbd-dev freetds-dev -y
retVal=$? retVal=$?
if [[ $retVal -ne 0 ]]; then if [[ $retVal -ne 0 ]]; then
echo "apt-get libmariadb installation error." echo "apt-get libmariadb installation error."
exit 1 exit 1
fi fi
if [ "18.04" == "${UBUNTU_VERSION}" ]; then if [ "18.04" == "${UBUNTU_VERSION}" ]; then
${APT_GET} install libpng12-0 -y sudo apt-get install libpng12-0 -y
retVal=$? retVal=$?
if [[ $retVal -ne 0 ]]; then if [[ $retVal -ne 0 ]]; then
echo "apt-get libpng installation error." echo "apt-get libpng installation error."
@ -113,14 +103,14 @@ if [ "18.04" == "${UBUNTU_VERSION}" ]; then
fi fi
fi fi
# Dependencies for pyenv # Dependencies for pyenv
${APT_GET} install make libssl-dev zlib1g-dev libreadline-dev libsqlite3-dev curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev liblzma-dev -y sudo apt-get install make libssl-dev zlib1g-dev libreadline-dev libsqlite3-dev curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev liblzma-dev -y
retVal=$? retVal=$?
if [[ $retVal -ne 0 ]]; then if [[ $retVal -ne 0 ]]; then
echo "apt-get pyenv dependencies installation error." echo "apt-get pyenv dependencies installation error."
exit 1 exit 1
fi fi
# Dependencies for selenium # Dependencies for selenium
${APT_GET} install libcairo2-dev python3-dev pkg-config libxt-dev libgirepository1.0-dev -y sudo apt-get install libcairo2-dev python3-dev pkg-config libxt-dev libgirepository1.0-dev -y
retVal=$? retVal=$?
if [[ $retVal -ne 0 ]]; then if [[ $retVal -ne 0 ]]; then
echo "apt-get selenium dependencies installation error." echo "apt-get selenium dependencies installation error."
@ -128,8 +118,8 @@ if [[ $retVal -ne 0 ]]; then
fi fi
echo -e "\n---- Installing nodeJS NPM and rtlcss for LTR support ----" echo -e "\n---- Installing nodeJS NPM and rtlcss for LTR support ----"
${APT_GET} update sudo apt-get update
${APT_GET} install -y ca-certificates curl gnupg sudo apt-get install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
@ -142,8 +132,8 @@ else
fi fi
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
${APT_GET} update sudo apt-get update
${APT_GET} install nodejs -y sudo apt-get install nodejs -y
sudo npm install npm@latest -g sudo npm install npm@latest -g
retVal=$? retVal=$?
@ -187,12 +177,7 @@ fi
#-------------------------------------------------- #--------------------------------------------------
# Install Wkhtmltopdf if needed # Install Wkhtmltopdf if needed
#-------------------------------------------------- #--------------------------------------------------
if [ "$(uname -m)" != "x86_64" ]; then if [ ${EL_INSTALL_WKHTMLTOPDF} = "True" ]; then
# WKHTMLTOX_X64 pointe vers un .deb amd64 : sur toute autre architecture
# (s390x, arm64/aarch64…) gdebi échouerait. On saute proprement plutôt que
# d'avorter tout l'install (wkhtmltopdf est optionnel).
echo "wkhtmltopdf : pas de build pour $(uname -m), ignoré (optionnel)."
elif [ ${EL_INSTALL_WKHTMLTOPDF} = "True" ]; then
echo -e "\n---- Installing wkhtml ----" echo -e "\n---- Installing wkhtml ----"
INSTALLED=$(dpkg -s wkhtmltox | grep installed) INSTALLED=$(dpkg -s wkhtmltox | grep installed)
if [ "" == "${INSTALLED}" ]; then if [ "" == "${INSTALLED}" ]; then

View file

@ -3,11 +3,11 @@
if [[ "${OSTYPE}" == "linux-gnu" ]]; then if [[ "${OSTYPE}" == "linux-gnu" ]]; then
source /etc/os-release source /etc/os-release
if [[ "${ID}" == "ubuntu" ]]; then if [[ "${ID}" == "ubuntu" ]]; then
if [[ "${VERSION_ID}" == "18.04" || "${VERSION_ID}" == "20.04" || "${VERSION_ID}" == "22.04" || "${VERSION_ID}" == "22.10" || "${VERSION_ID}" == "23.04" || "${VERSION_ID}" == "23.10" || "${VERSION_ID}" == "24.04" || "${VERSION_ID}" == "25.04" || "${VERSION_ID}" == "25.10" || "${VERSION_ID}" == "26.04" ]]; then if [[ "${VERSION_ID}" == "18.04" || "${VERSION_ID}" == "20.04" || "${VERSION_ID}" == "22.04" || "${VERSION_ID}" == "22.10" || "${VERSION_ID}" == "23.04" || "${VERSION_ID}" == "23.10" || "${VERSION_ID}" == "24.04" || "${VERSION_ID}" == "25.04" || "${VERSION_ID}" == "25.10" ]]; then
echo "\n---- linux-gnu installation process started ----" echo "\n---- linux-gnu installation process started ----"
./script/install/install_debian_dependency.sh ./script/install/install_debian_dependency.sh
else else
echo "Your version is not supported, only support 18.04, 20.04 and 22.04 - 24.04, 25.04, 25.10, 26.04 : ${VERSION_ID}" echo "Your version is not supported, only support 18.04, 20.04 and 22.04 - 24.04, 25.04, 25.10 : ${VERSION_ID}"
fi fi
elif [[ "${ID}" == "linuxmint" ]]; then elif [[ "${ID}" == "linuxmint" ]]; then
if [[ "${VERSION_ID}" == "22.3" ]]; then if [[ "${VERSION_ID}" == "22.3" ]]; then
@ -20,12 +20,9 @@ if [[ "${OSTYPE}" == "linux-gnu" ]]; then
./script/install/install_debian_dependency.sh ./script/install/install_debian_dependency.sh
elif [[ "${ID}" == "arch" ]]; then elif [[ "${ID}" == "arch" ]]; then
./script/install/install_arch_linux.sh ./script/install/install_arch_linux.sh
elif [[ "${ID}" == "fedora" || "${ID_LIKE}" == *"fedora"* || "${ID_LIKE}" == *"rhel"* ]]; then
echo "\n---- Fedora installation process started ----"
./script/install/install_fedora_dependency.sh
else else
./script/install/install_debian_dependency.sh ./script/install/install_debian_dependency.sh
echo "Your Linux system is not supported, only support Ubuntu 18.04 or Ubuntu 20.04 or Ubuntu 22.04 - Ubuntu 23.10 - Ubuntu 24.04, Ubuntu 25.04, Ubuntu 25.10, Debian, Fedora, Arch." echo "Your Linux system is not supported, only support Ubuntu 18.04 or Ubuntu 20.04 or Ubuntu 22.04 - Ubuntu 23.10 - Ubuntu 24.04, Ubuntu 25.04, Ubuntu 25.10 ."
fi fi
elif [[ "${OSTYPE}" == "darwin"* ]]; then elif [[ "${OSTYPE}" == "darwin"* ]]; then
echo "\n---- Darwin installation process started ----" echo "\n---- Darwin installation process started ----"

View file

@ -1,129 +0,0 @@
#!/usr/bin/env bash
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
#
# Dépendances système ERPLibre pour Fedora (dnf). Équivalent Fedora de
# install_debian_dependency.sh. « --skip-unavailable » tolère un nom de paquet
# absent (dnf5) au lieu d'échouer sur tout le lot.
. ./env_var.sh
EL_USER=${USER}
# dnf résilient : rafraîchit le cache (évite « checksum doesn't match » /
# signature après un cache périmé) et saute les paquets introuvables.
DNF="sudo dnf install -y --refresh --skip-unavailable"
#--------------------------------------------------
# Outils de compilation (build Python via pyenv, extensions Python)
#--------------------------------------------------
echo -e "\n---- Groupe outils de développement ----"
# Groupes par ID (le nom affiché « C Development Tools... » n'est pas matché).
sudo dnf group install -y --skip-unavailable development-tools c-development \
|| sudo dnf install -y gcc gcc-c++ make automake patch
#--------------------------------------------------
# PostgreSQL
#--------------------------------------------------
echo -e "\n---- Install PostgreSQL Server ----"
${DNF} postgresql-server postgresql-contrib libpq-devel
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "dnf install postgresql installation error."
exit 1
fi
# Initialisation du cluster (Fedora ne le fait pas automatiquement).
if [ ! -f /var/lib/pgsql/data/PG_VERSION ]; then
echo -e "\n---- Initialisation du cluster PostgreSQL ----"
# Nettoie un init partiel et FORCE une locale valide : les images cloud
# Fedora n'ont pas de LANG défini -> « initdb: invalid locale settings ».
sudo rm -rf /var/lib/pgsql/data
sudo PGSETUP_INITDB_OPTIONS="--locale=C.UTF-8 --encoding=UTF8" \
postgresql-setup --initdb || true
fi
sudo systemctl enable --now postgresql 2>/dev/null || true
# PostGIS : optionnel (géospatial), ne bloque pas.
${DNF} postgis || echo "PostGIS non installé (optionnel)."
echo -e "\n---- Creating the ERPLibre PostgreSQL User ----"
sudo su - postgres -c "createuser -s ${EL_USER}" 2>/dev/null || true
#--------------------------------------------------
# Dépendances de build (extensions Python, Odoo)
#--------------------------------------------------
echo -e "\n--- Installing fedora dependency --"
# git-daemon : sur Fedora la sous-commande « git daemon » N'EST PAS dans le
# paquet « git » de base (contrairement à Debian/Ubuntu/Arch). ERPLibre sert
# son manifeste via un « git daemon » local (git://127.0.0.1:9418/) pendant
# « repo sync » -> sans ce paquet : « git: 'daemon' is not a git command »
# puis « Connection refused » et l'échec de la synchro du manifeste.
${DNF} \
git git-daemon wget libxslt-devel libzip-devel openldap-devel \
cyrus-sasl-devel \
libffi-devel bzip2-devel parallel swig cmake portaudio-devel \
cups-devel xmlsec1 xmlsec1-openssl mariadb-connector-c-devel freetds-devel
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "dnf fedora tool installation error."
exit 1
fi
# Dépendances de build pour pyenv (compilation de CPython) — CRITIQUE.
echo -e "\n---- Dépendances pyenv (compilation Python) ----"
${DNF} \
make gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel \
openssl-devel tk-devel libffi-devel xz-devel patch findutils
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "dnf pyenv dependencies installation error."
exit 1
fi
# Dépendances selenium / bindings.
${DNF} \
cairo-devel python3-devel pkgconf-pkg-config gobject-introspection-devel \
libXt-devel || echo "Dépendances selenium partielles (optionnel)."
#--------------------------------------------------
# Node.js + npm (rtlcss, less)
#--------------------------------------------------
echo -e "\n---- Installing nodeJS NPM and rtlcss ----"
${DNF} nodejs npm
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "dnf nodejs installation error."
exit 1
fi
sudo npm install -g rtlcss less || echo "npm rtlcss/less: erreur (optionnel)."
echo -e "\n---- Test tool ----"
npm install || echo "npm install (prettier/plugin-xml): erreur (optionnel)."
sudo ln -fs /usr/local/bin/lessc /usr/bin/lessc 2>/dev/null || true
#--------------------------------------------------
# nginx (optionnel)
#--------------------------------------------------
if [ "${EL_INSTALL_NGINX}" = "True" ]; then
echo -e "\n---- Installing nginx ----"
${DNF} nginx || echo "nginx: erreur (optionnel)."
fi
#--------------------------------------------------
# wkhtmltopdf (optionnel) — paquet RPM officiel wkhtmltopdf
#--------------------------------------------------
if [ "${EL_INSTALL_WKHTMLTOPDF}" = "True" ]; then
if ! command -v wkhtmltopdf >/dev/null 2>&1; then
echo -e "\n---- Installing wkhtml (best-effort) ----"
# wkhtmltopdf ne publie plus de build « fedora-* » ; le RPM AlmaLinux 9
# (EL9) est compatible Fedora (testé sur F42 : dnf résout les deps).
# Repli AlmaLinux 8 au besoin.
_base="https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3"
sudo dnf install -y "${_base}/wkhtmltox-0.12.6.1-3.almalinux9.x86_64.rpm" \
|| sudo dnf install -y "${_base}/wkhtmltox-0.12.6.1-3.almalinux8.x86_64.rpm" \
|| echo "wkhtmltopdf non installé (optionnel)."
else
echo -e "\n---- Already installed wkhtml ----"
fi
fi
echo -e "\n---- Fedora dependency installation done ----"

View file

@ -22,14 +22,6 @@ VENV_ODOO_PATH=".venv.${EL_ERPLIBRE_VERSION}"
POETRY_ODOO_PATH=${VENV_ERPLIBRE_PATH}/bin/poetry POETRY_ODOO_PATH=${VENV_ERPLIBRE_PATH}/bin/poetry
export WITH_POETRY_INSTALLATION=1 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.
if [[ "${EL_VERBOSE:-0}" == "1" ]]; then
POETRY_VERBOSE="-vvv"
else
POETRY_VERBOSE="-q"
fi
# EL_PHASE controls which steps to execute. Used by install_locally_dev.sh # EL_PHASE controls which steps to execute. Used by install_locally_dev.sh
# for parallel installation — do not set manually unless you know what you do. # for parallel installation — do not set manually unless you know what you do.
# all (default) – full install: setup + poetry phases # all (default) – full install: setup + poetry phases
@ -91,7 +83,7 @@ if [[ "${EL_PHASE}" != "setup" ]]; then
# To fix keyring problem when installation is blocked, use # To fix keyring problem when installation is blocked, use
export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
if [[ ${WITH_POETRY_INSTALLATION} -ne 0 ]]; then if [[ ${WITH_POETRY_INSTALLATION} -ne 0 ]]; then
poetry install --no-root ${POETRY_VERBOSE} poetry install --no-root -vvv
fi fi
retVal=$? retVal=$?
if [[ $retVal -ne 0 ]]; then if [[ $retVal -ne 0 ]]; then

View file

@ -2,14 +2,6 @@
. ./env_var.sh . ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml" #EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml" #EL_MANIFEST_DEV="./manifest/default.dev.xml"
@ -18,4 +10,4 @@ fi
# Update git-repo # Update git-repo
.venv.erplibre/bin/repo init -u https://github.com/ERPLibre/ERPLibre -b $(git rev-parse --verify HEAD) -m ${EL_MANIFEST_DEV} .venv.erplibre/bin/repo init -u https://github.com/ERPLibre/ERPLibre -b $(git rev-parse --verify HEAD) -m ${EL_MANIFEST_DEV}
.venv.erplibre/bin/repo sync ${REPO_VERBOSE} .venv.erplibre/bin/repo sync -v

View file

@ -1,19 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
. ./env_var.sh . ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml" #EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml" #EL_MANIFEST_DEV="./manifest/default.dev.xml"
# Update git-repo # Update git-repo
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} & git daemon --base-path=. --export-all --reuseaddr --informative-errors --verbose &
DAEMON_PID=$! DAEMON_PID=$!
if [ -L "$EL_MANIFEST_DEV" ]; then if [ -L "$EL_MANIFEST_DEV" ]; then
@ -32,6 +24,6 @@ fi
.venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA .venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA
.venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET} "$@" .venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET} "$@"
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET} .venv.erplibre/bin/repo sync -c -j "$JOBS" -v -m ${MANIFEST_TARGET}
kill ${DAEMON_PID} kill ${DAEMON_PID}

View file

@ -2,19 +2,11 @@
. ./env_var.sh . ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml" #EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml" #EL_MANIFEST_DEV="./manifest/default.dev.xml"
# Update git-repo # Update git-repo
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} & git daemon --base-path=. --export-all --reuseaddr --informative-errors --verbose &
DAEMON_PID=$! DAEMON_PID=$!
if [ -L "$EL_MANIFEST_DEV" ]; then if [ -L "$EL_MANIFEST_DEV" ]; then
@ -33,6 +25,6 @@ fi
.venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA .venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA
.venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET} -g base,code_generator .venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET} -g base,code_generator
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET} .venv.erplibre/bin/repo sync -c -j "$JOBS" -v -m ${MANIFEST_TARGET}
kill ${DAEMON_PID} kill ${DAEMON_PID}

View file

@ -1,19 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
. ./env_var.sh . ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml" #EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml" #EL_MANIFEST_DEV="./manifest/default.dev.xml"
# Update git-repo # Update git-repo
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} & git daemon --base-path=. --export-all --reuseaddr --informative-errors --verbose &
DAEMON_PID=$! DAEMON_PID=$!
if [ -L "$EL_MANIFEST_DEV" ]; then if [ -L "$EL_MANIFEST_DEV" ]; then
@ -32,6 +24,6 @@ fi
.venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_mobile .venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_mobile
.venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET} "$@" .venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET} "$@"
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET} .venv.erplibre/bin/repo sync -c -j "$JOBS" -v -m ${MANIFEST_TARGET}
kill ${DAEMON_PID} kill ${DAEMON_PID}

View file

@ -2,19 +2,11 @@
. ./env_var.sh . ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml" #EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml" #EL_MANIFEST_DEV="./manifest/default.dev.xml"
# Update git-repo # Update git-repo
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} & git daemon --base-path=. --export-all --reuseaddr --informative-errors --verbose &
DAEMON_PID=$! DAEMON_PID=$!
if [ -L "$EL_MANIFEST_DEV" ]; then if [ -L "$EL_MANIFEST_DEV" ]; then
@ -33,6 +25,6 @@ fi
.venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA .venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA
.venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET} .venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET}
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET} .venv.erplibre/bin/repo sync -c -j "$JOBS" -v -m ${MANIFEST_TARGET}
kill ${DAEMON_PID} kill ${DAEMON_PID}

View file

@ -2,14 +2,6 @@
. ./env_var.sh . ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml" #EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml" #EL_MANIFEST_DEV="./manifest/default.dev.xml"
@ -24,4 +16,4 @@ fi
# Update git-repo # Update git-repo
.venv.erplibre/bin/repo init -u https://github.com/ERPLibre/ERPLibre -b $(git rev-parse --verify HEAD) .venv.erplibre/bin/repo init -u https://github.com/ERPLibre/ERPLibre -b $(git rev-parse --verify HEAD)
.venv.erplibre/bin/repo sync ${REPO_VERBOSE} -j "$JOBS" .venv.erplibre/bin/repo sync -v -j "$JOBS"

View file

@ -51,7 +51,6 @@ import hashlib
import os import os
import re import re
import shutil import shutil
import socket
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
@ -91,60 +90,19 @@ FEDORA_VERSIONS: dict[str, tuple[str, str, int, str]] = {
"43": ("43", "fedora43", 2048, "15G"), "43": ("43", "fedora43", 2048, "15G"),
"44": ("44", "fedora44", 2048, "15G"), "44": ("44", "fedora44", 2048, "15G"),
} }
# Arch est en rolling release : une seule « version » (latest).
ARCH_VERSIONS: dict[str, tuple[str, str, int, str]] = {
"latest": ("latest", "archlinux", 1024, "10G"),
}
# distro -> (table des versions, version par défaut). # distro -> (table des versions, version par défaut).
DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = { DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = {
"ubuntu": (UBUNTU_VERSIONS, "24.04"), "ubuntu": (UBUNTU_VERSIONS, "24.04"),
"debian": (DEBIAN_VERSIONS, "12"), "debian": (DEBIAN_VERSIONS, "12"),
"fedora": (FEDORA_VERSIONS, "42"), "fedora": (FEDORA_VERSIONS, "42"),
"arch": (ARCH_VERSIONS, "latest"),
} }
# Traduction de l'arch générique (amd64/arm64) vers le nom propre à la distro. # Traduction de l'arch générique (amd64/arm64) vers le nom propre à la distro.
# s390x s'écrit « s390x » partout (identité), donc aucune entrée n'est requise.
ARCH_ALIASES: dict[str, dict[str, str]] = { ARCH_ALIASES: dict[str, dict[str, str]] = {
"fedora": {"amd64": "x86_64", "arm64": "aarch64"}, "fedora": {"amd64": "x86_64", "arm64": "aarch64"},
"arch": {"amd64": "x86_64", "arm64": "aarch64"},
} }
# Architectures non-x86 nécessitant une machine/un amorçage spécifiques (voir
# virt_install). Émulées en TCG (pas de KVM), donc LENTES, quand elles
# diffèrent de l'architecture de l'hôte.
NON_X86_ARCHES: tuple[str, ...] = ("arm64", "s390x")
# Distros publiant des images cloud par architecture (vérifié juillet 2026) :
# - s390x (IBM Z) : Ubuntu seulement (Debian/Fedora : 404 ; Arch : x86/arm).
# - arm64/aarch64 : Ubuntu, Debian, Fedora (Arch : pas d'image cloud officielle
# aarch64 sur geo.mirror.pkgbuild.com).
S390X_DISTROS: tuple[str, ...] = ("ubuntu",)
ARM64_DISTROS: tuple[str, ...] = ("ubuntu", "debian", "fedora")
# arch générique -> distros la publiant (pour valider --arch tôt).
ARCH_DISTRO_SUPPORT: dict[str, tuple[str, ...]] = {
"s390x": S390X_DISTROS,
"arm64": ARM64_DISTROS,
}
def host_arch() -> str:
"""Architecture de l'hôte en jeton générique (amd64/arm64/s390x)."""
try:
machine = os.uname().machine
except (AttributeError, OSError):
machine = ""
return {
"x86_64": "amd64",
"amd64": "amd64",
"aarch64": "arm64",
"arm64": "arm64",
"s390x": "s390x",
}.get(machine, "amd64")
ARCH_CLOUD_BASE = "https://geo.mirror.pkgbuild.com/images/latest"
CLOUD_IMG_BASE = "https://cloud-images.ubuntu.com" CLOUD_IMG_BASE = "https://cloud-images.ubuntu.com"
# Debian : cloud.debian.org est un redirecteur qui, selon le réseau, peut # Debian : cloud.debian.org est un redirecteur qui, selon le réseau, peut
# renvoyer vers un miroir injoignable. On essaie donc plusieurs bases dans # renvoyer vers un miroir injoignable. On essaie donc plusieurs bases dans
@ -201,9 +159,6 @@ def image_candidates(
] ]
if distro == "fedora": if distro == "fedora":
return [resolve_fedora_url(version, arch, dry_run)] return [resolve_fedora_url(version, arch, dry_run)]
if distro == "arch":
# Rolling release : image « latest » officielle (cloud-init inclus).
return [f"{ARCH_CLOUD_BASE}/Arch-Linux-{a}-cloudimg.qcow2"]
raise ValueError(f"URL indisponible pour la distro {distro!r}") raise ValueError(f"URL indisponible pour la distro {distro!r}")
@ -238,8 +193,6 @@ def default_image_name(distro: str, code: str, arch: str, version: str) -> str:
return f"ubuntu-{version}-server-cloudimg-{a}.img" return f"ubuntu-{version}-server-cloudimg-{a}.img"
if distro == "debian": if distro == "debian":
return f"debian-{version}-genericcloud-{a}.qcow2" return f"debian-{version}-genericcloud-{a}.qcow2"
if distro == "arch":
return f"arch-linux-{a}-cloudimg.qcow2"
return f"fedora-cloud-{version}-{a}.qcow2" return f"fedora-cloud-{version}-{a}.qcow2"
@ -422,44 +375,6 @@ DAEMON_PACKAGES: dict[str, list[str]] = {
"brew": [], "brew": [],
} }
# Émulateurs QEMU système pour architectures non-x86, requis pour --arch
# <arch> sur un hôte d'architecture différente (émulation TCG). Par arch puis
# par gestionnaire de paquets. NB apt : qemu-system-misc ne contient PAS s390x
# (alpha/avr/… seulement) -> paquet dédié qemu-system-s390x ; qemu-system-arm
# fournit qemu-system-aarch64 ; l'arm64 exige aussi le firmware UEFI AAVMF.
EMULATOR_PACKAGES: dict[str, dict[str, list[str]]] = {
"s390x": {
"apt": ["qemu-system-s390x"],
"dnf": ["qemu-system-s390x"],
"pacman": ["qemu-emulators-full"],
"zypper": ["qemu-s390"],
"brew": [],
},
"arm64": {
"apt": ["qemu-system-arm", "qemu-efi-aarch64"],
"dnf": ["qemu-system-aarch64", "edk2-aarch64"],
"pacman": ["qemu-emulators-full", "edk2-aarch64"],
"zypper": ["qemu-arm", "qemu-uefi-aarch64"],
"brew": [],
},
}
# Binaire émulateur par arch (détection de présence).
EMULATOR_BINARY: dict[str, str] = {
"s390x": "qemu-system-s390x",
"arm64": "qemu-system-aarch64",
}
# Firmwares UEFI AAVMF possibles (arm64) : au moins un doit exister.
AARCH64_FIRMWARE_PATHS: tuple[str, ...] = (
"/usr/share/AAVMF/AAVMF_CODE.fd",
"/usr/share/AAVMF/AAVMF_CODE.no-secboot.fd",
"/usr/share/edk2/aarch64/QEMU_EFI.fd",
"/usr/share/edk2/aarch64/QEMU_EFI-silent.fd",
"/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
"/usr/share/edk2-armvirt/aarch64/QEMU_EFI.fd",
)
# Gestionnaires de paquets, dans l'ordre de préférence : # Gestionnaires de paquets, dans l'ordre de préférence :
# (clé TOOL_PACKAGES, binaire à détecter, commande d'installation, sudo, refresh) # (clé TOOL_PACKAGES, binaire à détecter, commande d'installation, sudo, refresh)
PKG_MANAGERS: tuple[ PKG_MANAGERS: tuple[
@ -668,64 +583,6 @@ def ensure_tools(runner: Runner, assume_yes: bool, no_install: bool) -> None:
ensure_libvirt_service(runner) ensure_libvirt_service(runner)
def emulator_ready(arch: str) -> bool:
"""Vrai si l'émulateur (et, pour arm64, un firmware UEFI AAVMF) est là."""
if shutil.which(EMULATOR_BINARY[arch]) is None:
return False
if arch == "arm64":
return any(os.path.exists(p) for p in AARCH64_FIRMWARE_PATHS)
return True
def ensure_emulator(
arch: str, runner: Runner, assume_yes: bool, no_install: bool
) -> None:
"""Vérifie l'émulateur QEMU système pour `arch` (et le firmware UEFI pour
arm64), requis quand on émule une architecture différente de l'hôte ;
l'installe au besoin via le gestionnaire de paquets."""
binary = EMULATOR_BINARY[arch]
if emulator_ready(arch):
return
pm = detect_pkg_manager()
if pm is None:
sys.exit(
f" Émulateur {arch} introuvable ({binary}) et gestionnaire de "
"paquets non reconnu.\n Installez-le manuellement."
)
pm_key, install_cmd, use_sudo, refresh = pm
packages = EMULATOR_PACKAGES.get(arch, {}).get(pm_key, [])
if not packages:
sys.exit(
f" Émulateur {arch} introuvable ({binary}) : installez le paquet "
"QEMU système correspondant de votre distribution "
"(+ firmware UEFI pour arm64)."
)
full_cmd = install_cmd + packages
printable = ("sudo " if use_sudo and runner.use_sudo else "") + " ".join(
full_cmd
)
print(f" Émulateur {arch} manquant ({binary}).")
print(f" Commande d'installation : {printable}")
if no_install:
sys.exit(
" Installation automatique désactivée (--no-install-deps).\n"
" Installez manuellement : " + printable
)
if not assume_yes and not prompt_yes_no(
f" Installer l'émulateur {arch} maintenant ?"
):
sys.exit(" Installation refusée.\n Commande manuelle : " + printable)
if refresh:
runner.run(refresh, privileged=use_sudo, check=False)
runner.run(full_cmd, privileged=use_sudo)
if not emulator_ready(arch):
sys.exit(
f" Émulateur {arch} ({binary}) ou firmware toujours absent après "
f"installation.\n Essayez manuellement : {printable}"
)
print(f" Émulateur {arch} installé avec succès.")
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Étapes # Étapes
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
@ -766,15 +623,6 @@ def _download_one(url: str, tmp: Path, timeout: int) -> None:
print(f" {pct:3d}%", flush=True) print(f" {pct:3d}%", flush=True)
if is_tty: if is_tty:
print() print()
# Vérifie la COMPLÉTUDE : si le serveur a annoncé une taille et qu'on a
# reçu moins (connexion coupée), le .part est TRONQUÉ. Sans ce contrôle,
# il était validé comme « complet » -> qcow2 valide mais VIDE (juste
# l'en-tête) -> VM qui ne boote pas, et cache empoisonné réutilisé ensuite.
if total > 0 and done < total:
raise OSError(
f"téléchargement incomplet : {done}/{total} octets reçus "
"(connexion interrompue)"
)
def download_image( def download_image(
@ -942,19 +790,15 @@ def build_cloud_config(
lines.append(f"package_update: {'true' if do_update else 'false'}") lines.append(f"package_update: {'true' if do_update else 'false'}")
lines.append(f"package_upgrade: {'true' if do_upgrade else 'false'}") lines.append(f"package_upgrade: {'true' if do_upgrade else 'false'}")
# On n'installe AUCUN paquet via cloud-init : cela exige le réseau au 1er # openssh-server : garantit un serveur SSH sur toutes les distros (les
# boot et peut bloquer longtemps (dnf/apt/pacman lents sur réseau lent) — # images Debian genericcloud notamment ne l'ont pas toujours activé).
# cloud-init reste alors « running » et tout ce qui suit attend. sshd est packages = ["qemu-guest-agent", "openssh-server", *args.package]
# DÉJÀ présent dans toutes les images cloud (Ubuntu/Debian/Fedora/Arch) ;
# on l'active seulement (runcmd). Les outils nécessaires (curl/git/make…)
# sont installés — avec dépôts optimisés — par le bootstrap d'installation.
packages = list(dict.fromkeys(args.package)) # seulement --package
if packages:
lines.append("packages:") lines.append("packages:")
lines += [f" - {p}" for p in packages] lines += [
f" - {p}" for p in dict.fromkeys(packages)
] # dédoublonne, ordre gardé
# Active et démarre SSH quel que soit le nom du service (ssh sur # Active et démarre SSH quel que soit le nom du service (ssh sur
# Debian/Ubuntu, sshd sur Fedora/Arch) — sans quoi la VM peut booter # Debian/Ubuntu, sshd sur Fedora) — sans quoi la VM peut booter sans SSH.
# sans SSH accessible.
lines += [ lines += [
"runcmd:", "runcmd:",
" - systemctl enable --now ssh 2>/dev/null" " - systemctl enable --now ssh 2>/dev/null"
@ -966,13 +810,10 @@ def build_cloud_config(
# network-config (cloud-init v2) : DHCP sur toute interface « e* ». Les images # network-config (cloud-init v2) : DHCP sur toute interface « e* ». Les images
# Debian genericcloud ne configurent pas toujours le réseau sans ça (le NIC # Debian genericcloud ne configurent pas toujours le réseau sans ça (le NIC
# reste down -> pas d'IP), contrairement à Ubuntu. Inoffensif pour Ubuntu. # reste down -> pas d'IP), contrairement à Ubuntu. Inoffensif pour Ubuntu.
# La clé est « eth0 » car le renderer cloud-init d'Arch utilise la CLÉ comme
# nom d'interface (en ignorant « match ») et l'image Arch nomme son NIC eth0 ;
# Debian/Fedora/Ubuntu utilisent bien « match: name: e* » (leur en*).
NETWORK_CONFIG = ( NETWORK_CONFIG = (
"version: 2\n" "version: 2\n"
"ethernets:\n" "ethernets:\n"
" eth0:\n" " primary:\n"
" match:\n" " match:\n"
' name: "e*"\n' ' name: "e*"\n'
" dhcp4: true\n" " dhcp4: true\n"
@ -1116,11 +957,6 @@ def virt_install(
osinfo: str, osinfo: str,
runner: Runner, runner: Runner,
) -> None: ) -> None:
# Émulée (TCG, pas de KVM) si l'arch demandée diffère de celle de l'hôte.
emulated = args.arch != host_arch()
# s390x n'a pas de port série ISA : la console est SCLP (ttysclp0), et non
# ttyS0. Ailleurs (x86/arm64), console série classique.
console_target = "sclp" if args.arch == "s390x" else "serial"
cmd = [ cmd = [
"virt-install", "virt-install",
"--name", "--name",
@ -1137,7 +973,6 @@ def virt_install(
# volume « cidata » est visible dès init-local et cloud-init le lit. # volume « cidata » est visible dès init-local et cloud-init le lit.
# En CD-ROM, l'initramfs Debian ne charge pas sr_mod à temps -> le # En CD-ROM, l'initramfs Debian ne charge pas sr_mod à temps -> le
# seed n'est pas vu et rien ne s'applique (Ubuntu, lui, tolère le CD). # seed n'est pas vu et rien ne s'applique (Ubuntu, lui, tolère le CD).
# Sur s390x, bus=virtio est mappé en virtio-ccw par libvirt.
"--disk", "--disk",
f"path={seed},readonly=on,bus=virtio", f"path={seed},readonly=on,bus=virtio",
"--osinfo", "--osinfo",
@ -1147,64 +982,21 @@ def virt_install(
"--graphics", "--graphics",
args.graphics, args.graphics,
"--console", "--console",
f"pty,target_type={console_target}", "pty,target_type=serial",
] ]
if args.arch == "s390x": # Boot UEFI par défaut : Debian 13 (trixie) et les images cloud récentes
# s390x (IBM Z) : machine s390-ccw-virtio, amorçage IPL/zipl depuis le # n'embarquent plus le chargeur BIOS/GRUB-pc et partent en boucle
# disque (ni BIOS ni UEFI/OVMF -> aucun --boot). # « Booting... » en SeaBIOS. UEFI (OVMF) fonctionne pour Ubuntu/Debian/
cmd += ["--arch", "s390x", "--machine", "s390-ccw-virtio"] # Fedora. --bios force l'ancien BIOS si OVMF est indisponible.
elif args.arch == "arm64": if not args.bios:
# arm64/aarch64 : machine « virt » + UEFI (firmware AAVMF). Les images cmd += ["--boot", "uefi"]
# cloud arm64 n'ont pas de BIOS -> UEFI obligatoire. Secure Boot
# DÉSACTIVÉ (comme x86) : sinon libvirt sélectionne l'AAVMF « secure »
# à clés Microsoft enrôlées et la VM RESTE FIGÉE au firmware (aucun
# boot, aucune IP). secure-boot=no -> AAVMF non-SB, boot OK.
cmd += [
"--arch",
"aarch64",
"--machine",
"virt",
"--boot",
"uefi,firmware.feature0.name=secure-boot,"
"firmware.feature0.enabled=no",
]
elif not args.bios:
# Boot UEFI par défaut (x86) : Debian 13 (trixie) et les images cloud
# récentes n'embarquent plus le chargeur BIOS/GRUB-pc et partent en
# boucle « Booting... » en SeaBIOS. UEFI (OVMF) fonctionne pour
# Ubuntu/Debian/Fedora. --bios force l'ancien BIOS si OVMF est absent.
# Secure Boot DÉSACTIVÉ : le chargeur d'Arch (GRUB) n'est pas signé et
# OVMF Secure Boot le refuse (« Access Denied » -> pas de boot).
# Ubuntu/Debian/Fedora bootent aussi sans Secure Boot.
cmd += [
"--boot",
"uefi,firmware.feature0.name=secure-boot,"
"firmware.feature0.enabled=no",
]
if emulated:
# Architecture différente de l'hôte -> émulation logicielle TCG
# (pas de KVM). LENT.
cmd += ["--virt-type", "qemu"]
if not args.attach_console: if not args.attach_console:
cmd.append("--noautoconsole") cmd.append("--noautoconsole")
runner.run(cmd, privileged=True) runner.run(cmd, privileged=True)
def _ip_reachable(ip: str, port: int = 22, timeout: float = 3) -> bool:
"""Vrai si le port SSH répond (distingue le bail actif du bail périmé)."""
try:
with socket.create_connection((ip, port), timeout=timeout):
return True
except OSError:
return False
def wait_for_ip(name: str, use_sudo: bool, timeout: int) -> str | None: def wait_for_ip(name: str, use_sudo: bool, timeout: int) -> str | None:
"""Interroge les baux DHCP libvirt jusqu'à obtenir l'IPv4 de la VM. Une VM """Interroge les baux DHCP libvirt jusqu'à obtenir l'IPv4 de la VM."""
peut avoir PLUSIEURS baux (l'image demande d'abord une IP avec le hostname
par défaut « ubuntu », puis cloud-init fixe le vrai hostname -> 2e bail) :
on renvoie une IP JOIGNABLE (sshd up), sinon la plus récente, jamais
aveuglément la 1re (souvent le bail précoce périmé, « No route to host »)."""
base = (["sudo"] if use_sudo else []) + [ base = (["sudo"] if use_sudo else []) + [
"virsh", "virsh",
"domifaddr", "domifaddr",
@ -1213,15 +1005,13 @@ def wait_for_ip(name: str, use_sudo: bool, timeout: int) -> str | None:
"lease", "lease",
] ]
deadline = time.time() + timeout deadline = time.time() + timeout
ips: list[str] = []
while time.time() < deadline: while time.time() < deadline:
res = subprocess.run(base, capture_output=True, text=True) res = subprocess.run(base, capture_output=True, text=True)
ips = re.findall(r"(\d+\.\d+\.\d+\.\d+)", res.stdout) m = re.search(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
for ip in ips: if m:
if _ip_reachable(ip): return m.group(1)
return ip
time.sleep(3) time.sleep(3)
return ips[-1] if ips else None return None
def ssh_command(user: str, ip: str, has_key: bool) -> str: def ssh_command(user: str, ip: str, has_key: bool) -> str:
@ -1280,9 +1070,7 @@ def build_parser() -> argparse.ArgumentParser:
g_img.add_argument( g_img.add_argument(
"--arch", "--arch",
default="amd64", default="amd64",
help="Architecture de l'image (défaut : amd64). amd64/x86_64, " help="Architecture de l'image (défaut : amd64).",
"arm64/aarch64 (Ubuntu/Debian/Fedora) ou s390x (Ubuntu). Toute arch "
"différente de l'hôte est ÉMULÉE (TCG, lente).",
) )
g_img.add_argument( g_img.add_argument(
"--image-dir", "--image-dir",
@ -1518,14 +1306,6 @@ def main() -> None:
f"Version {args.version!r} inconnue pour {args.distro}. " f"Version {args.version!r} inconnue pour {args.distro}. "
f"Choix : {', '.join(versions)} (voir --list-images)." f"Choix : {', '.join(versions)} (voir --list-images)."
) )
# Certaines architectures ne sont publiées que par une partie des distros
# (voir ARCH_DISTRO_SUPPORT) : on valide tôt plutôt qu'échouer au download.
supported = ARCH_DISTRO_SUPPORT.get(args.arch)
if supported is not None and args.distro not in supported:
sys.exit(
f"Architecture {args.arch} indisponible pour {args.distro!r} : "
f"images cloud publiées seulement pour {', '.join(supported)}."
)
code, default_osinfo, min_ram, min_disk = versions[args.version] code, default_osinfo, min_ram, min_disk = versions[args.version]
if args.codename: if args.codename:
code = args.codename code = args.codename
@ -1535,10 +1315,6 @@ def main() -> None:
args.memory = min_ram args.memory = min_ram
if args.disk_size is None: if args.disk_size is None:
args.disk_size = min_disk args.disk_size = min_disk
# Un nombre nu (« 30 ») serait pris pour des OCTETS par qemu-img et ferait
# échouer le resize : on suppose des Go par défaut (« 30 » -> « 30G »).
if re.fullmatch(r"\d+", str(args.disk_size)):
args.disk_size = f"{args.disk_size}G"
urls = image_candidates( urls = image_candidates(
args.distro, code, args.arch, args.version, args.dry_run args.distro, code, args.arch, args.version, args.dry_run
) )
@ -1597,18 +1373,6 @@ def main() -> None:
if not args.dry_run: if not args.dry_run:
ensure_tools(runner, args.assume_yes, args.no_install_deps) ensure_tools(runner, args.assume_yes, args.no_install_deps)
# Émulation requise si l'arch diffère de l'hôte : installe l'émulateur
# QEMU système adéquat (+ firmware UEFI pour arm64) et prévient de la
# lenteur.
if args.arch in EMULATOR_BINARY and args.arch != host_arch():
ensure_emulator(
args.arch, runner, args.assume_yes, args.no_install_deps
)
print(
f" Note : {args.arch} est ÉMULÉ (TCG) sur cet hôte "
f"({host_arch()}) — le boot et l'installation seront "
"nettement plus lents que l'architecture native."
)
print(f"\n== 1/5 Image cloud ({args.distro} {args.version} / {code}) ==") print(f"\n== 1/5 Image cloud ({args.distro} {args.version} / {code}) ==")
download_image(urls, args.image_path, args.dry_run) download_image(urls, args.image_path, args.dry_run)

View file

@ -1,315 +0,0 @@
#!/usr/bin/env python3
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Dashboard Textual : suivi des installations ERPLibre parallèles sur VM.
Principe « détachable » : chaque installation tourne dans un processus
DÉTACHÉ (setsid) qui écrit un fichier log et, à la fin, un marqueur
« __ERPLIBRE_EXIT__ <code> ». Ce module ne fait que VISUALISER ces fichiers :
quitter le dashboard n'arrête rien, on peut le rouvrir pour ré-attacher.
- launch_installs(...) : lance les process détachés + écrit un manifeste JSON.
- run_monitor(manifest_path) : ouvre le dashboard Textual sur un manifeste.
"""
from __future__ import annotations
import json
import os
import shlex
import subprocess
import time
from pathlib import Path
try:
from script.todo.todo_i18n import t
except Exception: # pragma: no cover - repli si i18n indisponible
def t(key: str) -> str:
return key
EXIT_MARKER = "__ERPLIBRE_EXIT__"
SSH_OPTS = (
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
"-o ConnectTimeout=8"
)
def session_dir() -> Path:
"""Répertoire des logs/manifestes d'installation (créé au besoin)."""
base = Path(os.path.expanduser("~/.erplibre/qemu-install"))
base.mkdir(parents=True, exist_ok=True)
return base
def _launch_one(ip: str, remote_cmd: str, log_path: str) -> None:
"""Lance une install SSH DÉTACHÉE : attend le sshd, exécute, journalise
la sortie puis écrit le marqueur de fin avec le code de sortie."""
# Sonde de disponibilité : on attend que sshd réponde ET que cloud-init
# soit TERMINÉ, via des connexions COURTES successives (jusqu'à ~20 min :
# une architecture ÉMULÉE, s390x/arm64 sur hôte x86, boote lentement).
# Au 1er boot, cloud-init régénère les clés d'hôte et REDÉMARRE sshd : une
# session SSH longue (ex. « cloud-init status --wait ») serait alors tuée
# (« Connection closed by remote host », exit 255 — cas Fedora). Chaque
# itération étant une connexion neuve, un redémarrage de sshd ne casse que
# la tentative en cours. On imprime toujours l'état (|| true) pour matcher
# sur le TEXTE, « status: running » n'ayant pas de code de sortie fiable.
ci_probe = (
"if command -v cloud-init >/dev/null 2>&1; then "
"cloud-init status 2>/dev/null || true; else echo nocloudinit; fi"
)
log_q = shlex.quote(log_path)
# On écrit un message d'attente + un battement toutes les ~30 s : sinon le
# log reste VIDE pendant tout le boot émulé et paraît « bloqué ».
msg_wait = t("Waiting for the VM to start (boot + cloud-init)")
msg_slow = t("(an emulated architecture can be slow; this is normal)")
msg_ready = t("VM ready - starting the ERPLibre install")
wrapper = (
f"echo {shlex.quote('== ' + msg_wait + ' ==')} >> {log_q}; "
f"echo {shlex.quote(' ' + msg_slow)} >> {log_q}; "
f"for i in $(seq 1 240); do "
f"st=$(ssh {SSH_OPTS} -o BatchMode=yes erplibre@{ip} "
f"{shlex.quote(ci_probe)} 2>/dev/null); "
f'case "$st" in '
f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) break;; "
f"esac; "
f'if [ $((i % 6)) -eq 0 ]; then echo " ... $((i*5))s" >> {log_q}; fi; '
f"sleep 5; done; "
f"echo {shlex.quote('== ' + msg_ready + ' ==')} >> {log_q}; "
f"ssh {SSH_OPTS} erplibre@{ip} {shlex.quote(remote_cmd)} "
f">> {log_q} 2>&1; "
f'echo "{EXIT_MARKER} $?" >> {log_q}'
)
# setsid -f : le process survit à la fermeture du menu / du dashboard.
subprocess.Popen(
["setsid", "-f", "bash", "-c", wrapper],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
def _log_header(vm: dict, branch: str, when: str) -> str:
"""En-tête du log : date, VM, distribution, version, architecture, branche.
Permet d'identifier l'installation d'un coup d'œil (et de ne jamais laisser
le log vide pendant l'attente du boot)."""
distro = vm.get("distro") or "?"
version = vm.get("version") or ""
arch = vm.get("arch") or "?"
bar = "=" * 64
return (
f"{bar}\n"
f" ERPLibre — {t('installation')}\n"
f" Date : {when}\n"
f" VM : {vm['name']}\n"
f" Distribution : {distro} {version}\n"
f" Architecture : {arch}\n"
f" Branche : {branch}\n"
f" IP : {vm['ip']}\n"
f"{bar}\n\n"
)
def launch_installs(vms: list[dict], branch: str, remote_cmd: str) -> str:
"""vms : [{name, ip, distro?, version?, arch?}]. Lance chaque install
détachée, écrit un manifeste et retourne son chemin. remote_cmd : script
exécuté dans chaque VM."""
sdir = session_dir()
stamp = time.strftime("%Y%m%d-%H%M%S")
when = time.strftime("%Y-%m-%d %H:%M:%S")
logdir = sdir / stamp
logdir.mkdir(parents=True, exist_ok=True)
entries = []
for vm in vms:
log_path = str(logdir / f"{vm['name']}.log")
# En-tête d'emblée (date/distro/version/arch) : le log n'est jamais
# vide, l'utilisateur voit tout de suite QUOI s'installe.
Path(log_path).write_text(_log_header(vm, branch, when))
_launch_one(vm["ip"], remote_cmd, log_path)
entries.append(
{
"name": vm["name"],
"ip": vm["ip"],
"distro": vm.get("distro"),
"version": vm.get("version"),
"arch": vm.get("arch"),
"log": log_path,
"ssh": f"ssh erplibre@{vm['ip']}",
}
)
manifest = {
"branch": branch,
"started": time.time(),
"vms": entries,
}
manifest_path = str(logdir / "session.json")
Path(manifest_path).write_text(json.dumps(manifest, indent=2))
return manifest_path
def read_status(log_path: str) -> tuple[str, int | None]:
"""(état, code) d'un log : pending / running / done / failed."""
try:
data = Path(log_path).read_text(errors="replace")
except OSError:
return "pending", None
if not data.strip():
return "pending", None
for line in reversed(data.splitlines()):
if EXIT_MARKER in line:
try:
code = int(line.split()[-1])
except ValueError:
code = 1
return ("done" if code == 0 else "failed"), code
return "running", None
# --------------------------------------------------------------------------- #
# Dashboard Textual
# --------------------------------------------------------------------------- #
def run_monitor(manifest_path: str) -> None:
"""Ouvre le dashboard Textual sur un manifeste d'installation."""
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.widgets import DataTable, Footer, Header, RichLog, Static
manifest = json.loads(Path(manifest_path).read_text())
started = manifest.get("started", time.time())
vms = manifest["vms"]
ICON = {
"pending": "⏳",
"running": "⏳",
"done": "✅",
"failed": "❌",
}
class Monitor(App):
CSS = """
DataTable { width: 40; border: solid $accent; }
RichLog { border: solid $accent; }
#sshbar { height: 2; color: $text-muted; }
"""
BINDINGS = [
("q", "quit", "Quitter (détaché)"),
("s", "ssh", "SSH"),
("f", "follow", "Suivre"),
("c", "copy_log", "Copier log"),
]
def __init__(self):
super().__init__()
self._offsets = {vm["name"]: 0 for vm in vms}
self._selected = vms[0]["name"] if vms else None
self._follow = True
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
table = DataTable(id="vms", cursor_type="row")
# Clés de colonnes explicites : update_cell() les référence.
table.add_column("VM", key="vm")
table.add_column("État", key="state")
table.add_column("Durée", key="elapsed")
for vm in vms:
table.add_row(vm["name"], "⏳", "00:00", key=vm["name"])
self._log = RichLog(id="log", highlight=False, markup=False)
with Horizontal():
yield table
yield self._log
yield Static("", id="sshbar")
yield Footer()
def on_mount(self) -> None:
self.title = "ERPLibre — suivi d'installation"
self._refresh_ssh()
self._load_selected_log(reset=True)
self.set_interval(1.0, self._tick)
# -- helpers -------------------------------------------------------- #
def _vm_by_name(self, name):
return next((v for v in vms if v["name"] == name), None)
def _refresh_ssh(self):
vm = self._vm_by_name(self._selected)
bar = self.query_one("#sshbar", Static)
if vm:
bar.update(
f" {vm['ssh']} (s = SSH · c = copier le log · "
"Maj+glisser = sélectionner)\n"
f" Log : {vm['log']}"
)
def _load_selected_log(self, reset=False):
vm = self._vm_by_name(self._selected)
if not vm:
return
if reset:
self._log.clear()
self._offsets[vm["name"]] = 0
try:
data = Path(vm["log"]).read_text(errors="replace")
except OSError:
return
off = self._offsets[vm["name"]]
new = data[off:]
if new:
for line in new.splitlines():
if EXIT_MARKER not in line:
self._log.write(line)
self._offsets[vm["name"]] = len(data)
def _tick(self):
table = self.query_one("#vms", DataTable)
for vm in vms:
state, code = read_status(vm["log"])
elapsed = time.time() - started
mm, ss = divmod(int(elapsed), 60)
label = ICON[state]
if state == "failed":
label = f"❌ ({code})"
elif state == "done":
label = "✅"
table.update_cell(vm["name"], "state", label)
if state in ("running", "pending"):
table.update_cell(
vm["name"], "elapsed", f"{mm:02d}:{ss:02d}"
)
if self._follow:
self._load_selected_log()
# -- events --------------------------------------------------------- #
def on_data_table_row_highlighted(self, event) -> None:
name = event.row_key.value
if name and name != self._selected:
self._selected = name
self._refresh_ssh()
self._load_selected_log(reset=True)
def action_follow(self) -> None:
self._follow = not self._follow
def action_ssh(self) -> None:
vm = self._vm_by_name(self._selected)
if not vm:
return
with self.suspend():
os.system(f"ssh {SSH_OPTS} erplibre@{vm['ip']} || true")
def action_copy_log(self) -> None:
"""Copie le log complet de la VM sélectionnée dans le
presse-papiers (OSC 52 ; marche aussi à travers SSH)."""
vm = self._vm_by_name(self._selected)
if not vm:
return
try:
text = Path(vm["log"]).read_text(errors="replace")
except OSError:
return
self.copy_to_clipboard(text)
self.notify(
f"Log de {vm['name']} copié ({len(text)} car.)",
title="Presse-papiers",
)
Monitor().run()

File diff suppressed because it is too large Load diff

View file

@ -1162,139 +1162,6 @@ TRANSLATIONS = {
"fr": "Version pour", "fr": "Version pour",
"en": "Version for", "en": "Version for",
}, },
"Architecture:": {
"fr": "Architecture :",
"en": "Architecture:",
},
"native": {
"fr": "native",
"en": "native",
},
"IBM Z — emulated, slow on x86; Ubuntu only": {
"fr": "IBM Z — émulé, lent sur x86 ; Ubuntu uniquement",
"en": "IBM Z — emulated, slow on x86; Ubuntu only",
},
"IBM Z — emulated, slow; Ubuntu only": {
"fr": "IBM Z — émulé, lent ; Ubuntu uniquement",
"en": "IBM Z — emulated, slow; Ubuntu only",
},
"ARM 64-bit — emulated, slow": {
"fr": "ARM 64 bits — émulé, lent",
"en": "ARM 64-bit — emulated, slow",
},
"emulated, slow": {
"fr": "émulé, lent",
"en": "emulated, slow",
},
"This architecture is emulated (TCG): boot and install are"
" much slower than the native one.": {
"fr": "Cette architecture est émulée (TCG) : le boot et l'installation"
" sont bien plus lents que l'architecture native.",
"en": "This architecture is emulated (TCG): boot and install are"
" much slower than the native one.",
},
"images for this arch only exist for:": {
"fr": "images pour cette architecture disponibles seulement pour :",
"en": "images for this arch only exist for:",
},
"All supported architectures": {
"fr": "Toutes les architectures supportées",
"en": "All supported architectures",
},
"installation": {
"fr": "installation",
"en": "installation",
},
"Resources per VM (x1 = catalog minimum):": {
"fr": "Ressources par VM (x1 = minimum catalogue) :",
"en": "Resources per VM (x1 = catalog minimum):",
},
"Host:": {
"fr": "Hôte :",
"en": "Host:",
},
"free": {
"fr": "libres",
"en": "free",
},
"free RAM unknown": {
"fr": "RAM libre inconnue",
"en": "free RAM unknown",
},
"> host free RAM": {
"fr": "> RAM libre de l'hôte",
"en": "> host free RAM",
},
"total RAM": {
"fr": "RAM totale",
"en": "total RAM",
},
"Choice (1-4, default 1):": {
"fr": "Choix (1-4, défaut 1) :",
"en": "Choice (1-4, default 1):",
},
"(includes emulated architectures — some VMs are slow)": {
"fr": "(inclut des architectures émulées — certaines VM sont lentes)",
"en": "(includes emulated architectures — some VMs are slow)",
},
"Waiting for the VM to start (boot + cloud-init)": {
"fr": "En attente du démarrage de la VM (boot + cloud-init)",
"en": "Waiting for the VM to start (boot + cloud-init)",
},
"(an emulated architecture can be slow; this is normal)": {
"fr": "(une architecture émulée peut être lente ; c'est normal)",
"en": "(an emulated architecture can be slow; this is normal)",
},
"VM ready - starting the ERPLibre install": {
"fr": "VM prête — installation ERPLibre en cours",
"en": "VM ready - starting the ERPLibre install",
},
"Choice (number or name, blank = native):": {
"fr": "Choix (numéro ou nom, vide = native) :",
"en": "Choice (number or name, blank = native):",
},
"Pick exact versions (comma-separated list)": {
"fr": "Choisir des versions précises (liste séparée par des virgules)",
"en": "Pick exact versions (comma-separated list)",
},
"Selection (numbers, 'all', 'principal' or 'granulaire',"
" default: all): ": {
"fr": "Sélection (numéros, « all », « principal » ou « granulaire »,"
" défaut : all) : ",
"en": "Selection (numbers, 'all', 'principal' or 'granulaire',"
" default: all): ",
},
"All versions:": {
"fr": "Toutes les versions :",
"en": "All versions:",
},
"Selection (comma-separated numbers): ": {
"fr": "Sélection (numéros séparés par des virgules) : ",
"en": "Selection (comma-separated numbers): ",
},
"s390x images only exist for:": {
"fr": "images s390x disponibles seulement pour :",
"en": "s390x images only exist for:",
},
"ignored:": {
"fr": "ignoré :",
"en": "ignored:",
},
"IBM Z — emulated, slow on x86": {
"fr": "IBM Z — émulé, lent sur x86",
"en": "IBM Z — emulated, slow on x86",
},
"Choice (number or name, blank = amd64):": {
"fr": "Choix (numéro ou nom, vide = amd64) :",
"en": "Choice (number or name, blank = amd64):",
},
"s390x is emulated (TCG): boot and install are much "
"slower than x86.": {
"fr": "s390x est émulé (TCG) : le boot et l'installation sont bien "
"plus lents que x86.",
"en": "s390x is emulated (TCG): boot and install are much "
"slower than x86.",
},
"Choice (number or version, blank = default):": { "Choice (number or version, blank = default):": {
"fr": "Choix (numéro ou version, vide = défaut) :", "fr": "Choix (numéro ou version, vide = défaut) :",
"en": "Choice (number or version, blank = default):", "en": "Choice (number or version, blank = default):",
@ -1347,102 +1214,6 @@ TRANSLATIONS = {
"fr": "Effacement terminé.", "fr": "Effacement terminé.",
"en": "Deletion done.", "en": "Deletion done.",
}, },
"Clean up QEMU (orphan files)": {
"fr": "Nettoyer QEMU (fichiers orphelins)",
"en": "Clean up QEMU (orphan files)",
},
"Scanning for orphan QEMU files...": {
"fr": "Recherche des fichiers QEMU orphelins...",
"en": "Scanning for orphan QEMU files...",
},
"orphan disk": {
"fr": "disque orphelin",
"en": "orphan disk",
},
"orphan seed": {
"fr": "seed orphelin",
"en": "orphan seed",
},
"partial download": {
"fr": "téléchargement interrompu",
"en": "partial download",
},
"orphan UEFI nvram": {
"fr": "nvram UEFI orpheline",
"en": "orphan UEFI nvram",
},
"No orphan files found.": {
"fr": "Aucun fichier orphelin trouvé.",
"en": "No orphan files found.",
},
"Orphan files:": {
"fr": "Fichiers orphelins :",
"en": "Orphan files:",
},
"Total:": {
"fr": "Total :",
"en": "Total:",
},
"files": {
"fr": "fichiers",
"en": "files",
},
"Delete these orphan files? (y/N): ": {
"fr": "Effacer ces fichiers orphelins ? (o/N) : ",
"en": "Delete these orphan files? (y/N): ",
},
"Cleanup done.": {
"fr": "Nettoyage terminé.",
"en": "Cleanup done.",
},
"Cached base images (reusable):": {
"fr": "Images de base en cache (réutilisables) :",
"en": "Cached base images (reusable):",
},
"Also delete the cached base images? (y/N): ": {
"fr": "Effacer aussi les images de base en cache ? (o/N) : ",
"en": "Also delete the cached base images? (y/N): ",
},
"All cached base images (reusable):": {
"fr": "Toutes les images de base en cache (réutilisables) :",
"en": "All cached base images (reusable):",
},
"Delete ALL cached base images? (y/N): ": {
"fr": "Effacer TOUTES les images de base en cache ? (o/N) : ",
"en": "Delete ALL cached base images? (y/N): ",
},
"Ghost domains (defined but disk missing):": {
"fr": "Domaines fantômes (définis, disque manquant) :",
"en": "Ghost domains (defined but disk missing):",
},
"Undefine these ghost domains? (y/N): ": {
"fr": "Supprimer la définition de ces domaines ? (o/N) : ",
"en": "Undefine these ghost domains? (y/N): ",
},
"Stale codename-named Ubuntu images (duplicates):": {
"fr": "Images Ubuntu nommées par codename (doublons) :",
"en": "Stale codename-named Ubuntu images (duplicates):",
},
"Delete these duplicate images? (y/N): ": {
"fr": "Effacer ces images en double ? (o/N) : ",
"en": "Delete these duplicate images? (y/N): ",
},
"Orphan ~/.ssh/config entries:": {
"fr": "Entrées ~/.ssh/config orphelines :",
"en": "Orphan ~/.ssh/config entries:",
},
"Remove these ~/.ssh/config entries? (y/N): ": {
"fr": "Retirer ces entrées de ~/.ssh/config ? (o/N) : ",
"en": "Remove these ~/.ssh/config entries? (y/N): ",
},
"Stale DHCP leases (no matching VM):": {
"fr": "Baux DHCP périmés (aucune VM correspondante) :",
"en": "Stale DHCP leases (no matching VM):",
},
"Clear these stale leases? (y/N): ": {
"fr": "Effacer ces baux périmés ? (o/N) : ",
"en": "Clear these stale leases? (y/N): ",
},
"Deploy ERPLibre infra (one minimal VM per image)": { "Deploy ERPLibre infra (one minimal VM per image)": {
"fr": "Déployer l'infra ERPLibre (une VM minimale par image)", "fr": "Déployer l'infra ERPLibre (une VM minimale par image)",
"en": "Deploy ERPLibre infra (one minimal VM per image)", "en": "Deploy ERPLibre infra (one minimal VM per image)",
@ -1463,14 +1234,6 @@ TRANSLATIONS = {
"fr": "Tout le catalogue (chaque version)", "fr": "Tout le catalogue (chaque version)",
"en": "Whole catalog (every version)", "en": "Whole catalog (every version)",
}, },
"The main version of each distro (marked *)": {
"fr": "La version principale de chaque distro (marquée *)",
"en": "The main version of each distro (marked *)",
},
"Selection (numbers, 'all' or 'principal', default: all): ": {
"fr": "Sélection (numéros, « all » ou « principal », défaut : all) : ",
"en": "Selection (numbers, 'all' or 'principal', default: all): ",
},
"Selection (numbers, or 'all', default: all): ": { "Selection (numbers, or 'all', default: all): ": {
"fr": "Sélection (numéros, ou « all », défaut : all) : ", "fr": "Sélection (numéros, ou « all », défaut : all) : ",
"en": "Selection (numbers, or 'all', default: all): ", "en": "Selection (numbers, or 'all', default: all): ",
@ -1524,70 +1287,6 @@ TRANSLATIONS = {
"fr": "Clonage d'ERPLibre sur chaque VM", "fr": "Clonage d'ERPLibre sur chaque VM",
"en": "Cloning ERPLibre on each VM", "en": "Cloning ERPLibre on each VM",
}, },
"Installing ERPLibre on each VM": {
"fr": "Installation d'ERPLibre sur chaque VM",
"en": "Installing ERPLibre on each VM",
},
"Install ERPLibre into ~/git/erplibre on this VM? (y/N): ": {
"fr": "Installer ERPLibre dans ~/git/erplibre sur cette VM ? (o/N) : ",
"en": "Install ERPLibre into ~/git/erplibre on this VM? (y/N): ",
},
"installing ERPLibre": {
"fr": "installation d'ERPLibre",
"en": "installing ERPLibre",
},
"no IP obtained, ERPLibre install skipped.": {
"fr": "aucune IP obtenue, installation ERPLibre ignorée.",
"en": "no IP obtained, ERPLibre install skipped.",
},
"waiting for SSH...": {
"fr": "attente du SSH...",
"en": "waiting for SSH...",
},
"SSH not reachable, ERPLibre install skipped.": {
"fr": "SSH injoignable, installation ERPLibre ignorée.",
"en": "SSH not reachable, ERPLibre install skipped.",
},
"Interactive monitoring dashboard? (y/N): ": {
"fr": "Suivi interactif (dashboard) ? (o/N) : ",
"en": "Interactive monitoring dashboard? (y/N): ",
},
"resolving IP...": {
"fr": "résolution de l'IP...",
"en": "resolving IP...",
},
"no IP, skipped.": {
"fr": "pas d'IP, ignorée.",
"en": "no IP, skipped.",
},
"No VM to install.": {
"fr": "Aucune VM à installer.",
"en": "No VM to install.",
},
"Opening the interactive monitor...": {
"fr": "Ouverture du suivi interactif...",
"en": "Opening the interactive monitor...",
},
"Monitor closed. Installs keep running in the background.": {
"fr": "Suivi fermé. Les installations continuent en arrière-plan.",
"en": "Monitor closed. Installs keep running in the background.",
},
"Logs:": {
"fr": "Logs :",
"en": "Logs:",
},
"Read the logs:": {
"fr": "Lire les logs :",
"en": "Read the logs:",
},
"Log files:": {
"fr": "Fichiers de log :",
"en": "Log files:",
},
"Install textual for the dashboard (pip).": {
"fr": "Installez textual pour le dashboard (pip).",
"en": "Install textual for the dashboard (pip).",
},
"Parallel deployments (default:": { "Parallel deployments (default:": {
"fr": "Déploiements en parallèle (défaut :", "fr": "Déploiements en parallèle (défaut :",
"en": "Parallel deployments (default:", "en": "Parallel deployments (default:",
@ -1660,17 +1359,9 @@ TRANSLATIONS = {
"fr": "vCPU (défaut : 2) : ", "fr": "vCPU (défaut : 2) : ",
"en": "vCPUs (default: 2): ", "en": "vCPUs (default: 2): ",
}, },
"Disk size (blank = version min; e.g. 30G, 1T): ": { "Disk size (blank = version minimum): ": {
"fr": "Taille du disque (vide = min. version ; ex. 30G, 1T) : ", "fr": "Taille du disque (vide = minimum de la version) : ",
"en": "Disk size (blank = version min; e.g. 30G, 1T): ", "en": "Disk size (blank = version minimum): ",
},
"VM name (default: ": {
"fr": "Nom de la VM (défaut : ",
"en": "VM name (default: ",
},
"Deployment failed (see the error above).": {
"fr": "Échec du déploiement (voir l'erreur ci-dessus).",
"en": "Deployment failed (see the error above).",
}, },
"SSH public key path": { "SSH public key path": {
"fr": "Chemin de la clé publique SSH", "fr": "Chemin de la clé publique SSH",