[ADD] run: make and ./run.sh, Electron start check, container matrix

make with no target does what ./run.sh does: Electron on a graphical
session, Vite's server without one or where Electron is not published
(s390x, ppc64le). Every npm, npx and node recipe goes through the
project's Node. make essai_demarrage starts the real Electron through
Playwright and reads its window title, on Chromium's headless Ozone
platform when there is no display: no X server is needed.
scripts/verifier_systemes.sh replays install and tests in podman over
the QEMU catalogue of TODO, and stops only its own containers.

Checked: 17 Linux images and Nix pass all six steps; pwsh parses.

--- FR ---

[ADD] lancement : make et ./run.sh, essai d'Electron, matrice

make sans cible fait ce que fait ./run.sh : Electron sur une session
graphique, le serveur de Vite sans elle ou là où Electron n'est pas
publié (s390x, ppc64le). Chaque recette npm, npx ou node passe par le
Node du projet. make essai_demarrage lance la vraie coquille Electron
par Playwright et lit le titre de sa fenêtre, sur la plateforme Ozone
headless de Chromium sans écran : aucun serveur X n'est requis.
scripts/verifier_systemes.sh rejoue installation et épreuves sous
podman sur le catalogue QEMU de TODO, et n'arrête que ses conteneurs.

Vérifié : 17 images Linux et Nix passent les six étapes ; pwsh analyse.

Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
This commit is contained in:
Mathieu Benoit 2026-10-06 05:52:17 -04:00
parent d83362166a
commit b9090508ff
6 changed files with 635 additions and 8 deletions

108
Makefile Normal file
View file

@ -0,0 +1,108 @@
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
#
# Cibles du projet. Chacune délègue à un script qui porte la logique et se
# lance aussi sans make : ./run.sh, ./install.sh, ./install_dev.sh, et sous
# Windows run.cmd, install.cmd, install_dev.cmd. npm, npx et node passent par
# avec_node, qui pose le Node du projet en tête du PATH après avoir vérifié
# l'installation.
#
# make lance l'application, comme ./run.sh
# make help la liste des cibles
ifeq ($(OS),Windows_NT)
PS := powershell.exe -NoProfile -ExecutionPolicy Bypass
LANCER := $(PS) -File scripts/windows/lancer.ps1
INSTALLER := $(PS) -File scripts/windows/installer.ps1
INSTALLER_DEV := $(PS) -File scripts/windows/installer.ps1 -Dev
AVEC_NODE := $(PS) -File scripts/windows/avec_node.ps1
else
# bash, où qu'il soit : /bin/bash n'existe pas sous NixOS, et make s'y
# arrêterait avant la première recette.
SHELL := $(shell command -v bash 2>/dev/null || echo /bin/bash)
LANCER := ./run.sh
INSTALLER := ./install.sh
INSTALLER_DEV := ./install_dev.sh
AVEC_NODE := scripts/installation/avec_node.sh
endif
.DEFAULT_GOAL := run
.PHONY: run
run: ## Lance l'application : Electron sur une session graphique, sinon le serveur de Vite
$(LANCER)
.PHONY: web
web: ## Sert l'application à un navigateur, http://localhost:5173
$(LANCER) --web
.PHONY: electron
electron: ## Lance la coquille Electron
$(LANCER) --electron
.PHONY: install
install: ## Installe de quoi lancer l'application et jouer les épreuves sous node
$(INSTALLER)
.PHONY: install_dev
install_dev: ## Installe aussi Chromium, des polices et podman
$(INSTALLER_DEV)
.PHONY: test
test: ## Joue la série surveillée, sous node (§ 14.4)
$(AVEC_NODE) npm test
.PHONY: test_long
test_long: ## Joue la série longue, sous node
$(AVEC_NODE) npm run test:long
.PHONY: test_navigateur
test_navigateur: ## Joue les épreuves du navigateur, sous Chromium
$(AVEC_NODE) npm run test:navigateur
.PHONY: test_tout
test_tout: ## Joue les trois séries
$(AVEC_NODE) npm run test:tout
.PHONY: build
build: ## Construit la page dans www/
$(AVEC_NODE) npm run build
.PHONY: verifier
verifier: ## Contrôle la version, le nom du livrable et le changelog (§ 18.5)
$(AVEC_NODE) node scripts/version.js controler
.PHONY: essai_demarrage
essai_demarrage: ## Lance Electron, lit le titre de sa fenêtre et ferme
$(AVEC_NODE) node scripts/essai_demarrage.js
ifeq ($(OS),Windows_NT)
.PHONY: windows
windows: ##W Construit l'exécutable Windows portable (sous Linux seulement, § 16)
@echo La construction de l'executable se fait sous Linux, en conteneur : make windows.
@exit 1
.PHONY: clean
clean: ##W Retire la page construite, www/
$(PS) -Command "Remove-Item -Recurse -Force -ErrorAction SilentlyContinue www"
.PHONY: help
help: ##W Affiche cette liste
@findstr /R /C:"^[a-z_]*:.*## " /C:"^[a-z_]*:.*##W " Makefile
else
.PHONY: windows
windows: ##U Construit l'exécutable Windows portable, sous podman (§ 16)
$(AVEC_NODE) scripts/construire_windows.sh
.PHONY: verifier_systemes
verifier_systemes: ##U Installe et éprouve sur chaque système du catalogue, en conteneurs
scripts/verifier_systemes.sh
.PHONY: clean
clean: ##U Retire la page construite, www/
rm -rf www
.PHONY: help
help: ##U Affiche cette liste
@awk 'BEGIN { FS = ":.*##U? " } /^[a-z_]+:.*##U? / { printf " make %-18s %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
endif

