erplibre/conf/nixos/erplibre.nix
Mathieu Benoit 42e10bcced [UPD] install : le venv d'outillage passe en Python 3.14.7
install_erplibre.sh bâtissait .venv.erplibre avec le python3 du système, quelle
que soit sa version : un 3.10 y donnait un venv que l'outillage ne sait pas
charger. Il délègue à install_venv.sh, qui obtient la version par mise, pyenv
ou la distribution, et rebâtit un venv hors service. Le PATCH ne borne que le
venv d'Odoo, dont le pyproject exige « >=3.12.10,<3.13 » : l'exiger ailleurs
écartait un Python de distribution d'un cran en retard — NixOS 25.11 livre
3.14.2 — et faisait compiler CPython pour rien. Le module NixOS déclare
désormais les DEUX Python, substitués depuis les fichiers de version.
Vérifié : 15 tests sur des venvs factices, conservés, rebâtis ou refusés selon
leur état, et un chemin sans pyvenv.cfg garde son contenu.

--- EN ---

install_erplibre.sh built .venv.erplibre with the system python3, whatever its
version: a 3.10 gave a venv the tooling cannot load. It delegates to
install_venv.sh, which obtains the version through mise, pyenv or the
distribution, and rebuilds a venv out of service. The PATCH bounds only Odoo's
venv, whose pyproject requires ">=3.12.10,<3.13": requiring it elsewhere turned
away a distribution Python one step behind — NixOS 25.11 ships 3.14.2 — and
compiled CPython for nothing. The NixOS module now declares BOTH Pythons,
substituted from the version files.
Checked: 15 tests on stub venvs, kept, rebuilt or refused by their state, and a
path without pyvenv.cfg keeps its content.

Assisted-by: Claude Opus 5
2026-09-24 13:56:57 -04:00

372 lines
17 KiB
Nix

# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
#
# Dépendances système d'ERPLibre, pour NixOS.
#
# Le pendant DÉCLARATIF des quatre install_<distro>_dependency.sh. Ceux-là
# posent des paquets par apt, dnf, pacman ou zypper ; ici rien ne s'installe
# par une commande — ce qui doit rester se déclare, et ce fichier est cette
# déclaration. Un « nixos-rebuild switch » l'applique ; ce qui aurait été posé
# à la main disparaîtrait à la reconstruction suivante.
#
# Déposé en /etc/nixos/erplibre.nix par script/install/install_nixos_dependency.sh,
# qui l'ajoute aussi aux « imports » de la configuration de la machine.
#
# DEUX options portent tout le reste, et sans elles rien de ce dépôt ne
# fonctionne sur NixOS :
#
# services.envfs.enable — NixOS ne peuple pas /bin ni /usr/bin. Or le
# Makefile force « SHELL := /bin/bash » (toute cible make échouerait avant
# sa première ligne) et lib_python_provider.sh ne cherche l'interpréteur du
# système qu'en /usr/bin/pythonX.Y, jamais dans le PATH. envfs fabrique ces
# deux répertoires à la volée depuis le PATH : les scripts du dépôt marchent
# alors sans être réécrits.
#
# programs.nix-ld.enable — les binaires téléchargés (roues manylinux de pip,
# CPython précompilé de mise, archives amont) sont liés à
# /lib64/ld-linux-x86-64.so.2, qui n'existe pas ici. nix-ld le fournit et
# leur donne les bibliothèques listées plus bas.
{ config, lib, pkgs, ... }:
{
# ── Ce que /bin, /usr/bin et l'éditeur de liens doivent porter ──────────
services.envfs.enable = true;
programs.nix-ld.enable = true;
# Les bibliothèques que réclament les roues manylinux d'Odoo : psycopg2 veut
# libpq, lxml veut libxml2/libxslt, cryptography veut openssl, pillow veut
# zlib et freetype. Une roue qui n'en trouve pas une échoue à l'IMPORT, pas
# à l'installation — donc bien après, et sans rapport apparent.
programs.nix-ld.libraries = with pkgs; [
stdenv.cc.cc.lib
zlib
openssl
libxml2
libxslt
libffi
libpq
freetype
fontconfig
libjpeg
glib
expat
bzip2
xz
ncurses
readline
sqlite
util-linux
openldap
cyrus_sasl
cups
libmysqlclient
];
# ── La base de données ───────────────────────────────────────────────────
# Le compte de service est SUPERUSER comme sur les autres distributions :
# ERPLibre crée et détruit des bases (migrations, copies neutralisées), ce
# qu'un rôle ordinaire ne peut pas faire.
services.postgresql = {
enable = true;
# PostGIS comme les quatre autres scripts de distribution le posent. Ici
# c'est une extension DU SERVEUR, pas un paquet du système : déclarée
# ailleurs, elle ne serait pas chargeable par « CREATE EXTENSION ».
extensions = ps: with ps; [ postgis ];
ensureUsers = [
{
name = "@EL_USER@";
ensureClauses.superuser = true;
ensureClauses.createdb = true;
ensureClauses.login = true;
}
];
};
# ── Les outils ───────────────────────────────────────────────────────────
# Les DEUX Python du dépôt, substitués depuis ses fichiers de version :
# celui d'Odoo (.python-odoo-version) et celui de l'outillage
# (conf/python-erplibre-version), qui ne sont plus la même version. envfs
# les rend visibles en /usr/bin/python3.X, où lib_python_provider.sh les
# cherche — ni mise ni pyenv n'ont alors rien à compiler. Déclarer le seul
# Python d'Odoo laissait pyenv bâtir l'autre, et la compilation de CPython
# s'arrête ici sur « Modules/_cursesmodule.o ».
#
# Le second marqueur est VIDE quand les deux versions coïncident : nommer
# deux fois le même paquet ferait entrer en collision deux chemins
# identiques dans le profil.
#
# Les sorties « .dev » portent les en-têtes : sans elles, une roue absente
# du dépôt amont devrait se compiler et ne trouverait ni libpq-fe.h ni
# openssl/ssl.h. Elles ne servent qu'à ce cas, et ne coûtent que du disque.
environment.systemPackages = with pkgs; [
@EL_PY_ODOO_PKG@
@EL_PY_ODOO_PKG@Packages.pip
@EL_PY_ODOO_PKG@Packages.virtualenv
@EL_PY_TOOLS_PKG@
uv
nodejs_22
postgresql
postgresql.dev
git
gnumake
gcc
pkg-config
openssl
openssl.dev
zlib
zlib.dev
libxml2
libxml2.dev
libxslt
libxslt.dev
libffi
libffi.dev
# python-ldap n'a PAS de roue amont : il compile, et réclame lber.h
# (openldap) plus sasl.h. Sans ces sorties « .dev », « poetry install »
# s'arrête sur « fatal error: lber.h: No such file or directory ».
openldap
openldap.dev
cyrus_sasl
cyrus_sasl.dev
# « pg_config » est une dérivation à PART dans nixpkgs : il n'est ni dans
# postgresql ni dans sa sortie « .dev », et psycopg2 s'arrête sur
# « Error: pg_config executable not found ».
postgresql.pg_config
# pycups veut cups/http.h, mysqlclient veut mysql.h. Les quatre autres
# scripts posent les mêmes (libcups2-dev, cups-devel,
# mariadb-connector-c-devel, mariadb-libs).
cups
# « cups.lib » porte libcups.so, que « out » n'a pas : sans elle la
# compilation de pycups PASSE et l'édition de liens échoue sur « -lcups ».
cups.lib
cups.dev
libmysqlclient
libmysqlclient.dev
# « less » est le PAGINATEUR ; « nodePackages.less » est lessc, le
# compilateur LESS des assets Odoo. Les quatre autres scripts les posent
# tous deux, l'un par le gestionnaire du système et l'autre par « npm
# install -g » — geste impossible ici, le préfixe npm étant le store, en
# lecture seule. nixpkgs les porte : ils se déclarent comme le reste.
less
nodePackages.less
nodePackages.rtlcss
sshpass
curl
wget
unzip
wkhtmltopdf
# « xmlsec » pose bin/xmlsec1, que les CINQ autres plateformes posent
# aussi. Le manifeste d'auth_saml (OCA server-auth, présent dans
# l'addons_path) déclare « bin: ["xmlsec1"] », et Odoo REFUSE d'installer
# ou de mettre à niveau le module tant que le binaire n'est pas dans le
# PATH : « Unable to find 'xmlsec1' in path », en boîte de dialogue.
xmlsec
# « parallel » et « shfmt » sont appelés par leur NOM NU, l'un par
# script/database/db_drop_all.py, l'autre par script/maintenance/
# format_bash.sh. Sans parallel, « make db_drop_all » annonce des bases
# détruites qui ne l'ont pas été — une opération destructrice qui rend un
# succès qu'elle n'a pas obtenu.
parallel
shfmt
# « cloud-utils » porte growpart, qu'aucune autre voie ne fournit ici.
# L'agrandissement du disque s'écrit « sudo growpart … || true » : sans le
# binaire, il rend 0 sans rien agrandir, et la VM garde la taille de son
# image pendant que le déploiement annonce la taille demandée.
cloud-utils
];
# Le profil du système ne porte PAS « /include » : la liste par défaut de
# ce qui y est lié ne contient ni les en-têtes ni les fichiers pkg-config.
# Sans cette ligne, déclarer une sortie « .dev » ne met rien nulle part, et
# « fatal error: lber.h: No such file or directory » reste entier.
environment.pathsToLink = [ "/include" "/lib/pkgconfig" ];
# Les manuels HTML, et EUX SEULS, sont écartés.
#
# NixOS installe la sortie « doc » de CHAQUE paquet du système
# (environment.extraOutputsToInstall vaut « man info doc »). Celle de
# CPython n'est pas dans le cache binaire : le premier « nixos-rebuild »
# la BÂTIT — un Sphinx qui lit puis écrit 3 000 pages. Sur une VM de 4 Go
# et 4 cœurs, cela domine le temps d'installation ; sur une de 2 Go, la
# machine cesse de répondre pendant la construction.
#
# « documentation.doc » et non « documentation » : les pages de manuel et
# info restent, elles se lisent depuis un terminal et ne coûtent rien.
documentation.doc.enable = false;
# « sessionVariables » et NON « variables » : la seconde n'écrit que dans
# /etc/set-environment, que seul un shell de CONNEXION lit. Or le
# déploiement installe par « ssh hôte 'commande' », qui n'en est pas un —
# la variable y serait vide. sessionVariables passe par pam_env, que toute
# session traverse, y compris celle-là.
environment.sessionVariables = {
CPATH = "/run/current-system/sw/include";
LIBRARY_PATH = "/run/current-system/sw/lib";
PKG_CONFIG_PATH = "/run/current-system/sw/lib/pkgconfig";
} // lib.optionalAttrs ("@EL_CA_BUNDLE@" != "") {
# L'autorité du cache de téléchargement, pour TOUTE session d'après
# l'installation — un « git pull » échouerait sinon sur un certificat
# qu'il ne reconnaît pas, comme le clone avant elle.
#
# La commande d'installation porte ces mêmes variables elle-même : elle
# tourne AVANT la première reconstruction, et aucun chemin PAM n'est
# inscriptible d'ici là. Les deux moitiés se relaient.
#
# « optionalAttrs » et non une valeur de repli : sur une machine sans
# cache le faisceau n'existe pas, et y pointer SSL_CERT_FILE couperait
# TLS partout.
SSL_CERT_FILE = "@EL_CA_BUNDLE@";
CURL_CA_BUNDLE = "@EL_CA_BUNDLE@";
GIT_SSL_CAINFO = "@EL_CA_BUNDLE@";
REQUESTS_CA_BUNDLE = "@EL_CA_BUNDLE@";
NODE_EXTRA_CA_CERTS = "@EL_CA_BUNDLE@";
PIP_CERT = "@EL_CA_BUNDLE@";
};
# Les réglages régionaux demandés au déploiement.
#
# Le FUSEAU marche déjà sans cela : cloud-init pose /etc/localtime, que
# NixOS laisse mutable tant que « time.timeZone » n'est pas déclaré.
# Mesuré sur une VM installée — le lien pointe bien la zone demandée. Le
# déclarer ne répare donc rien ; il fait passer la garantie du côté du
# module, comme pour l'agent invité.
#
# La LOCALE, elle, ne marchait pas : cloud-init l'applique par locale-gen
# et update-locale, qui n'existent pas ici, et la VM gardait le défaut de
# NixOS. Mesuré : « fr_CA.UTF-8 » demandé, « en_US.UTF-8 » obtenu.
#
# « mkIf » plutôt qu'une valeur de repli : sur une machine où rien n'a été
# demandé — une NixOS que l'on avait déjà, installée par « --hote » —
# l'option n'est PAS définie, et le réglage de son propriétaire reste.
# Écrire un défaut ici l'écraserait sans le dire.
#
# CE QUE CELA COÛTE, mesuré : une locale autre que celle du défaut change
# l'ensemble des locales prises en charge, donc la dérivation de
# glibc-locales, qui n'est alors pas dans le cache binaire et se BÂTIT. La
# première reconstruction est longue, et silencieuse — il vaut mieux le
# savoir que de la prendre pour un blocage.
i18n.defaultLocale = lib.mkIf ("@EL_LOCALE@" != "") "@EL_LOCALE@";
time.timeZone = lib.mkIf ("@EL_TZ@" != "") "@EL_TZ@";
# Le guide de connexion, AFFICHÉ.
#
# Le déploiement écrit /etc/motd dans toutes les distributions, et compte
# sur pam_motd pour le montrer — c'est vrai des quatre images cloud, où
# sshd est en « PrintMotd no » et où ajouter l'inverse afficherait le guide
# DEUX FOIS. Ici, ni l'un ni l'autre : mesuré sur une VM installée, sshd
# rend « printmotd no » et /etc/pam.d/sshd ne contient AUCUN pam_motd. Le
# fichier est donc écrit, complet, et personne ne le lit.
#
# sshd et non pam_motd : le double affichage qu'on redoute ailleurs ne peut
# pas se produire tant que le PAM d'ici n'en contient pas, et c'est le seul
# des deux qui ne demande rien de plus que cette ligne.
services.openssh.settings.PrintMotd = true;
# L'agent invité, DÉCLARÉ ici plutôt que reçu de l'image.
#
# L'image épinglée l'active déjà (son configuration.nix porte la ligne), et
# c'est précisément le problème : la garantie appartient alors au tiers qui
# rebâtit l'image, pas au dépôt. Les quatre autres distributions reçoivent
# l'agent par le runcmd du déploiement, qui appelle leur gestionnaire de
# paquets — geste impossible sur un système déclaratif. Ici, c'est cette
# ligne ou rien.
#
# Sans agent, la voie Proxmox perd sa source PRIMAIRE d'adresse et retombe
# sur « ip neigh ». Déclarer l'option deux fois est sans effet : une option
# booléenne ne rompt l'évaluation que sur des valeurs DIFFÉRENTES. Hors
# QEMU, l'unité ne démarre pas — elle n'a pas de section [Install] et
# attend une règle udev sur le port virtio.
services.qemuGuest.enable = true;
# Ce que l'agent doit trouver dans son PATH.
#
# Une unité systemd n'hérite pas du profil du système : celle-ci ne porte
# que coreutils, findutils, grep, sed et systemd. Or « guest-exec » exécute
# les commandes du produit DANS ce PATH, et l'agrandissement du disque
# commence par « findmnt -no SOURCE / » — absent, donc code 127 dès la
# première ligne.
systemd.services.qemu-guest-agent.path = with pkgs; [
util-linux
e2fsprogs
cloud-utils
];
# Le port d'Odoo, OUVERT.
#
# NixOS active un pare-feu par défaut ; aucune des images cloud des quatre
# autres distributions n'en active un. Le service écoute bien sur
# 0.0.0.0:8069 et répond en local, mais l'extérieur ne reçoit RIEN — pas un
# refus, un silence, donc une attente jusqu'au délai. Ce qui sonde depuis
# l'hôte conclut « Odoo absent » sur une machine où il tourne, et le journal
# de l'installation ne porte aucune trace de la cause : elle est dans le
# pare-feu, pas dans l'application.
#
# 8069 SEUL. Le port websocket est configuré à 8072, mais Odoo ne le lie
# qu'en mode multi-processus, que cette configuration n'emploie pas : rien
# n'y écoute, et l'ouvrir donnerait un port béant sans service derrière.
# PostgreSQL n'écoute déjà que sur la boucle locale et n'a rien à ouvrir.
networking.firewall.allowedTCPPorts = [ 8069 ];
# Le service ERPLibre, DÉCLARÉ et non écrit.
#
# Sur toute autre distribution l'installation dépose l'unité par
# « tee /etc/systemd/system/erplibre.service ». Ici /etc est généré depuis
# le store et monté en lecture seule : le tee échoue sur « Read-only file
# system », et l'installation entière rend 1 à sa dernière étape, après que
# tout le reste a réussi.
#
# « wantedBy » est l'équivalent déclaratif de « systemctl enable » :
# l'activation par lien symbolique écrirait elle aussi dans /etc.
#
# L'interpréteur vient du STORE et non de /bin. /bin et /usr/bin sont ici
# un montage FUSE d'envfs, et systemd résout l'exécutable d'ExecStart
# lui-même, hors de portée de ce montage : « /bin/bash » y rend
# « 203/EXEC, Unable to locate executable ». Avec Restart=always, l'unité
# boucle alors indéfiniment. La même raison vaut pour /usr/bin/env, donc
# le shebang de run.sh ne suffirait pas davantage.
#
# La première reconstruction déclare le service AVANT qu'ERPLibre ne soit
# installé, et son démarrage échoue alors — c'est attendu, et c'est
# exactement le cas que « nixos-rebuild rend 4 » recouvre. L'installation
# le relance une fois le dépôt en place.
systemd.services.erplibre = {
description = "ERPLibre";
requires = [ "postgresql.service" ];
after = [ "network.target" "network-online.target" "postgresql.service" ];
wantedBy = [ "multi-user.target" ];
# Une unité systemd ne reçoit PAS le PATH d'une session : le sien ne
# porte que coreutils, findutils, grep, sed et systemd.
#
# bash — run.sh lance odoo_bin.sh et lib_db_select.sh, dont le shebang est
# « #!/usr/bin/env bash ». env est là, bash non : « env: 'bash': No such
# file or directory », et run.sh s'arrête avant Odoo.
#
# python3 — la sonde de réveil tourne AVANT que odoo_bin.sh n'active le
# venv, donc avec le python du système. Son échec est silencieux
# (« 2>/dev/null ») : sans elle, la première page ouverte attendrait le
# chargement du registre sans que rien ne le dise.
path = with pkgs; [ bash @EL_PY_ODOO_PKG@ ];
# L'unité est déclarée par le module, donc démarrée par la
# reconstruction — qui a lieu PENDANT « make install_os », alors que la
# source d'Odoo n'arrive qu'à « make install_odoo_18 ». Sans condition,
# run.sh échoue sur un odoo-bin absent et « Restart = always » le rejoue
# toutes les cinq secondes jusqu'à ce que l'installation le pose — vingt
# et un échecs mesurés sur une pose ordinaire, et un « nixos-rebuild »
# qui rend 4 parce qu'une unité n'a pas démarré.
#
# Une CONDITION, et non une dépendance : systemd saute l'unité en le
# disant une fois, sans la marquer en échec, et la démarre d'elle-même
# au prochain déclenchement une fois le fichier là. Le motif évite de
# figer la version d'Odoo ici, où elle vieillirait en silence.
unitConfig.ConditionPathExistsGlob = "@EL_DIR@/odoo*/odoo/odoo-bin";
serviceConfig = {
Type = "simple";
User = "@EL_USER@";
WorkingDirectory = "@EL_DIR@";
ExecStart = "${pkgs.bash}/bin/bash @EL_DIR@/run.sh";
Restart = "always";
RestartSec = 5;
StandardOutput = "journal+console";
};
};
}