From 62f3e62e83b9985aa5e481c3f903183e5d7b0e13 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 02:56:18 -0400 Subject: [PATCH] [FIX] install s390x: the compiler was killed for lack of memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- script/install/install_debian_dependency.sh | 6 + script/install/install_fedora_dependency.sh | 7 ++ script/install/install_locally.sh | 10 ++ script/install/install_suse_dependency.sh | 7 ++ script/install/lib_lowmem.sh | 123 ++++++++++++++++++++ 5 files changed, 153 insertions(+) create mode 100644 script/install/lib_lowmem.sh diff --git a/script/install/install_debian_dependency.sh b/script/install/install_debian_dependency.sh index d8cb64f..61e759b 100755 --- a/script/install/install_debian_dependency.sh +++ b/script/install/install_debian_dependency.sh @@ -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. diff --git a/script/install/install_fedora_dependency.sh b/script/install/install_fedora_dependency.sh index b60d38d..eb571e4 100755 --- a/script/install/install_fedora_dependency.sh +++ b/script/install/install_fedora_dependency.sh @@ -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). diff --git a/script/install/install_locally.sh b/script/install/install_locally.sh index fe71f79..d73d13d 100755 --- a/script/install/install_locally.sh +++ b/script/install/install_locally.sh @@ -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 diff --git a/script/install/install_suse_dependency.sh b/script/install/install_suse_dependency.sh index 8170bf5..40eeed8 100755 --- a/script/install/install_suse_dependency.sh +++ b/script/install/install_suse_dependency.sh @@ -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 diff --git a/script/install/lib_lowmem.sh b/script/install/lib_lowmem.sh new file mode 100644 index 0000000..00a3eda --- /dev/null +++ b/script/install/lib_lowmem.sh @@ -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)." +}