# © 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__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"; }; }; }