[FIX] install s390x: the compiler was killed for lack of memory

matplotlib dies on "c++: fatal error: Killed signal terminated program
cc1plus". "Killed" is a SIGKILL: the kernel's OOM killer, and nothing in
the message names memory. A single matplotlib source file asks cc1plus
for up to 2.5 GiB.

Two causes compound. The VM is small -- the catalogue starts at 1 or
2 GiB -- and every build backend launches nproc compilations in
parallel, each with its own cc1plus. Six cores exhaust 8 GiB.

Hence two answers. A swap file tops memory up to 8 GiB, never taking
more than half the free disk nor touching fstab -- a declared and
missing swap degrades boot. And parallelism is bounded by actual
memory, 2 GiB per task.

ninja has no parallelism environment variable, but meson-python reads
"NINJA", the executable PATH -- verified in mesonpy 0.20. We point it at
a wrapper that adds the -j. That is what saves matplotlib; MAKEFLAGS and
CMAKE_BUILD_PARALLEL_LEVEL cover the rest.

--- FR ---

matplotlib s'arrête sur « c++: fatal error: Killed signal terminated
program cc1plus ». « Killed » est un SIGKILL : c'est le tueur du noyau,
et rien dans le message ne nomme la mémoire. Un seul fichier de
matplotlib demande jusqu'à 2,5 Gio à cc1plus.

Deux causes se cumulent. La VM est petite — le catalogue démarre à 1 ou
2 Gio — et chaque moteur de build lance nproc compilations en parallèle,
chacune avec son cc1plus. Six cœurs épuisent 8 Gio.

D'où deux réponses. Un fichier d'échange complète la mémoire jusqu'à
8 Gio, sans jamais prendre plus de la moitié du disque libre ni toucher
à fstab — un swap déclaré et disparu dégrade le démarrage. Et le
parallélisme est borné d'après la mémoire réelle, 2 Gio par tâche.

ninja n'a aucune variable de parallélisme, mais meson-python lit
« NINJA », le CHEMIN de l'exécutable — vérifié dans mesonpy 0.20. On y
met une enveloppe qui ajoute le -j. C'est ce qui sauve matplotlib ;
MAKEFLAGS et CMAKE_BUILD_PARALLEL_LEVEL couvrent les autres.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-12 02:56:18 -04:00
parent d6221ee48d
commit 62f3e62e83
5 changed files with 153 additions and 0 deletions

View file

@ -2,6 +2,7 @@
. ./env_var.sh
. ./script/install/lib_qpdf.sh
. ./script/install/lib_lowmem.sh
EL_USER=${USER}
#EL_INSTALL_WKHTMLTOPDF="True"
@ -72,6 +73,11 @@ fi
#--------------------------------------------------
if [ "$(uname -m)" = "s390x" ]; then
echo "Arch s390x detected"
# La mémoire est la ressource qui manque en premier ici : cc1plus demande
# jusqu'à 2,5 Gio pour un seul fichier de matplotlib, et le tueur du noyau
# abrège sans jamais nommer la mémoire (« Killed signal terminated program
# cc1plus »). On complète par du swap avant d'en arriver là.
el_swap_ensure
# Sur une VM fraîche, l'index apt peut être vide : « apt install » échouerait
# alors sur TOUT le lot, et l'absence d'un seul paquet ne se voit que bien
# plus loin, sous la forme d'une commande introuvable.

View file

@ -11,6 +11,7 @@
. ./env_var.sh
. ./script/install/lib_qpdf.sh
. ./script/install/lib_lowmem.sh
EL_USER=${USER}
@ -100,6 +101,12 @@ sudo dnf group install -y ${DNF_SKIP} development c-development \
# « The headers or library files could not be found for jpeg » pour pillow.
if [ "$(uname -m)" = "s390x" ]; then
echo -e "\n---- Dependances de compilation s390x ----"
# La mémoire est la ressource qui manque en premier ici : cc1plus
# demande jusqu'à 2,5 Gio pour un seul fichier de matplotlib, et le
# tueur du noyau abrège sans jamais nommer la mémoire (« Killed signal
# terminated program cc1plus »). On complète par du swap avant d'en
# arriver là.
el_swap_ensure
# Best-effort : un nom qui change d'une version à l'autre ne doit pas
# emporter le lot. Ce qui est vraiment indispensable est déjà installé
# au-dessus (compilateurs) ou plus bas (pyenv, PostgreSQL).