View file

@ -4,18 +4,52 @@ Logiciel hors ligne qui place les personnes d'un événement autour de tables et
les fait tourner sur plusieurs tours, pour que chacune rencontre le plus de
monde possible.
## Installer et lancer
| système | installer | lancer |
|---|---|---|
| Linux, macOS | `./install.sh` | `./run.sh`, ou `make` |
| Windows | `install.cmd` | `run.cmd` |
| NixOS | `./install.sh` | `./run.sh` ; `make` dans `nix-shell scripts/installation/shell.nix` |
`./install_dev.sh` (sous Windows `install_dev.cmd`) installe en plus le Chromium
des épreuves du navigateur, des polices et, sur amd64, `podman`, sous lequel se
construit l'exécutable Windows.
L'installation pose les paquets du système qui manquent — apt, dnf, zypper ou
pacman, par `sudo` —, puis Node de la majeure écrite dans `.node-version`,
téléchargé de nodejs.org et vérifié contre sa somme, hors du dépôt :
`~/.local/share/gestion_table_tournante_libre`, sous Windows
`%LOCALAPPDATA%\gestion_table_tournante_libre`. Node n'exige donc aucun droit
d'administration. Sous NixOS, `scripts/installation/shell.nix` déclare Node,
Electron et Chromium, d'un nixpkgs épinglé.
Systèmes éprouvés, en conteneurs, par `make verifier_systemes` : Ubuntu 24.04,
25.10 et 26.04, Debian 12 et 13 (Proxmox VE 9 en dérive), Fedora 41 à 44,
AlmaLinux et Rocky Linux 9 et 10, openSUSE Leap 16.0 et Tumbleweed, Arch Linux,
et Nix ; les scripts Windows sous `pwsh`.
`./run.sh` lance la coquille Electron sur une session graphique ; sans écran,
ou là où Electron n'est pas publié (s390x, ppc64le), il sert l'application à un
navigateur sur http://localhost:5173. `./run.sh --web` et `./run.sh --electron`
forcent l'un ou l'autre.
## Commandes
| commande | effet |
|---|---|
| `npm install` | installe les outils de construction et d'épreuve |
| `npx playwright install chromium` | télécharge le Chromium des épreuves navigateur, une fois par machine |
| `npm run dev` | sert l'application en développement, sur la plateforme web |
| `npm test` | série `node` : les fonctions pures, à chaque modification |
| `npm run test:long` | épreuves lourdes, avant un commit |
| `npm run test:navigateur` | épreuves de rendu dans Chromium, avant une livraison |
| `npm run build` | construit la page dans `www/` |
| `scripts/construire_windows.sh` | construit l'exécutable Windows portable, sous `podman` |
| `make` | lance l'application, comme `./run.sh` |
| `make test` | série `node` : les fonctions pures, à chaque modification |
| `make test_long` | épreuves lourdes, avant un commit |
| `make test_navigateur` | épreuves de rendu dans Chromium, avant une livraison |
| `make build` | construit la page dans `www/` |
| `make windows` | construit l'exécutable Windows portable, sous `podman`, depuis Linux |
| `make essai_demarrage` | lance la vraie coquille Electron, lit son titre et ferme |
| `make help` | toutes les cibles |
Chaque cible passe par le Node du projet. Les commandes npm d'origine restent
valables avec un Node de la bonne majeure dans le `PATH` : `npm test`,
`npm run dev`, `npm run build`.
L'exécutable porte la version datée du logiciel dans son nom, selon le gabarit
`gestion_table_tournante_libre_v<AAAA>_<MM>_<JJ>_<NN>.exe`.

67
run.sh Executable file
View file

@ -0,0 +1,67 @@
#!/usr/bin/env bash
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
# Lance l'application depuis les sources. Sur une session graphique, la
# coquille Electron charge la page que Vite construit dans www/ (§ 13.1) ;
# sans écran, ou là où Electron n'est pas publié, le serveur de
# développement de Vite la sert à un navigateur.
#
# ./run.sh Electron si possible, sinon le serveur de Vite
# ./run.sh --web le serveur de Vite, http://localhost:5173
# ./run.sh --electron Electron, ou un refus qui dit pourquoi
set -euo pipefail
cd -- "$(dirname -- "$0")"
. scripts/installation/commun.sh
mode=auto
for argument in "$@"; do
case "$argument" in
--web) mode=web ;;
--electron) mode=electron ;;
-h | --help)
sed -n '5,12p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
gtt_dire "usage : ./run.sh [--web | --electron]"
exit 2
;;
esac
done
[ "$(gtt_famille)" = windows ] && gtt_powershell lancer.ps1 "$@"
gtt_dans_nix_shell ./run.sh "$@"
gtt_preparer
if [ "$mode" = auto ]; then
if gtt_electron_disponible && gtt_session_graphique; then
mode=electron
else
mode=web
fi
fi
if [ "$mode" = web ]; then
gtt_dire "Serveur de développement de Vite : http://localhost:5173 (Ctrl+C pour l'arrêter)."
exec npm run --silent dev
fi
gtt_electron_disponible \
|| gtt_refuser "Electron n'est pas publié pour $(gtt_os_node)-$(gtt_arch_node) : ./run.sh --web."
gtt_session_graphique \
|| gtt_refuser "aucune session graphique (DISPLAY, WAYLAND_DISPLAY) : ./run.sh --web ; make essai_demarrage lance Electron sans écran."
options=()
if [ "$(gtt_os_node)" = linux ]; then
if [ "$(id -u)" = 0 ]; then
gtt_dire "Sous root, Chromium refuse son bac à sable : Electron se lance sans."
options+=(--no-sandbox)
elif ! gtt_bac_a_sable_pret; then
gtt_dire "Le bac à sable d'Electron demande chrome-sandbox setuid root (espaces de noms restreints)."
gtt_confirmer "Le poser par sudo ?" \
|| gtt_refuser "bac à sable non posé : ./install.sh le pose, puis relancer."
gtt_preparer_bac_a_sable
fi
fi
npm run --silent build
exec npx --no-install electron . ${options[@]+"${options[@]}"}

View file