View file

@ -28,6 +28,8 @@ POETRY_ODOO_PATH=${VENV_ODOO_PATH}/bin/poetry
# Choix pip/uv : un seul endroit décide, comme pour mise/pyenv.
# shellcheck source=script/install/lib_pip_provider.sh
. ./script/install/lib_pip_provider.sh
# Bornage des compilations : voir el_build_limit_jobs.
. ./script/install/lib_lowmem.sh
export WITH_POETRY_INSTALLATION=1
# Verbosité de l'installation Poetry. « -q » a été retiré du défaut : dans Cleo,
@ -153,6 +155,14 @@ if [[ "${EL_PHASE}" != "setup" ]]; then
echo "Poetry introuvable a ${POETRY_ODOO_PATH}, arret."
exit 1
fi
# Là où rien n'a de roue, « poetry install » COMPILE des centaines de
# paquets, et le moteur de build de chacun lance nproc tâches en
# parallèle. cc1plus demande jusqu'à 2,5 Gio pour un seul fichier de
# matplotlib : six cœurs épuisent 8 Gio, et le noyau tue le compilateur
# sans jamais nommer la mémoire. On borne d'après la mémoire disponible.
if [[ "$(uname -m)" == "s390x" ]]; then
el_build_limit_jobs
fi
"${POETRY_ODOO_PATH}" --version
# To fix keyring problem when installation is blocked, use
export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring

View file

@ -15,6 +15,7 @@
. ./env_var.sh
. ./script/install/lib_qpdf.sh
. ./script/install/lib_lowmem.sh
EL_USER=${USER}
@ -224,6 +225,12 @@ echo -e "\n--- Installing suse dependency --"
# compiler ». Tumbleweed livre 1.94, bien au-dessus du 1.78 qu'exige le
# Cargo.lock v4 de cryptography. On évite ainsi ~200 Mo inutiles sur amd64.
if [ "$(uname -m)" = "s390x" ]; then
# La mémoire est la ressource qui manque en premier ici : cc1plus
# demande jusqu'à 2,5 Gio pour un seul fichier de matplotlib, et le
# tueur du noyau abrège sans jamais nommer la mémoire (« Killed signal
# terminated program cc1plus »). On complète par du swap avant d'en
# arriver là.
el_swap_ensure
zyp_soft rust cargo
if ! command -v cargo > /dev/null 2>&1; then
# FATAL, et non plus un simple avertissement : sur s390x, bcrypt et

View file