@ -0,0 +1,62 @@
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Essai de démarrage : construit la page, lance la vraie coquille Electron
// par Playwright, attend sa fenêtre, lit le titre et le bandeau, puis ferme.
// Il prouve ce que la coquille factice des épreuves (test/coquille.test.js)
// ne peut pas dire : qu'Electron démarre sur ce système, avec ses
// bibliothèques, et y charge la page.
//
// node scripts/essai_demarrage.js
//
// Sous Linux sans session graphique, Electron se lance sur la plateforme
// Ozone « headless » de Chromium : la fenêtre existe et la page s'y charge
// et s'y met en page, sans serveur d'affichage. Sous root, ou avec
// GTT_SANS_BAC_A_SABLE=1 là où le système refuse les espaces de noms
// d'utilisateur — un conteneur —, Electron se lance sans son bac à sable.
// Sort à 0 sur un démarrage conforme, à 1 sinon.
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { _electron } from 'playwright';
import { VERSION } from '../src/version.genere.js';
import { titreAvecVersion } from '../src/interface/libelles.js';
const RACINE = fileURLToPath(new URL('..', import.meta.url));
const ATTENTE_MS = 60_000;
const sousRoot = typeof process.getuid === 'function' && process.getuid() === 0;
const sansBacASable = sousRoot || process.env.GTT_SANS_BAC_A_SABLE === '1';
const sansEcran = process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
// npm est un script .cmd sous Windows, que Node ne lance que par un shell.
execFileSync('npm', ['run', '--silent', 'build'], {
cwd: RACINE,
stdio: 'inherit',
shell: process.platform === 'win32',
});
const attendu = titreAvecVersion(VERSION.affichee);
const application = await _electron.launch({
args: [
...(sansBacASable ? ['--no-sandbox'] : []),
...(sansEcran ? ['--ozone-platform=headless'] : []),
'.',
],
cwd: RACINE,
timeout: ATTENTE_MS,
});
let conforme = false;
try {
const fenetre = await application.firstWindow({ timeout: ATTENTE_MS });
await fenetre.locator('header.bandeau .titre').waitFor({ timeout: ATTENTE_MS });
const titre = await fenetre.title();
const bandeau = (await fenetre.locator('header.bandeau .titre').textContent()).trim();
conforme = titre === attendu && bandeau === attendu;
console.log(`Titre de la fenêtre : ${titre}`);
console.log(`Bandeau : ${bandeau}`);
if (!conforme) console.error(`Attendu : ${attendu}`);
} finally {
await application.close();
}
console.log(conforme ? 'Démarrage conforme.' : 'Démarrage non conforme.');
process.exit(conforme ? 0 : 1);

244
scripts/verifier_systemes.sh Executable file
View file

@ -0,0 +1,244 @@
#!/usr/bin/env bash
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
# Installe et éprouve le projet sur chaque système du catalogue de machines
# virtuelles de TODO (ERPLibre), chacun dans un conteneur podman : sous un
# compte ordinaire qui passe par sudo, ./install_dev.sh, puis make test,
# make test_long, make test_navigateur, l'essai de démarrage d'Electron, et
# le serveur de Vite. Les scripts PowerShell s'éprouvent sous pwsh.
#
# scripts/verifier_systemes.sh tout le catalogue
# scripts/verifier_systemes.sh debian-13 arch ces systèmes
# scripts/verifier_systemes.sh --liste les systèmes connus
#
# Ce qu'un conteneur n'éprouve pas : le bac à sable d'Electron (pas d'espaces
# de noms d'utilisateur, Electron s'y lance sans), une autre architecture
# que celle de l'hôte, NixOS lui-même (l'image nix éprouve shell.nix, sur
# une base qui n'est pas NixOS), une vraie session graphique.
#
# GTT_PARALLELES (4 par défaut) borne les conteneurs simultanés ; les
# journaux vont dans GTT_JOURNAUX, un dossier temporaire par défaut. Les
# téléchargements — npm, Electron, Chromium, Node — se partagent entre
# conteneurs par des volumes nommés gttl-cache-*.
set -euo pipefail
racine="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
# Système, image : le catalogue de script/qemu/deploy_qemu.py d'ERPLibre.
# Proxmox VE 9 est Debian 13 sous ses paquets : debian-13 le représente.
CATALOGUE='
ubuntu-24.04 docker.io/library/ubuntu:24.04
ubuntu-25.10 docker.io/library/ubuntu:25.10
ubuntu-26.04 docker.io/library/ubuntu:26.04
debian-12 docker.io/library/debian:12
debian-13 docker.io/library/debian:13
fedora-41 docker.io/library/fedora:41
fedora-42 docker.io/library/fedora:42
fedora-43 docker.io/library/fedora:43
fedora-44 docker.io/library/fedora:44
almalinux-9 docker.io/library/almalinux:9
almalinux-10 docker.io/library/almalinux:10
rocky-9 docker.io/rockylinux/rockylinux:9
rocky-10 docker.io/rockylinux/rockylinux:10
opensuse-leap-16.0 registry.opensuse.org/opensuse/leap:16.0
opensuse-tumbleweed registry.opensuse.org/opensuse/tumbleweed:latest
arch docker.io/library/archlinux:latest
nix docker.io/nixos/nix:latest
pwsh mcr.microsoft.com/powershell:latest
'
usage() {
sed -n '9,12p' "$0" | sed 's/^# \{0,1\}//'
}
image_de() {
printf '%s\n' "$CATALOGUE" | awk -v s="$1" '$1 == s { print $2 }'
}
systemes=()
for argument in "$@"; do
case "$argument" in
--liste)
printf '%s\n' "$CATALOGUE" | awk 'NF { print $1 }'
exit 0
;;
-h | --help)
usage
exit 0
;;
-*)
usage >&2
exit 2
;;
*)
[ -n "$(image_de "$argument")" ] || { echo "système inconnu : $argument (--liste)" >&2; exit 2; }
systemes+=("$argument")
;;
esac
done
if [ ${#systemes[@]} -eq 0 ]; then
while read -r s _; do
[ -n "$s" ] && systemes+=("$s")
done <<< "$CATALOGUE"
fi
command -v podman > /dev/null || { echo "podman introuvable." >&2; exit 1; }
journaux="${GTT_JOURNAUX:-$(mktemp -d "${TMPDIR:-/tmp}/verifier_systemes.XXXXXX")}"
mkdir -p "$journaux"
# Étiquette des conteneurs de cette vérification : une interruption arrête
# ceux-là, et aucun autre conteneur de la machine.
ETIQUETTE="gttl-verification=$$"
arreter() {
podman ps -q --filter "label=$ETIQUETTE" | xargs -r podman stop -t 2 > /dev/null 2>&1 || true
}
trap 'arreter; exit 130' INT
trap 'arreter; exit 143' TERM
# Copie de l'arbre de travail tel qu'il est — fichiers suivis et nouveaux
# non ignorés —, sans node_modules : chaque conteneur part de zéro.
source_copie="$journaux/source"
rm -rf "$source_copie"
mkdir -p "$source_copie"
(cd "$racine" && git ls-files -co --exclude-standard -z | tar --null -cf - -T - | tar -xf - -C "$source_copie")
# Préparation sous root, par famille : sudo et un compte ordinaire « dev »
# qui l'emploie sans mot de passe, comme un poste de développement. Son uid
# est le même dans chaque conteneur — 1500, libre aussi sous Ubuntu, dont
# l'image donne 1000 à son propre compte — : les caches partagés ne
# changent jamais de propriétaire.
preparation() {
case "$1" in
ubuntu-* | debian-*) echo 'apt-get update -q && DEBIAN_FRONTEND=noninteractive apt-get install -y -q sudo' ;;
fedora-* | almalinux-* | rocky-*) echo 'dnf install -y sudo shadow-utils' ;;
opensuse-*) echo 'zypper --non-interactive install sudo shadow' ;;
arch) echo 'pacman -Syu --noconfirm sudo' ;;
esac
}
# Étapes sous le compte dev, chacune annoncée par une ligne « === étape
# statut » que le bilan relit. L'essai et le serveur de Vite ne se jouent
# que sur une installation réussie.
ETAPES_DEV='
etape() { local nom="$1"; shift; if "$@"; then echo "=== $nom ok"; else echo "=== $nom ÉCHEC"; return 1; fi; }
cd "$HOME/projet"
etape install_dev ./install_dev.sh || exit 1
etape test make test
etape test_long make test_long
etape test_navigateur make test_navigateur
etape essai_demarrage env GTT_SANS_BAC_A_SABLE=1 make essai_demarrage
web() {
./run.sh --web > "$HOME/web.log" 2>&1 &
scripts/installation/avec_node.sh node -e "
const fin = Date.now() + 60000;
(async () => {
while (Date.now() < fin) {
try {
const page = await (await fetch(\"http://localhost:5173/\")).text();
if (page.includes(\"/src/main.js\")) process.exit(0);
} catch {}
await new Promise((r) => setTimeout(r, 500));
}
process.exit(1);
})();"
}
etape web web
'
verifier_linux() {
local systeme="$1" image="$2"
podman run --rm --env-host=false --label "$ETIQUETTE" \
-v "$source_copie:/source:ro" \
-v gttl-cache-npm:/home/dev/.npm \
-v gttl-cache-outils:/home/dev/.cache \
"$image" bash -c "
set -e
$(preparation "$systeme")
useradd -m -u 1500 -s /bin/bash dev 2> /dev/null || true
echo 'dev ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/dev
cp -a /source /home/dev/projet
chown -R dev: /home/dev/projet
chown dev: /home/dev /home/dev/.npm /home/dev/.cache
sudo -u dev -H bash -c '$ETAPES_DEV'
"
}
# L'image nix tourne sous root, sans sudo ni useradd : elle éprouve
# shell.nix, que GTT_FAMILLE désigne, sur une base qui n'est pas NixOS.
# ./install.sh y entre de lui-même ; make n'existe que dans shell.nix, où
# les étapes suivantes s'exécutent, comme sous NixOS.
verifier_nix() {
podman run --rm --env-host=false --label "$ETIQUETTE" -e GTT_FAMILLE=nixos \
-v "$source_copie:/source:ro" \
"$2" sh -c "
cp -a /source /root/projet
export HOME=/root
cd /root/projet
if nix-shell -p bash --run ./install.sh; then echo '=== entree_shell_nix ok'; else echo '=== entree_shell_nix ÉCHEC'; exit 1; fi
nix-shell scripts/installation/shell.nix --run 'bash -c '\''$ETAPES_DEV'\'''
"
}
# Les scripts PowerShell sous pwsh : l'analyse syntaxique de chacun, puis la
# lecture de .node-version, le choix de l'archive dans SHASUMS256.txt, et le
# téléchargement, la vérification et l'extraction de Node pour win-x64 —
# node.exe ne s'exécute pas ici, son extraction se constate.
verifier_pwsh() {
podman run --rm --env-host=false --label "$ETIQUETTE" -v "$source_copie:/source:ro" "$2" pwsh -NoProfile -Command '
$ErrorActionPreference = "Stop"
$erreurs = 0
foreach ($f in Get-ChildItem /source/scripts/windows/*.ps1) {
$jetons = $null; $fautes = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$jetons, [ref]$fautes)
if ($fautes.Count -gt 0) { $erreurs++; Write-Host "$($f.Name) : $($fautes[0].Message)" }
}
if ($erreurs -eq 0) { Write-Host "=== analyse ok" } else { Write-Host "=== analyse ÉCHEC"; exit 1 }
Copy-Item -Recurse /source /tmp/projet
$env:LOCALAPPDATA = "/tmp/donnees"
$env:PROCESSOR_ARCHITECTURE = "AMD64"
try { . /tmp/projet/scripts/windows/commun.ps1 } catch { Write-Host "=== chargement ÉCHEC $_"; exit 1 }
Write-Host "=== chargement ok"
$choix = Find-GttArchiveNode @("a1 node-v26.10.0-win-x64.7z", "b2 node-v26.10.0-win-x64.zip", "c3 node-v26.10.0-win-arm64.zip") "x64"
if ("$choix" -eq "b2 node-v26.10.0-win-x64.zip" -and (Get-GttMajeure) -match "^[0-9]+$") { Write-Host "=== choix ok" } else { Write-Host "=== choix ÉCHEC $choix"; exit 1 }
try { Install-GttNode } catch { }
if (Test-Path (Join-Path (Get-GttDossierNode) "node.exe")) { Write-Host "=== node_windows ok" } else { Write-Host "=== node_windows ÉCHEC"; exit 1 }
'
}
verifier() {
local systeme="$1" image journal
image="$(image_de "$systeme")"
journal="$journaux/$systeme.log"
{
echo "### $systeme ($image)"
case "$systeme" in
nix) verifier_nix "$systeme" "$image" ;;
pwsh) verifier_pwsh "$systeme" "$image" ;;
*) verifier_linux "$systeme" "$image" ;;
esac
} > "$journal" 2>&1 || true
echo "$systeme : $(grep -c '^=== .* ok$' "$journal") ok, $(grep -c '^=== .* ÉCHEC' "$journal") échec(s)"
}
paralleles="${GTT_PARALLELES:-4}"
echo "Journaux : $journaux"
for systeme in "${systemes[@]}"; do
while [ "$(jobs -rp | wc -l)" -ge "$paralleles" ]; do
sleep 2
done
verifier "$systeme" &
done
wait
echo
echo "Bilan :"
statut=0
for systeme in "${systemes[@]}"; do
ligne="$(grep '^=== ' "$journaux/$systeme.log" | sed 's/^=== //' | tr '\n' ' ')"
case "$ligne" in
'' | *ÉCHEC*) statut=1 ;;
esac
printf ' %-22s %s\n' "$systeme" "${ligne:-aucune étape (voir $journaux/$systeme.log)}"
done
exit "$statut"

112
test/installation.test.js Normal file
View file

@ -0,0 +1,112 @@
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Les points d'entrée de l'installation et du lancement : ./install.sh,
// ./install_dev.sh, ./run.sh et le Makefile sous Linux et macOS ;
// install.cmd, install_dev.cmd et run.cmd sous Windows, qui passent la main
// à PowerShell ; shell.nix sous NixOS. Ces épreuves gardent ce qui les casse
// sans rien faire échouer ailleurs : un bit d'exécution perdu, un chemin
// renommé, une commande npm qui contourne le Node du projet, une fin de ligne
// d'Unix dans un fichier de commandes, une marque d'ordre d'octets absente.
import assert from 'node:assert/strict';
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, test } from './lanceur.js';
const RACINE = fileURLToPath(new URL('..', import.meta.url));
const lire = (chemin) => readFileSync(join(RACINE, chemin), 'utf8');
const SCRIPTS_BASH = ['install.sh', 'install_dev.sh', 'run.sh', 'scripts/installation/avec_node.sh'];
describe('installation : les scripts bash', () => {
test('ils sont exécutables, et cherchent bash dans le PATH : /bin/bash manque sous NixOS', () => {
for (const script of SCRIPTS_BASH) {
assert.equal(lire(script).split('\n')[0], '#!/usr/bin/env bash', script);
if (process.platform !== 'win32') {
assert.equal(statSync(join(RACINE, script)).mode & 0o111, 0o111, `${script} n'est pas exécutable`);
}
}
});
test('ils sourcent la bibliothèque commune, et s’arrêtent à la première erreur', () => {
for (const script of SCRIPTS_BASH) {
const texte = lire(script);
assert.match(texte, /^set -euo pipefail$/m, script);
assert.match(texte, /commun\.sh/, script);
}
});
});
describe('installation : le Makefile', () => {
const makefile = lire('Makefile');
const recettes = makefile.split('\n').filter((ligne) => ligne.startsWith('\t'));
test('make sans cible lance l’application, comme ./run.sh', () => {
assert.match(makefile, /^\.DEFAULT_GOAL := run$/m);
assert.match(makefile, /^LANCER := \.\/run\.sh$/m);
assert.match(makefile, /^run: .*\n\t\$\(LANCER\)$/m);
});
test('chaque script que le Makefile nomme existe', () => {
const chemins = [...makefile.matchAll(/(?:^|[\s=])(?:\.\/)?((?:scripts\/[\w/.-]+\.(?:sh|ps1|js))|[\w-]+\.sh)\b/gm)]
.map(([, chemin]) => chemin);
assert.ok(chemins.length > 0, 'aucun script relevé');
assert.deepEqual(chemins.filter((chemin) => !existsSync(join(RACINE, chemin))), []);
});
test('npm, npx et node ne s’y lancent que par avec_node, qui pose le Node du projet', () => {
const directes = recettes.filter((ligne) => /\b(?:npm|npx|node)\b/.test(ligne) && !ligne.includes('$(AVEC_NODE)'));
assert.ok(recettes.length > 0, 'aucune recette relevée');
assert.deepEqual(directes, []);
});
});
describe('installation : Windows', () => {
const commandes = readdirSync(RACINE).filter((nom) => nom.endsWith('.cmd')).sort();
test('install.cmd, install_dev.cmd et run.cmd existent à la racine', () => {
assert.deepEqual(commandes, ['install.cmd', 'install_dev.cmd', 'run.cmd']);
});
test('chaque fichier de commandes appelle un script PowerShell qui existe, avec ses arguments', () => {
for (const nom of commandes) {
const appel = lire(nom).match(/-File "%~dp0(scripts\\windows\\[\w.]+\.ps1)"[^\r\n]*%\*/);
assert.ok(appel, `${nom} n'appelle aucun script PowerShell avec %*`);
assert.ok(existsSync(join(RACINE, ...appel[1].split('\\'))), `${nom} → ${appel[1]} absent`);
}
});
test('les fichiers de commandes ont des fins de ligne de Windows', () => {
for (const nom of commandes) {
const texte = lire(nom);
assert.equal(texte.replaceAll('\r\n', '').includes('\n'), false, `${nom} a une fin de ligne d'Unix`);
}
});
test('les scripts PowerShell portent la marque d’ordre d’octets UTF-8 : sans elle, Windows PowerShell 5.1 défigure les accents', () => {
const scripts = readdirSync(join(RACINE, 'scripts', 'windows')).filter((nom) => nom.endsWith('.ps1'));
assert.ok(scripts.length > 0, 'aucun script PowerShell');
for (const nom of scripts) {
const octets = readFileSync(join(RACINE, 'scripts', 'windows', nom));
assert.deepEqual([...octets.subarray(0, 3)], [0xef, 0xbb, 0xbf], nom);
}
});
});
describe('installation : la majeure de Node a une seule source', () => {
test('.node-version porte une majeure entière', () => {
assert.match(lire('.node-version'), /^\d+\n$/);
});
test('bash, PowerShell et shell.nix la lisent dans .node-version au lieu de l’écrire', () => {
assert.match(lire('scripts/installation/commun.sh'), /\$GTT_RACINE\/\.node-version/);
assert.match(lire('scripts/windows/commun.ps1'), /'\.node-version'/);
assert.match(lire('scripts/installation/shell.nix'), /readFile \.\.\/\.\.\/\.node-version/);
});
test('shell.nix épingle nixpkgs par révision et par somme', () => {
const nix = lire('scripts/installation/shell.nix');
assert.match(nix, /archive\/[0-9a-f]{40}\.tar\.gz/);
assert.match(nix, /sha256 = "[0-9a-z]{52}"/);
});
});