@ -0,0 +1,123 @@
#!/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 : compiler sans se faire tuer par manque de mémoire.
#
# Le symptôme est toujours le même, et il ne nomme jamais la mémoire :
# c++: fatal error: Killed signal terminated program cc1plus
# « Killed » est un SIGKILL, donc le tueur de mémoire du noyau. Il est arrivé
# sur matplotlib, dont _backend_agg_wrapper.cpp instancie assez de gabarits
# C++ pour demander 1,5 à 2,5 Gio à cc1plus — À LUI SEUL.
#
# Deux causes se cumulent, et il faut les traiter toutes les deux :
# 1. la VM est petite (le catalogue démarre à 1 ou 2 Gio) ;
# 2. le moteur de build lance nproc compilations EN PARALLÈLE, chacune avec
# son propre cc1plus. Six cœurs suffisent à épuiser 8 Gio.
#
# Réservé aux architectures sans roue PyPI par l'appelant : ailleurs pip pose
# des binaires et rien de tout cela ne s'exécute.
# RAM + swap visés, en Gio. « 0 » désactive entièrement la création de swap,
# pour qui préfère dimensionner sa machine lui-même.
EL_SWAP_TARGET_GIB="${EL_SWAP_TARGET_GIB:-8}"
EL_SWAP_FILE="${EL_SWAP_FILE:-/swapfile.erplibre}"
# Mio de mémoire à réserver par compilation simultanée. 2048 vient du pic
# mesuré de cc1plus sur matplotlib ; en dessous, le tueur revient.
EL_MEM_PER_JOB_MB="${EL_MEM_PER_JOB_MB:-2048}"
el_mem_mb() {
awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo 2> /dev/null || echo 0
}
el_swap_mb() {
awk '/^SwapTotal:/ {print int($2 / 1024)}' /proc/meminfo 2> /dev/null || echo 0
}
# Complète la mémoire par un fichier d'échange, si elle manque.
#
# Volontairement NON persisté dans /etc/fstab : un swap déclaré et disparu
# dégrade le démarrage, et cette machine n'en a besoin que le temps de
# l'installation. La fonction est idempotente, elle repassera au besoin.
el_swap_ensure() {
local ram swap missing free_mb size
[ "${EL_SWAP_TARGET_GIB}" -gt 0 ] 2> /dev/null || return 0
ram="$(el_mem_mb)"
swap="$(el_swap_mb)"
missing=$((EL_SWAP_TARGET_GIB * 1024 - ram - swap))
if [ "${missing}" -le 0 ]; then
echo " memoire : ${ram} Mio + ${swap} Mio de swap, suffisant."
return 0
fi
if [ -e "${EL_SWAP_FILE}" ]; then
# Déjà là mais pas actif : un « swapon » suffit, on ne le réécrit pas.
sudo swapon "${EL_SWAP_FILE}" 2> /dev/null && {
echo " swap : ${EL_SWAP_FILE} reactive."
return 0
}
fi
# Ne jamais remplir le disque pour gagner de la mémoire : au plus la moitié
# de ce qui reste libre. Un disque plein casse l'installation autrement.
free_mb="$(df -Pm / | awk 'NR==2 {print $4}')"
size="${missing}"
[ "${size}" -gt $((free_mb / 2)) ] && size=$((free_mb / 2))
if [ "${size}" -lt 512 ]; then
echo " swap : pas assez d'espace disque libre (${free_mb} Mio), abandon." >&2
return 0
fi
echo " memoire : ${ram} Mio seulement — ajout de ${size} Mio de swap."
# fallocate est instantané ; dd est le repli des systèmes de fichiers qui
# ne le supportent pas (et un swap sur trous ne fonctionnerait pas).
if ! sudo fallocate -l "${size}M" "${EL_SWAP_FILE}" 2> /dev/null; then
sudo dd if=/dev/zero of="${EL_SWAP_FILE}" bs=1M count="${size}" \
status=none 2> /dev/null || {
echo " swap : creation impossible, on continue sans." >&2
return 0
}
fi
sudo chmod 600 "${EL_SWAP_FILE}"
if sudo mkswap "${EL_SWAP_FILE}" > /dev/null 2>&1 \
&& sudo swapon "${EL_SWAP_FILE}"; then
echo " swap : ${size} Mio actifs (${EL_SWAP_FILE})."
else
echo " swap : activation impossible, on continue sans." >&2
sudo rm -f "${EL_SWAP_FILE}"
fi
}
# Nombre de compilations simultanées que la mémoire tolère.
el_build_jobs() {
local total cpus jobs
total=$(($(el_mem_mb) + $(el_swap_mb)))
cpus="$(nproc 2> /dev/null || echo 1)"
jobs=$((total / EL_MEM_PER_JOB_MB))
[ "${jobs}" -lt 1 ] && jobs=1
[ "${jobs}" -gt "${cpus}" ] && jobs="${cpus}"
echo "${jobs}"
}
# Exporte de quoi brider les moteurs de build. À appeler AVANT « poetry
# install », dans le shell qui le lance.
#
# make MAKEFLAGS, lu par tout Makefile.
# cmake CMAKE_BUILD_PARALLEL_LEVEL, lu par « cmake --build » (manifold3d).
# ninja aucune variable de parallélisme n'existe — mais meson-python lit
# « NINJA », le CHEMIN de l'exécutable (vérifié dans mesonpy 0.20 :
# « env_ninja = os.environ.get('NINJA') »). On y met une enveloppe
# qui ajoute le -j. C'est ce qui sauve matplotlib.
# MAX_JOBS convention de PyTorch et de quelques autres.
el_build_limit_jobs() {
local jobs real wrapper
jobs="$(el_build_jobs)"
export MAKEFLAGS="-j${jobs}"
export CMAKE_BUILD_PARALLEL_LEVEL="${jobs}"
export MAX_JOBS="${jobs}"
real="$(command -v ninja || command -v ninja-build || true)"
if [ -n "${real}" ]; then
wrapper="${TMPDIR:-/tmp}/el-ninja-j${jobs}"
printf '#!/bin/sh\nexec %s -j%s "$@"\n' "${real}" "${jobs}" > "${wrapper}"
chmod +x "${wrapper}"
export NINJA="${wrapper}"
fi
echo " compilation bridee a ${jobs} tache(s) simultanee(s)."
}