Compare commits

...

3 commits

Author SHA1 Message Date
b9090508ff [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
2026-10-06 05:52:17 -04:00
d83362166a [ADD] install: Node 26 and system packages, per family and under Nix
./install.sh installs what is missing through apt, dnf, zypper or
pacman, then Node of the .node-version major from nodejs.org, SHA-256
checked, outside the repository, then npm ci and Electron's binary,
which its package no longer fetches at install; ldd refuses a missing
library. ./install_dev.sh adds Playwright's Chromium, fonts, and podman
on amd64. Under NixOS a pinned shell.nix provides Node, Electron and
Chromium, which the browser tests then launch. Build-script tests run
on Linux only, where § 16 builds.

Checked: ten library tests, three mutations caught; local install.

--- FR ---

[ADD] installation : Node 26 et paquets du système, par famille et Nix

./install.sh installe ce qui manque par apt, dnf, zypper ou pacman, puis
Node de la majeure de .node-version, de nodejs.org, somme SHA-256
vérifiée, hors du dépôt, puis npm ci et le binaire d'Electron, que son
paquet ne télécharge plus à l'installation ; ldd refuse une bibliothèque
absente. ./install_dev.sh ajoute le Chromium de Playwright, des polices,
et podman sur amd64. Sous NixOS, un shell.nix épinglé fournit Node,
Electron et Chromium, que lancent alors les épreuves du navigateur. Les
épreuves du script de construction ne se jouent que sous Linux (§ 16).

Vérifié : dix épreuves de la bibliothèque, trois mutations attrapées ;
installation locale.

Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 05:52:05 -04:00
67368b719f [ADD] scripts: Windows install and run through PowerShell 5.1
install.cmd, install_dev.cmd and run.cmd hand over to PowerShell
scripts that place Node of the .node-version major under %LOCALAPPDATA%,
checked against the SHA-256 that nodejs.org publishes, then run npm ci:
no administrator right is needed. The scripts stay within Windows
PowerShell 5.1, shipped with Windows, and carry a UTF-8 byte order mark,
without which 5.1 reads them in the system code page and mangles
accents. The licence guard now reads .ps1, .cmd, .nix and Makefile.

Checked: parsed and run under pwsh in a container, Node for win-x64
downloaded, verified and extracted; not yet run on Windows itself.

--- FR ---

[ADD] scripts : installer et lancer sous Windows, par PowerShell 5.1

install.cmd, install_dev.cmd et run.cmd passent la main à des scripts
PowerShell qui posent Node de la majeure de .node-version sous
%LOCALAPPDATA%, vérifié contre la somme SHA-256 que publie nodejs.org,
puis lancent npm ci : aucun droit d'administrateur n'est requis. Les
scripts s'en tiennent à Windows PowerShell 5.1, livré avec Windows, et
portent une marque d'ordre d'octets UTF-8, sans laquelle 5.1 les lit
dans la page de code du système et défigure les accents. La garde de
licence lit désormais les .ps1, .cmd, .nix et le Makefile.

Vérifié : analysés et exécutés sous pwsh en conteneur, Node win-x64
téléchargé, vérifié, extrait ; pas encore exécutés sous Windows même.

Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 05:51:31 -04:00
26 changed files with 1891 additions and 30 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
* text=auto eol=lf
*.cmd text eol=crlf

1
.node-version Normal file
View file

@ -0,0 +1 @@
26

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`.

6
install.cmd Normal file
View file

@ -0,0 +1,6 @@
@REM © 2026 TechnoLibre (http://www.technolibre.ca)
@REM License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
@echo off
rem Installe de quoi lancer l'application depuis les sources : scripts\windows\installer.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\windows\installer.ps1" %*
exit /b %ERRORLEVEL%

73
install.sh Executable file
View file

@ -0,0 +1,73 @@
#!/usr/bin/env bash
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
# Installe de quoi lancer l'application depuis les sources et jouer les
# épreuves sous node : les outils et les bibliothèques du système, Node de la
# majeure de .node-version, et les dépendances de package-lock.json.
# Relancé, il ne refait que ce qui manque.
#
# ./install.sh
#
# Sous Windows (bash de Git), la main passe à install.cmd ; sous NixOS, à
# scripts/installation/shell.nix, qui déclare ce que les autres systèmes
# installent.
set -euo pipefail
cd -- "$(dirname -- "$0")"
. scripts/installation/commun.sh
for argument in "$@"; do
case "$argument" in
-h | --help)
sed -n '5,9p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
gtt_dire "usage : ./install.sh"
exit 2
;;
esac
done
famille="$(gtt_famille)"
case "$famille" in
windows) gtt_powershell installer.ps1 ;;
nixos)
gtt_dans_nix_shell ./install.sh
gtt_npm_ci
gtt_dire "Installation terminée. Lancer : ./run.sh, ou make."
exit 0
;;
macos)
xcode-select -p > /dev/null 2>&1 \
|| gtt_dire "Outils de ligne de commande de Xcode absents, dont make : xcode-select --install"
;;
debian | fedora | suse | arch)
paquets="$(gtt_paquets_outils "$famille")"
if gtt_electron_disponible; then
paquets="$paquets $(gtt_paquets_electron "$famille")"
fi
# shellcheck disable=SC2086
gtt_installer_paquets "$famille" $paquets
;;
*)
gtt_refuser "système non reconnu ($(uname -s), /etc/os-release). Systèmes pris en charge :" \
"Debian, Ubuntu, Proxmox VE, Fedora, AlmaLinux, Rocky Linux, openSUSE, Arch, NixOS, macOS, Windows." \
"Un dérivé se rattache à sa famille : GTT_FAMILLE=debian ./install.sh (debian, fedora, suse, arch)."
;;
esac
gtt_poser_node
gtt_activer_node
if ! gtt_electron_disponible; then
gtt_dire "Electron n'est pas publié pour $(gtt_os_node)-$(gtt_arch_node) :" \
"./run.sh servira l'application à un navigateur."
fi
gtt_npm_ci
if gtt_electron_disponible && [ "$(gtt_os_node)" = linux ]; then
manquantes="$(gtt_bibliotheques_manquantes node_modules/electron/dist/electron)"
[ -z "$manquantes" ] || gtt_refuser "Electron ne trouve pas :" "$manquantes"
gtt_preparer_bac_a_sable
fi
gtt_dire "Installation terminée. Lancer : ./run.sh, ou make."

6
install_dev.cmd Normal file
View file

@ -0,0 +1,6 @@
@REM © 2026 TechnoLibre (http://www.technolibre.ca)
@REM License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
@echo off
rem Installe l'environnement de developpement : scripts\windows\installer.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\windows\installer.ps1" -Dev %*
exit /b %ERRORLEVEL%

61
install_dev.sh Executable file
View file

@ -0,0 +1,61 @@
#!/usr/bin/env bash
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
# Installe l'environnement de développement : ce qu'installe ./install.sh,
# puis le Chromium de Playwright des épreuves du navigateur, des polices, et
# podman, sous lequel se construit l'exécutable Windows (§ 16) sur amd64.
#
# ./install_dev.sh
set -euo pipefail
cd -- "$(dirname -- "$0")"
. scripts/installation/commun.sh
for argument in "$@"; do
case "$argument" in
-h | --help)
sed -n '5,10p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
gtt_dire "usage : ./install_dev.sh"
exit 2
;;
esac
done
famille="$(gtt_famille)"
case "$famille" in
windows) gtt_powershell installer.ps1 -Dev ;;
# shell.nix déclare aussi Chromium et ses polices.
nixos) exec ./install.sh ;;
esac
./install.sh
case "$famille" in
debian | fedora | suse | arch)
paquets="$(gtt_paquets_developpement "$famille")"
if [ "$(gtt_arch_node)" = x64 ]; then
paquets="$paquets podman"
fi
# shellcheck disable=SC2086
gtt_installer_paquets "$famille" $paquets
;;
esac
gtt_activer_node
if gtt_chromium_disponible; then
npx --no-install playwright install --only-shell chromium
if [ "$(gtt_os_node)" = linux ]; then
navigateurs="${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}"
coquille="$(find "$navigateurs" -maxdepth 3 -type f \
\( -name headless_shell -o -name chrome-headless-shell \) 2> /dev/null | head -n 1)"
[ -n "$coquille" ] || gtt_refuser "Chromium de Playwright introuvable sous $navigateurs."
manquantes="$(gtt_bibliotheques_manquantes "$coquille")"
[ -z "$manquantes" ] || gtt_refuser "Chromium de Playwright ne trouve pas :" "$manquantes"
fi
else
gtt_dire "Playwright ne publie pas Chromium pour $(gtt_os_node)-$(gtt_arch_node) :" \
"les épreuves du navigateur ne s'y jouent pas."
fi
gtt_dire "Environnement de développement prêt : make help."

6
run.cmd Normal file
View file

@ -0,0 +1,6 @@
@REM © 2026 TechnoLibre (http://www.technolibre.ca)
@REM License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
@echo off
rem Lance l'application depuis les sources : scripts\windows\lancer.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\windows\lancer.ps1" %*
exit /b %ERRORLEVEL%

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

@ -118,6 +118,11 @@ const NODE_CONSIGNE = [
// bin/ en reçoit des liens : seul podman y manque.
const OUTILS = ['bash', 'dirname', 'find', 'mkdir', 'rm', 'wc', 'file'];
// La construction se fait sous Linux (§ 16) : le script emploie bash 4 et le
// find de GNU, que ni macOS ni Windows ne livrent. Ces épreuves ne se jouent
// pas ailleurs.
const HORS_LINUX = process.platform !== 'linux';
// Chemin d'un exécutable dans le PATH de l'épreuve, ou null.
function chercher(nom) {
for (const dossier of (process.env.PATH ?? '').split(delimiter)) {
@ -248,7 +253,7 @@ const taperLeNom = (nom) => (projet) =>
let reussie;
const constructionReussie = () => (reussie ??= construire({ produit: NOM }));
describe('construction Windows : refus avant de construire', () => {
describe('construction Windows : refus avant de construire', { skip: HORS_LINUX }, () => {
test('une version sans section au changelog ne se construit pas (§ 16)', () => {
const r = construire({
preparer: (projet) =>
@ -321,7 +326,7 @@ describe('construction Windows : refus avant de construire', () => {
});
});
describe('construction Windows : la page', () => {
describe('construction Windows : la page', { skip: HORS_LINUX }, () => {
test('une page qui ne se construit pas arrête tout, avant podman', () => {
const r = construire({ produit: NOM, codeNpm: 1 });
assert.equal(r.statut, 1, r.erreurs);
@ -339,7 +344,7 @@ describe('construction Windows : la page', () => {
});
});
describe('construction Windows : la construction', () => {
describe('construction Windows : la construction', { skip: HORS_LINUX }, () => {
test("construit la page puis l'exécutable, et imprime son chemin, sa taille et son type", () => {
const r = constructionReussie();
assert.equal(r.statut, 0, r.erreurs);
@ -393,7 +398,7 @@ describe('construction Windows : la construction', () => {
});
});
describe('construction Windows : contrôle de fin de construction (§ 18.5)', () => {
describe('construction Windows : contrôle de fin de construction (§ 18.5)', { skip: HORS_LINUX }, () => {
test("refuse un exécutable dont le nom ne dérive pas de version.json", () => {
const autre = 'Gestion table tournante Libre.exe';
const r = construire({ produit: autre });
@ -434,7 +439,7 @@ describe('construction Windows : contrôle de fin de construction (§ 18.5)', ()
});
});
describe('construction Windows : interruption', () => {
describe('construction Windows : interruption', { skip: HORS_LINUX }, () => {
// Le signal ne vise que le shell, comme un kill depuis un autre terminal.
// podman écrit après l'avoir reçu, comme electron-builder qui achève son
// travail : l'épreuve ne réagit qu'à des événements, l'annonce du podman

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);

View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
# Exécute une commande à la racine du projet, le Node du projet en tête du
# PATH, une fois l'installation vérifiée : c'est par lui que le Makefile
# lance npm, npx et node. Sous NixOS, la commande s'exécute dans
# scripts/installation/shell.nix.
#
# scripts/installation/avec_node.sh npm test
set -euo pipefail
. "$(dirname -- "$0")/commun.sh"
if [ $# -eq 0 ]; then
gtt_dire "usage : scripts/installation/avec_node.sh <commande> [argument…]"
exit 2
fi
[ "$(gtt_famille)" = windows ] && gtt_powershell avec_node.ps1 "$@"
gtt_dans_nix_shell "$0" "$@"
cd -- "$GTT_RACINE"
gtt_preparer
exec "$@"

View file

@ -0,0 +1,497 @@
#!/usr/bin/env bash
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
# Fonctions communes d'install.sh, d'install_dev.sh, de run.sh et
# d'avec_node.sh : reconnaître le système, poser le Node du projet, installer
# des paquets. Ce fichier se source, il ne s'exécute pas.
#
# Il reste lisible par le bash 3.2 de macOS : ni tableau associatif, ni
# ${v,,}, ni mapfile, ni readlink -f.
GTT_RACINE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
gtt_dire() {
printf '%s\n' "$*" >&2
}
gtt_refuser() {
printf 'Refusé : %s\n' "$*" >&2
exit 1
}
# Famille d'un fichier os-release : debian, fedora, suse, arch, nixos ou
# inconnu. ID puis ID_LIKE décident, et non le nom affiché : AlmaLinux et
# Rocky se déclarent « rhel centos fedora », Linux Mint « ubuntu debian »,
# Proxmox VE « debian ». NixOS passe en premier : sa famille ne pose aucun
# paquet.
gtt_famille_depuis_os_release() {
local fichier="$1" id id_like
if [ ! -r "$fichier" ]; then
echo inconnu
return
fi
id="$(sed -n 's/^ID=//p' "$fichier" | tr -d "\"'" | head -n 1)"
id_like="$(sed -n 's/^ID_LIKE=//p' "$fichier" | tr -d "\"'" | head -n 1)"
case " $id $id_like " in
*" nixos "*) echo nixos ;;
*" debian "* | *" ubuntu "*) echo debian ;;
*" suse "* | *" opensuse "* | *" opensuse-leap "* | *" opensuse-tumbleweed "* | *" sles "*) echo suse ;;
*" fedora "* | *" rhel "* | *" centos "*) echo fedora ;;
*" arch "*) echo arch ;;
*) echo inconnu ;;
esac
}
# Famille du système courant : celles d'os-release, plus macos et windows
# (bash de Git, MSYS2, Cygwin). GTT_FAMILLE la force, pour un dérivé que
# os-release ne rattache à rien. uname -s interroge le noyau ; $OSTYPE, figé
# à la compilation de bash, vaut « linux » sous openSUSE et « linux-gnu »
# ailleurs.
gtt_famille() {
if [ -n "${GTT_FAMILLE:-}" ]; then
echo "$GTT_FAMILLE"
return
fi
case "$(uname -s)" in
Darwin) echo macos ;;
MINGW* | MSYS* | CYGWIN*) echo windows ;;
Linux) gtt_famille_depuis_os_release /etc/os-release ;;
*) echo inconnu ;;
esac
}
# Architecture d'une machine au sens de nodejs.org (x64, arm64, s390x,
# ppc64le), « inconnue » ailleurs.
gtt_arch_node_depuis() {
case "$1" in
x86_64 | amd64) echo x64 ;;
aarch64 | arm64) echo arm64 ;;
s390x) echo s390x ;;
ppc64le) echo ppc64le ;;
*) echo inconnue ;;
esac
}
gtt_arch_node() {
gtt_arch_node_depuis "$(uname -m)"
}
# Système au sens de nodejs.org : linux, darwin, win.
gtt_os_node() {
case "$(gtt_famille)" in
macos) echo darwin ;;
windows) echo win ;;
*) echo linux ;;
esac
}
# Vrai quand Electron publie un binaire pour ce système et cette
# architecture. Ailleurs — s390x, ppc64le —, l'application se sert dans un
# navigateur.
gtt_electron_publie() {
case "$1-$2" in
linux-x64 | linux-arm64 | darwin-x64 | darwin-arm64) return 0 ;;
*) return 1 ;;
esac
}
gtt_electron_disponible() {
gtt_electron_publie "$(gtt_os_node)" "$(gtt_arch_node)"
}
# Vrai quand Playwright publie son Chromium pour ce système et cette
# architecture : les épreuves du navigateur n'ont pas d'autre moteur.
gtt_chromium_disponible() {
gtt_electron_publie "$(gtt_os_node)" "$(gtt_arch_node)"
}
# Majeure de Node que vise le projet. .node-version en est la seule source ;
# une valeur qui n'est pas un entier est refusée plutôt que passée à une URL.
gtt_node_majeure() {
local majeure
majeure="$(tr -d ' \t\r\n' < "$GTT_RACINE/.node-version")"
case "$majeure" in
'' | *[!0-9]*) gtt_refuser ".node-version doit porter une majeure entière : « $majeure »." ;;
esac
echo "$majeure"
}
# Dossiers hors du dépôt : le Node posé, et les archives téléchargées. Rangé
# hors du dépôt, Node échappe à l'observateur de Vite, au balayage des
# épreuves et à « git clean », et sert à chaque copie de travail.
gtt_dossier_donnees() {
echo "${XDG_DATA_HOME:-$HOME/.local/share}/gestion_table_tournante_libre"
}
gtt_dossier_cache() {
echo "${XDG_CACHE_HOME:-$HOME/.cache}/gestion_table_tournante_libre"
}
gtt_dossier_node() {
echo "$(gtt_dossier_donnees)/node-$(gtt_node_majeure)-$(gtt_os_node)-$(gtt_arch_node)"
}
# Somme et nom de l'archive .tar.xz d'une plateforme dans un SHASUMS256.txt
# de nodejs.org, lignes « <somme> node-v26.1.0-linux-x64.tar.xz » ; rien
# quand la plateforme n'y figure pas. Le suffixe se compare en entier :
# « linux-x64 » ne retient ni « linux-x64-musl » ni une archive .tar.gz.
gtt_archive_node() {
local sommes="$1" os="$2" arch="$3"
awk -v suffixe="-$os-$arch.tar.xz" '
$2 ~ /^node-v[0-9]+\.[0-9]+\.[0-9]+-/ &&
length($2) > length(suffixe) &&
substr($2, length($2) - length(suffixe) + 1) == suffixe {
print $1, $2
exit
}' "$sommes"
}
gtt_telecharger() {
local url="$1" destination="$2"
if command -v curl > /dev/null 2>&1; then
curl -fsSL --retry 3 -o "$destination" "$url"
elif command -v wget > /dev/null 2>&1; then
wget -q -O "$destination" "$url"
else
gtt_refuser "ni curl ni wget : impossible de télécharger $url"
fi
}
gtt_sha256() {
if command -v sha256sum > /dev/null 2>&1; then
sha256sum "$1" | cut -d ' ' -f 1
else
shasum -a 256 "$1" | cut -d ' ' -f 1
fi
}
# Pose la dernière version publiée de la majeure voulue, une fois : un Node
# déjà posé qui démarre est gardé. L'archive est vérifiée contre la somme
# que nodejs.org publie, puis extraite à côté de la cible et renommée, si
# bien qu'une extraction interrompue ne laisse jamais une cible à moitié
# écrite.
gtt_poser_node() {
local majeure os arch cible cache sommes ligne somme nom archive partiel tmp manquantes
majeure="$(gtt_node_majeure)"
os="$(gtt_os_node)"
arch="$(gtt_arch_node)"
[ "$arch" != inconnue ] || gtt_refuser "architecture $(uname -m) : nodejs.org n'y publie pas Node."
cible="$(gtt_dossier_node)"
if [ -x "$cible/bin/node" ] && "$cible/bin/node" --version > /dev/null 2>&1; then
gtt_dire "Node $("$cible/bin/node" --version) : $cible"
return 0
fi
cache="$(gtt_dossier_cache)"
mkdir -p "$(gtt_dossier_donnees)" "$cache"
sommes="$cache/SHASUMS256-v$majeure.txt"
gtt_telecharger "https://nodejs.org/dist/latest-v$majeure.x/SHASUMS256.txt" "$sommes"
ligne="$(gtt_archive_node "$sommes" "$os" "$arch")"
[ -n "$ligne" ] || gtt_refuser "nodejs.org ne publie pas Node $majeure pour $os-$arch."
somme="${ligne%% *}"
nom="${ligne#* }"
archive="$cache/$nom"
if [ ! -f "$archive" ] || [ "$(gtt_sha256 "$archive")" != "$somme" ]; then
gtt_dire "Téléchargement de $nom"
# Nom unique : deux installations qui partagent le cache téléchargent
# chacune dans son fichier.
partiel="$(mktemp "$archive.XXXXXX")"
gtt_telecharger "https://nodejs.org/dist/latest-v$majeure.x/$nom" "$partiel"
if [ "$(gtt_sha256 "$partiel")" != "$somme" ]; then
rm -f "$partiel"
gtt_refuser "$nom : la somme SHA-256 ne correspond pas à celle que publie nodejs.org."
fi
mv -f "$partiel" "$archive"
fi
tmp="$(mktemp -d "$(gtt_dossier_donnees)/extraction.XXXXXX")"
tar -xJf "$archive" -C "$tmp"
rm -rf "$cible"
mv "$tmp/${nom%.tar.xz}" "$cible"
rmdir "$tmp"
if ! "$cible/bin/node" --version > /dev/null 2>&1; then
manquantes="$(gtt_bibliotheques_manquantes "$cible/bin/node")"
rm -rf "$cible"
gtt_refuser "le Node de nodejs.org ne démarre pas ici${manquantes:+ ; bibliothèques absentes : $manquantes}."
fi
gtt_dire "Node $("$cible/bin/node" --version) posé : $cible"
}
# Met le Node du projet en tête du PATH ; faux quand il n'est pas posé.
gtt_activer_node() {
local bin
bin="$(gtt_dossier_node)/bin"
[ -x "$bin/node" ] || return 1
PATH="$bin:$PATH"
export PATH
}
# Vrai quand les dépendances npm suivent package-lock.json : npm écrit
# node_modules/.package-lock.json à chaque installation, et un verrou plus
# récent que lui annonce une installation à refaire.
gtt_dependances_a_jour() {
local recu="$GTT_RACINE/node_modules/.package-lock.json"
[ -f "$recu" ] && [ ! "$GTT_RACINE/package-lock.json" -nt "$recu" ]
}
# Vrai sur une réponse affirmative au terminal — o, oui, y, yes, sans égard à
# la casse. Le défaut est non, et sans terminal rien ne consent. GTT_OUI=1
# répond oui d'avance, pour une installation sans opérateur.
gtt_confirmer() {
local reponse
[ "${GTT_OUI:-}" = 1 ] && return 0
[ -t 0 ] || return 1
printf '%s [o/N] ' "$1" >&2
read -r reponse || return 1
case "$(printf '%s' "$reponse" | tr '[:upper:]' '[:lower:]')" in
o | oui | y | yes) return 0 ;;
*) return 1 ;;
esac
}
# Exécute une commande d'administration : telle quelle sous root, par sudo
# ailleurs.
gtt_admin() {
if [ "$(id -u)" = 0 ]; then
"$@"
elif command -v sudo > /dev/null 2>&1; then
sudo "$@"
else
gtt_refuser "sudo introuvable : relancer en root, ou exécuter : $*"
fi
}
# Nom d'un paquet apt sur ce système. Debian 13 et Ubuntu 24.04 ont renommé
# en « t64 » les bibliothèques dont l'interface a changé avec le temps sur
# 64 bits ; l'ancien nom n'y est plus qu'un paquet virtuel, qu'apt-get refuse
# d'installer sans qu'on choisisse son fournisseur.
gtt_nom_apt() {
if apt-cache show "${1}t64" > /dev/null 2>&1; then
echo "${1}t64"
else
echo "$1"
fi
}
# Paquets de la liste qui ne sont pas installés, un par ligne, sous le nom
# que le gestionnaire connaît. Sous apt, les noms t64 se résolvent par
# apt-cache, qui ne connaît que les paquets des listes déjà téléchargées.
gtt_paquets_manquants() {
local famille="$1" paquet nom
shift
case "$famille" in
arch) pacman -T "$@" 2> /dev/null || true ;;
*)
for paquet in "$@"; do
case "$famille" in
debian)
nom="$(gtt_nom_apt "$paquet")"
[ "$(dpkg-query -W -f='${db:Status-Abbrev}' "$nom" 2> /dev/null)" = "ii " ] || echo "$nom"
;;
fedora | suse) rpm -q "$paquet" > /dev/null 2>&1 || echo "$paquet" ;;
*) echo "$paquet" ;;
esac
done
;;
esac
}
# Installe ce qui manque de la liste par le gestionnaire de la famille, sans
# question ; rien, et sans sudo, quand tout est là. pacman met tout le
# système à jour : Arch ne prend pas en charge les mises à jour partielles.
# $noms se découpe en paquets, un par mot.
# shellcheck disable=SC2086
gtt_installer_paquets() {
local famille="$1" paquet noms
shift
[ $# -gt 0 ] || return 0
if [ "$famille" = debian ] && ! apt-cache show libc6 > /dev/null 2>&1; then
gtt_admin env DEBIAN_FRONTEND=noninteractive apt-get update -q
fi
noms="$(gtt_paquets_manquants "$famille" "$@" | tr '\n' ' ')"
if [ -z "${noms// /}" ]; then
gtt_dire "Paquets du système : déjà installés."
return 0
fi
gtt_dire "Paquets du système à installer : $noms"
case "$famille" in
debian)
gtt_admin env DEBIAN_FRONTEND=noninteractive apt-get update -q
gtt_admin env DEBIAN_FRONTEND=noninteractive apt-get install -y -q --no-install-recommends $noms
;;
fedora) gtt_admin dnf install -y $noms ;;
suse) gtt_admin zypper --non-interactive install --no-recommends $noms ;;
arch) gtt_admin pacman -Syu --noconfirm --needed $noms ;;
*) gtt_refuser "aucun gestionnaire de paquets connu pour la famille « $famille »." ;;
esac
}
# Outils dont les scripts et les épreuves se servent, par famille : de quoi
# télécharger et extraire Node, la libatomic que réclame le Node de
# nodejs.org, awk, que l'image de Tumbleweed n'a pas, make, et file et find,
# qu'appelle l'épreuve de scripts/construire_windows.sh. curl n'est demandé qu'en l'absence de curl
# et de wget : Fedora et ses dérivés livrent curl-minimal, qu'un paquet curl
# contredirait.
gtt_paquets_outils() {
local liste="make file tar"
if ! command -v curl > /dev/null 2>&1 && ! command -v wget > /dev/null 2>&1; then
liste="$liste curl"
fi
case "$1" in
debian) echo "$liste ca-certificates xz-utils findutils libatomic1" ;;
fedora) echo "$liste ca-certificates xz findutils libatomic" ;;
suse) echo "$liste ca-certificates xz findutils gawk libatomic1" ;;
arch) echo "$liste ca-certificates xz findutils gcc-libs" ;;
esac
}
# Bibliothèques qu'Electron charge au démarrage, par famille : GTK 3, NSS,
# ALSA, GBM, et les extensions X11 du compositeur. L'installation vérifie
# ensuite par ldd qu'aucune ne manque.
gtt_paquets_electron() {
case "$1" in
debian)
echo "libgtk-3-0 libnss3 libasound2 libgbm1 libdrm2 libxkbcommon0" \
"libatk-bridge2.0-0 libcups2 libxcomposite1 libxdamage1 libxrandr2" \
"libpango-1.0-0 libcairo2 libatspi2.0-0"
;;
fedora)
echo "gtk3 nss alsa-lib mesa-libgbm libdrm libxkbcommon at-spi2-atk" \
"cups-libs libXcomposite libXdamage libXrandr pango cairo"
;;
suse)
echo "libgtk-3-0 mozilla-nss libasound2 libgbm1 libdrm2 libxkbcommon0" \
"libatk-bridge-2_0-0 libcups2 libXcomposite1 libXdamage1 libXrandr2" \
"libpango-1_0-0 libcairo2"
;;
arch)
echo "gtk3 nss alsa-lib mesa libdrm libxkbcommon at-spi2-core libcups" \
"libxcomposite libxdamage libxrandr pango cairo"
;;
esac
}
# Ce que le développement ajoute : des polices, sans lesquelles Chromium
# dessine un texte sans largeur. Lancer Electron sans écran ne demande rien
# de plus : scripts/essai_demarrage.js emploie la plateforme Ozone headless.
gtt_paquets_developpement() {
case "$1" in
debian) echo "fonts-dejavu-core" ;;
fedora) echo "dejavu-sans-fonts" ;;
suse) echo "dejavu-fonts" ;;
arch) echo "ttf-dejavu" ;;
esac
}
# Bibliothèques qu'un exécutable réclame sans les trouver, sur une ligne ;
# rien quand toutes se résolvent, ou quand ldd manque. Un exécutable absent
# est nommé comme tel.
gtt_bibliotheques_manquantes() {
if [ ! -f "$1" ]; then
echo "$1 (absent)"
return 0
fi
command -v ldd > /dev/null 2>&1 || return 0
{ ldd "$1" 2> /dev/null || true; } | awk '/not found/ { print $1 }' | sort -u | tr '\n' ' ' | sed 's/ $//'
}
# Vrai quand une session graphique est ouverte : Electron a où dessiner.
gtt_session_graphique() {
case "$(gtt_famille)" in
macos | windows) return 0 ;;
esac
[ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]
}
# Vrai quand Chromium ne peut pas cloisonner ses processus par des espaces
# de noms d'utilisateur : Ubuntu depuis 24.04 les restreint par AppArmor, un
# noyau peut les couper. Electron se rabat alors sur son assistant
# chrome-sandbox, qui doit appartenir à root et porter le bit setuid.
gtt_espaces_utilisateur_restreints() {
[ "$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2> /dev/null)" = 1 ] && return 0
[ "$(cat /proc/sys/kernel/unprivileged_userns_clone 2> /dev/null)" = 0 ] && return 0
[ "$(cat /proc/sys/user/max_user_namespaces 2> /dev/null)" = 0 ] && return 0
return 1
}
GTT_ASSISTANT_BAC_A_SABLE="node_modules/electron/dist/chrome-sandbox"
# Vrai quand le bac à sable d'Electron démarre : espaces de noms libres, ou
# assistant setuid root. npm ci réécrit l'assistant sans ce bit.
gtt_bac_a_sable_pret() {
local assistant="$GTT_RACINE/$GTT_ASSISTANT_BAC_A_SABLE"
[ -f "$assistant" ] || return 0
gtt_espaces_utilisateur_restreints || return 0
[ -u "$assistant" ] && [ "$(stat -c %u "$assistant")" = 0 ]
}
gtt_preparer_bac_a_sable() {
local assistant="$GTT_RACINE/$GTT_ASSISTANT_BAC_A_SABLE"
gtt_bac_a_sable_pret && return 0
gtt_dire "Espaces de noms d'utilisateur restreints : chrome-sandbox devient setuid root."
gtt_admin chown root:root "$assistant"
gtt_admin chmod 4755 "$assistant"
}
# Propose ./install.sh, et le lance sur consentement ; sans lui, refuse.
gtt_proposer_installation() {
gtt_dire "$1"
if ! gtt_confirmer "Lancer ./install.sh (paquets du système par sudo, Node, dépendances npm) ?"; then
gtt_refuser "installation non lancée. Lancer ./install.sh, puis recommencer."
fi
"$GTT_RACINE/install.sh"
}
# Rend prêt à exécuter : le Node du projet en tête du PATH, et des
# dépendances npm qui suivent package-lock.json. Sous NixOS, le Node vient
# de scripts/installation/shell.nix, dans lequel l'appelant s'exécute déjà.
# Des dépendances en retard se réinstallent sans question : node_modules ne
# porte que ce que le verrou décrit.
gtt_preparer() {
if [ "$(gtt_famille)" = nixos ]; then
command -v node > /dev/null 2>&1 \
|| gtt_refuser "node absent : lancer depuis scripts/installation/shell.nix."
elif ! gtt_activer_node; then
gtt_proposer_installation "Node $(gtt_node_majeure) n'est pas posé."
gtt_activer_node || gtt_refuser "Node $(gtt_node_majeure) reste introuvable après l'installation."
fi
if ! gtt_dependances_a_jour; then
gtt_dire "Les dépendances npm ne suivent pas package-lock.json : npm ci."
(cd -- "$GTT_RACINE" && gtt_npm_ci)
else
(cd -- "$GTT_RACINE" && gtt_poser_electron)
fi
}
# Pose le binaire d'Electron de la version de package-lock.json. Le paquet
# electron ne le télécharge plus à l'installation mais au premier
# chargement ; install-electron le fait d'avance, contre les sommes que le
# paquet embarque, et ne refait rien quand il est déjà là. Rien là où
# Electron n'est pas publié, ni sous Nix, où ELECTRON_OVERRIDE_DIST_PATH
# désigne celui de nixpkgs.
gtt_poser_electron() {
gtt_electron_disponible || return 0
[ -z "${ELECTRON_OVERRIDE_DIST_PATH:-}" ] || return 0
npx --no-install install-electron
}
gtt_npm_ci() {
npm ci --no-audit --no-fund
gtt_poser_electron
}
# Ligne de commande de PowerShell pour un script de scripts/windows, depuis
# le bash de Git sous Windows.
gtt_powershell() {
local script="$1"
shift
exec powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$GTT_RACINE/scripts/windows/$script" "$@"
}
# Relance la commande dans scripts/installation/shell.nix, sous NixOS, quand
# elle n'y est pas déjà. shell.nix pose GTT_SHELL_NIX : IN_NIX_SHELL, que
# pose tout nix-shell, ne dit pas lequel.
gtt_dans_nix_shell() {
[ "$(gtt_famille)" = nixos ] || return 0
[ "${GTT_SHELL_NIX:-}" != 1 ] || return 0
exec nix-shell "$GTT_RACINE/scripts/installation/shell.nix" --run "$(printf '%q ' "$@")"
}

View file

@ -0,0 +1,183 @@
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Fonctions de scripts/installation/commun.sh, appelées par bash : la
// famille d'un os-release, l'architecture et la plateforme au sens de
// nodejs.org et d'Electron, l'archive de Node dans un SHASUMS256.txt, la
// majeure de .node-version, l'état des dépendances npm, le consentement.
// Sous Windows, ces scripts passent la main à PowerShell et ces épreuves ne
// se jouent pas.
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, test } from '../../test/lanceur.js';
const COMMUN = fileURLToPath(new URL('./commun.sh', import.meta.url));
const SOUS_WINDOWS = process.platform === 'win32';
// Exécute un script bash après avoir sourcé commun.sh, dans un
// environnement réduit au PATH, à HOME et à ce qu'on lui passe ; l'entrée
// standard n'est jamais un terminal. Rend { statut, sortie, erreurs }.
function bash(script, env = {}) {
const r = spawnSync('bash', ['-c', `. "$1"\n${script}`, 'bash', COMMUN], {
encoding: 'utf8',
env: { PATH: process.env.PATH, HOME: process.env.HOME, ...env },
input: '',
});
return { statut: r.status, sortie: r.stdout.trim(), erreurs: r.stderr.trim() };
}
// Écrit des fichiers dans un dossier temporaire, appelle f(dossier), efface.
function avecDossier(fichiers, f) {
const dossier = mkdtempSync(join(tmpdir(), 'installation-'));
try {
for (const [chemin, texte] of Object.entries(fichiers)) {
mkdirSync(join(dossier, chemin, '..'), { recursive: true });
writeFileSync(join(dossier, chemin), texte);
}
return f(dossier);
} finally {
rmSync(dossier, { recursive: true, force: true });
}
}
describe('installation : la famille du système, lue dans os-release', { skip: SOUS_WINDOWS }, () => {
// [contenu d'os-release, famille attendue] : les systèmes du catalogue de
// machines virtuelles, deux dérivés, des guillemets simples, un inconnu.
const CAS = [
['ID=ubuntu\nID_LIKE=debian\nVERSION_ID="24.04"\n', 'debian'],
['ID=debian\nVERSION_ID="13"\n', 'debian'],
['ID=linuxmint\nID_LIKE="ubuntu debian"\n', 'debian'],
['ID=fedora\nVERSION_ID=43\n', 'fedora'],
['ID="almalinux"\nID_LIKE="rhel centos fedora"\n', 'fedora'],
['ID="rocky"\nID_LIKE="rhel centos fedora"\n', 'fedora'],
['ID="opensuse-leap"\nID_LIKE="suse opensuse"\n', 'suse'],
['ID="opensuse-tumbleweed"\nID_LIKE="opensuse suse"\n', 'suse'],
['ID=arch\n', 'arch'],
['ID=manjaro\nID_LIKE=arch\n', 'arch'],
['ID=nixos\nVERSION_ID="25.11"\n', 'nixos'],
["ID='fedora'\n", 'fedora'],
['ID=void\n', 'inconnu'],
['NAME="sans identifiant"\n', 'inconnu'],
];
test('chaque os-release donne sa famille, par ID puis ID_LIKE', () => {
const ecarts = avecDossier({}, (dossier) =>
CAS.flatMap(([contenu, attendue], rang) => {
const fichier = join(dossier, `os-release-${rang}`);
writeFileSync(fichier, contenu);
const { sortie } = bash(`gtt_famille_depuis_os_release "${fichier}"`);
return sortie === attendue ? [] : [`${JSON.stringify(contenu)} → ${sortie}, attendu ${attendue}`];
}),
);
assert.deepEqual(ecarts, []);
});
test('un os-release absent donne inconnu', () => {
assert.equal(bash('gtt_famille_depuis_os_release /chemin/absent/os-release').sortie, 'inconnu');
});
test('GTT_FAMILLE l’emporte sur le système', () => {
assert.equal(bash('gtt_famille', { GTT_FAMILLE: 'suse' }).sortie, 'suse');
});
});
describe('installation : architecture et plateforme', { skip: SOUS_WINDOWS }, () => {
test('uname -m se traduit en architecture de nodejs.org', () => {
const CAS = [
['x86_64', 'x64'], ['amd64', 'x64'], ['aarch64', 'arm64'], ['arm64', 'arm64'],
['s390x', 's390x'], ['ppc64le', 'ppc64le'], ['riscv64', 'inconnue'], ['i686', 'inconnue'],
];
for (const [machine, attendue] of CAS) {
assert.equal(bash(`gtt_arch_node_depuis ${machine}`).sortie, attendue, machine);
}
});
test('Electron et le Chromium de Playwright ne sont publiés que pour linux et darwin, x64 et arm64', () => {
const CAS = [
['linux', 'x64', 0], ['linux', 'arm64', 0], ['darwin', 'x64', 0], ['darwin', 'arm64', 0],
['linux', 's390x', 1], ['linux', 'ppc64le', 1], ['linux', 'inconnue', 1],
];
for (const [os, arch, statut] of CAS) {
assert.equal(bash(`gtt_electron_publie ${os} ${arch}`).statut, statut, `${os}-${arch}`);
}
});
});
describe('installation : l’archive de Node dans SHASUMS256.txt', { skip: SOUS_WINDOWS }, () => {
// Sommes inventées ; noms de la forme que publie nodejs.org.
const SOMMES = [
'a1 node-v26.10.0-darwin-arm64.tar.gz',
'a2 node-v26.10.0-darwin-arm64.tar.xz',
'b1 node-v26.10.0-linux-x64.tar.gz',
'b2 node-v26.10.0-linux-x64-musl.tar.xz',
'b3 node-v26.10.0-linux-x64.tar.xz',
'c1 node-v26.10.0-linux-s390x.tar.xz',
'd1 node-v26.10.0-win-x64.zip',
'e1 node-v26.10.0.tar.xz',
'e2 node-v26.10.0-headers.tar.xz',
'e3 win-x64/node.exe',
'',
].join('\n');
test('chaque plateforme publiée donne sa somme et son archive .tar.xz, et rien d’autre', () => {
const CAS = [
['linux', 'x64', 'b3 node-v26.10.0-linux-x64.tar.xz'],
['linux', 's390x', 'c1 node-v26.10.0-linux-s390x.tar.xz'],
['darwin', 'arm64', 'a2 node-v26.10.0-darwin-arm64.tar.xz'],
['linux', 'ppc64le', ''],
['win', 'x64', ''],
];
avecDossier({ 'SHASUMS256.txt': SOMMES }, (dossier) => {
for (const [os, arch, attendu] of CAS) {
const { sortie } = bash(`gtt_archive_node "${join(dossier, 'SHASUMS256.txt')}" ${os} ${arch}`);
assert.equal(sortie, attendu, `${os}-${arch}`);
}
});
});
});
describe('installation : .node-version, dépendances, consentement', { skip: SOUS_WINDOWS }, () => {
test('la majeure de .node-version est un entier, et rien d’autre ne passe', () => {
const CAS = [['26\n', 0, '26'], [' 26 \r\n', 0, '26'], ['v26\n', 1, ''], ['26.10\n', 1, ''], ['', 1, '']];
for (const [contenu, statut, sortie] of CAS) {
avecDossier({ '.node-version': contenu }, (dossier) => {
const r = bash(`GTT_RACINE="${dossier}"\ngtt_node_majeure`);
assert.deepEqual([r.statut, r.sortie], [statut, sortie], JSON.stringify(contenu));
if (statut !== 0) assert.match(r.erreurs, /\.node-version doit porter une majeure entière/);
});
}
});
test('les dépendances suivent le verrou tant que node_modules/.package-lock.json n’est pas plus ancien', () => {
avecDossier({ 'package-lock.json': '{}', 'node_modules/.package-lock.json': '{}' }, (dossier) => {
const aJour = () => bash(`GTT_RACINE="${dossier}"\ngtt_dependances_a_jour`).statut;
const date = (chemin, secondes) => utimesSync(join(dossier, chemin), secondes, secondes);
date('package-lock.json', 1_000_000);
date('node_modules/.package-lock.json', 2_000_000);
assert.equal(aJour(), 0, 'installation postérieure au verrou');
date('package-lock.json', 3_000_000);
assert.equal(aJour(), 1, 'verrou modifié depuis l’installation');
rmSync(join(dossier, 'node_modules'), { recursive: true });
assert.equal(aJour(), 1, 'aucune installation');
});
});
test('sans terminal, rien ne consent ; GTT_OUI=1 consent d’avance', () => {
assert.equal(bash('gtt_confirmer "Question ?"').statut, 1);
assert.equal(bash('gtt_confirmer "Question ?"', { GTT_OUI: '1' }).statut, 0);
});
test('chaque famille qui pose des paquets a ses outils, ses bibliothèques d’Electron et ses outils de développement', () => {
for (const famille of ['debian', 'fedora', 'suse', 'arch']) {
for (const liste of ['gtt_paquets_outils', 'gtt_paquets_electron', 'gtt_paquets_developpement']) {
assert.notEqual(bash(`${liste} ${famille}`).sortie, '', `${liste} ${famille}`);
}
assert.match(bash(`gtt_paquets_electron ${famille}`).sortie, /gtk/, famille);
assert.match(bash(`gtt_paquets_outils ${famille}`).sortie, /\bmake\b/, famille);
}
});
});

View file

@ -0,0 +1,43 @@
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
#
# Environnement de développement sous NixOS : install.sh, run.sh et
# avec_node.sh s'y relancent d'eux-mêmes. Les binaires que les autres
# systèmes téléchargent — Node, Electron, le Chromium des épreuves —
# attendent un chargeur dynamique et des bibliothèques à des chemins que
# NixOS n'a pas ; ils viennent ici de nixpkgs. nixpkgs est épinglé par
# révision et par somme : l'environnement est le même sur toute machine, et
# ne dépend pas du canal du système.
#
# nix-shell scripts/installation/shell.nix
let
# nixos-26.05 : Node 26.10, Electron 43, Chromium 154.
nixpkgs = fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/b25309931cfda5f0b8805f462a29897eeae50168.tar.gz";
sha256 = "1svxsx57g7x76cjsgpw5vvg0l194mshk57fdvdqqgd5xdy0ari58";
};
pkgs = import nixpkgs { };
# La majeure de Node vient de .node-version, seule source du projet.
majeure = builtins.replaceStrings [ "\n" "\r" " " ] [ "" "" "" ]
(builtins.readFile ../../.node-version);
in
pkgs.mkShell {
packages = [
pkgs."nodejs_${majeure}"
pkgs.electron
pkgs.chromium
pkgs.gnumake
pkgs.file
];
# Les scripts du projet savent par là qu'ils s'exécutent ici.
GTT_SHELL_NIX = "1";
# Le paquet npm electron lance le binaire de ce dossier au lieu du sien.
ELECTRON_OVERRIDE_DIST_PATH = "${pkgs.electron}/bin";
# Les épreuves du navigateur lancent ce Chromium : celui que télécharge
# Playwright ne démarre pas sous NixOS (vitest.config.js).
GTT_CHROMIUM = "${pkgs.chromium}/bin/chromium";
# Des polices pour Chromium et Electron, que les autres systèmes reçoivent
# d'install_dev.sh : un NixOS minimal n'en déclare aucune, et un texte s'y
# dessine sans largeur.
FONTCONFIG_FILE = pkgs.makeFontsConf { fontDirectories = [ pkgs.dejavu_fonts ]; };
}

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"

View file

@ -0,0 +1,33 @@
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
# Exécute une commande à la racine du projet, le Node du projet en tête du
# PATH, une fois l'installation vérifiée : c'est par lui que le Makefile
# lance npm, npx et node sous Windows.
#
# powershell -File scripts\windows\avec_node.ps1 npm test
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Commande)
. (Join-Path $PSScriptRoot 'commun.ps1')
$elements = @($Commande | Where-Object { $_ })
if ($elements.Count -eq 0) {
Write-Gtt 'usage : scripts\windows\avec_node.ps1 <commande> [argument…]'
exit 2
}
Initialize-Gtt
Push-Location $script:GttRacine
try {
$programme = $elements[0]
# npm et npx se lancent par leur .cmd : leur variante .ps1 dépend de la
# stratégie d'exécution de la machine.
if (@('npm', 'npx') -contains $programme) { $programme = "$programme.cmd" }
if ($elements.Count -gt 1) {
$reste = $elements[1..($elements.Count - 1)]
& $programme @reste
} else {
& $programme
}
exit $LASTEXITCODE
} finally {
Pop-Location
}

171
scripts/windows/commun.ps1 Normal file
View file

@ -0,0 +1,171 @@
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
# Fonctions communes des scripts Windows : le Node du projet, posé hors du
# dépôt sous %LOCALAPPDATA%, et la vérification de l'installation. Ce
# fichier se charge par « . ». Il s'en tient à Windows PowerShell 5.1, livré
# avec Windows : ni ??, ni ?., ni opérateur ternaire. Il est enregistré en
# UTF-8 avec marque d'ordre d'octets, sans laquelle Windows PowerShell 5.1 le
# lit dans la page de code du système et défigure les accents.
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3.0
# Sans ces réglages, Windows PowerShell 5.1 écrit les accents dans la page de
# code de la console, peut refuser TLS 1.2 que nodejs.org exige, et ralentit
# chaque téléchargement par sa barre de progression.
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$ProgressPreference = 'SilentlyContinue'
$script:GttRacine = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
function Write-Gtt([string]$Message) {
[Console]::Error.WriteLine($Message)
}
function Stop-Gtt([string]$Message) {
Write-Gtt "Refusé : $Message"
exit 1
}
# Majeure de Node que vise le projet ; .node-version en est la seule source.
function Get-GttMajeure {
$majeure = (Get-Content -Raw (Join-Path $script:GttRacine '.node-version')).Trim()
if ($majeure -notmatch '^[0-9]+$') {
Stop-Gtt ".node-version doit porter une majeure entière : « $majeure »."
}
return $majeure
}
# Architecture au sens de nodejs.org. PROCESSOR_ARCHITEW6432 nomme celle du
# système quand PowerShell lui-même tourne en 32 bits.
function Get-GttArchNode {
$arch = $env:PROCESSOR_ARCHITEW6432
if (-not $arch) { $arch = $env:PROCESSOR_ARCHITECTURE }
switch ($arch) {
'AMD64' { return 'x64' }
'ARM64' { return 'arm64' }
default { Stop-Gtt "architecture $arch : nodejs.org n'y publie pas Node pour Windows." }
}
}
function Get-GttDossierDonnees {
return (Join-Path $env:LOCALAPPDATA 'gestion_table_tournante_libre')
}
function Get-GttDossierNode {
return (Join-Path (Get-GttDossierDonnees) ('node-{0}-win-{1}' -f (Get-GttMajeure), (Get-GttArchNode)))
}
# Somme et nom de l'archive .zip d'une architecture dans les lignes d'un
# SHASUMS256.txt de nodejs.org ; $null quand elle n'y figure pas.
function Find-GttArchiveNode([string[]]$Lignes, [string]$Arch) {
$suffixe = "-win-$Arch.zip"
foreach ($ligne in $Lignes) {
$champs = @($ligne.Trim() -split '\s+')
if ($champs.Count -ge 2 -and $champs[1] -match '^node-v[0-9]+\.[0-9]+\.[0-9]+-' -and
$champs[1].EndsWith($suffixe, [StringComparison]::Ordinal)) {
return @($champs[0], $champs[1])
}
}
return $null
}
# Pose la dernière version publiée de la majeure voulue, une fois. L'archive
# est vérifiée contre la somme que publie nodejs.org, puis extraite à côté de
# la cible et renommée : une extraction interrompue ne laisse jamais une
# cible à moitié écrite.
function Install-GttNode {
$majeure = Get-GttMajeure
$arch = Get-GttArchNode
$cible = Get-GttDossierNode
$node = Join-Path $cible 'node.exe'
if (Test-Path $node) {
Write-Gtt ('Node {0} : {1}' -f (& $node --version), $cible)
return
}
$cache = Join-Path (Get-GttDossierDonnees) 'cache'
New-Item -ItemType Directory -Force -Path $cache | Out-Null
$base = "https://nodejs.org/dist/latest-v$majeure.x"
$sommes = Join-Path $cache "SHASUMS256-v$majeure.txt"
Invoke-WebRequest -UseBasicParsing -Uri "$base/SHASUMS256.txt" -OutFile $sommes
$trouve = Find-GttArchiveNode (Get-Content $sommes) $arch
if (-not $trouve) { Stop-Gtt "nodejs.org ne publie pas Node $majeure pour win-$arch." }
$somme = $trouve[0]
$nom = $trouve[1]
$archive = Join-Path $cache $nom
if (-not (Test-Path $archive) -or (Get-FileHash -Algorithm SHA256 $archive).Hash -ne $somme) {
Write-Gtt "Téléchargement de $nom"
$partiel = "$archive.$PID.partiel"
Invoke-WebRequest -UseBasicParsing -Uri "$base/$nom" -OutFile $partiel
if ((Get-FileHash -Algorithm SHA256 $partiel).Hash -ne $somme) {
Remove-Item -Force $partiel
Stop-Gtt "$nom : la somme SHA-256 ne correspond pas à celle que publie nodejs.org."
}
Move-Item -Force $partiel $archive
}
$tmp = Join-Path (Get-GttDossierDonnees) ('extraction.' + [guid]::NewGuid().ToString('N'))
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($archive, $tmp)
if (Test-Path $cible) { Remove-Item -Recurse -Force $cible }
Move-Item (Join-Path $tmp ([IO.Path]::GetFileNameWithoutExtension($nom))) $cible
Remove-Item -Force $tmp
Write-Gtt ('Node {0} posé : {1}' -f (& $node --version), $cible)
}
# Met le Node du projet en tête du PATH de la séance ; faux quand il n'est
# pas posé.
function Enable-GttNode {
$cible = Get-GttDossierNode
if (-not (Test-Path (Join-Path $cible 'node.exe'))) { return $false }
$env:Path = "$cible;$env:Path"
return $true
}
# Vrai quand les dépendances npm suivent package-lock.json : npm écrit
# node_modules\.package-lock.json à chaque installation.
function Test-GttDependances {
$recu = Join-Path $script:GttRacine 'node_modules\.package-lock.json'
if (-not (Test-Path $recu)) { return $false }
$verrou = Get-Item (Join-Path $script:GttRacine 'package-lock.json')
return ($verrou.LastWriteTimeUtc -le (Get-Item $recu).LastWriteTimeUtc)
}
# Vrai sur une réponse affirmative — o, oui, y, yes. Le défaut est non, et
# une entrée redirigée ne consent à rien. GTT_OUI=1 répond oui d'avance.
function Confirm-Gtt([string]$Question) {
if ($env:GTT_OUI -eq '1') { return $true }
if ([Console]::IsInputRedirected) { return $false }
$reponse = Read-Host "$Question [o/N]"
return (@('o', 'oui', 'y', 'yes') -contains $reponse.Trim().ToLowerInvariant())
}
# Exécute un programme et refuse quand il échoue, en le nommant.
function Invoke-Gtt([string]$Programme, [string[]]$Arguments) {
& $Programme @Arguments
if ($LASTEXITCODE -ne 0) { Stop-Gtt "$Programme $($Arguments -join ' ') échoue (code $LASTEXITCODE)." }
}
function Invoke-GttNpmCi {
Invoke-Gtt 'npm.cmd' @('ci', '--no-audit', '--no-fund')
}
# Rend prêt à exécuter : le Node du projet en tête du PATH, et des
# dépendances npm qui suivent package-lock.json. Node absent, il propose
# install.cmd ; des dépendances en retard se réinstallent sans question.
function Initialize-Gtt {
if (-not (Enable-GttNode)) {
Write-Gtt "Node $(Get-GttMajeure) n'est pas posé."
if (-not (Confirm-Gtt 'Lancer install.cmd (Node, dépendances npm) ?')) {
Stop-Gtt 'installation non lancée. Lancer install.cmd, puis recommencer.'
}
& (Join-Path $PSScriptRoot 'installer.ps1')
if (-not (Enable-GttNode)) { Stop-Gtt "Node $(Get-GttMajeure) reste introuvable après l'installation." }
}
if (-not (Test-GttDependances)) {
Write-Gtt 'Les dépendances npm ne suivent pas package-lock.json : npm ci.'
Push-Location $script:GttRacine
try { Invoke-GttNpmCi } finally { Pop-Location }
}
}

View file

@ -0,0 +1,23 @@
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
# Installe sous Windows de quoi lancer l'application depuis les sources et
# jouer les épreuves : Node de la majeure de .node-version, posé sous
# %LOCALAPPDATA% sans droits d'administrateur, et les dépendances de
# package-lock.json. -Dev ajoute le Chromium de Playwright des épreuves du
# navigateur. install.cmd et install_dev.cmd l'appellent.
param([switch]$Dev)
. (Join-Path $PSScriptRoot 'commun.ps1')
Install-GttNode
[void](Enable-GttNode)
Push-Location $script:GttRacine
try {
Invoke-GttNpmCi
if ($Dev) {
Invoke-Gtt 'npx.cmd' @('--no-install', 'playwright', 'install', '--only-shell', 'chromium')
}
} finally {
Pop-Location
}
Write-Gtt 'Installation terminée. Lancer : run.cmd'

View file

@ -0,0 +1,38 @@
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
# Lance l'application depuis les sources, sous Windows : la coquille
# Electron charge la page que Vite construit dans www\ (§ 13.1). run.cmd
# l'appelle, ses options sont celles de run.sh.
#
# run.cmd Electron
# run.cmd --web le serveur de Vite, http://localhost:5173
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Options)
. (Join-Path $PSScriptRoot 'commun.ps1')
$mode = 'electron'
foreach ($option in @($Options | Where-Object { $_ })) {
switch ($option) {
'--web' { $mode = 'web' }
'--electron' { $mode = 'electron' }
default {
Write-Gtt 'usage : run.cmd [--web | --electron]'
exit 2
}
}
}
Initialize-Gtt
Push-Location $script:GttRacine
try {
if ($mode -eq 'web') {
Write-Gtt "Serveur de développement de Vite : http://localhost:5173 (Ctrl+C pour l'arrêter)."
& npm.cmd run --silent dev
exit $LASTEXITCODE
}
Invoke-Gtt 'npm.cmd' @('run', '--silent', 'build')
& npx.cmd --no-install electron .
exit $LASTEXITCODE
} finally {
Pop-Location
}

12
test/chromium.js Normal file
View file

@ -0,0 +1,12 @@
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Options du Chromium des épreuves du navigateur, que vitest.config.js passe
// au fournisseur Playwright. Playwright lance par défaut le Chromium qu'il a
// téléchargé ; GTT_CHROMIUM désigne un autre exécutable. Sous NixOS, où les
// binaires téléchargés ne démarrent pas, scripts/installation/shell.nix y
// met celui de nixpkgs.
export function optionsChromium(env) {
const chemin = env.GTT_CHROMIUM;
return chemin ? { launchOptions: { executablePath: chemin } } : {};
}

27
test/chromium.test.js Normal file
View file

@ -0,0 +1,27 @@
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le Chromium des épreuves du navigateur : celui de Playwright par défaut,
// celui que GTT_CHROMIUM désigne sinon, et vitest.config.js applique la règle.
import assert from 'node:assert/strict';
import configuration from '../vitest.config.js';
import { optionsChromium } from './chromium.js';
import { describe, test } from './lanceur.js';
describe('chromium : l’exécutable des épreuves du navigateur', () => {
test('sans GTT_CHROMIUM, Playwright lance le Chromium qu’il a téléchargé', () => {
assert.deepEqual(optionsChromium({}), {});
assert.deepEqual(optionsChromium({ GTT_CHROMIUM: '' }), {});
});
test('GTT_CHROMIUM désigne l’exécutable que Playwright lance', () => {
assert.deepEqual(optionsChromium({ GTT_CHROMIUM: '/nix/store/abc-chromium/bin/chromium' }), {
launchOptions: { executablePath: '/nix/store/abc-chromium/bin/chromium' },
});
});
test('vitest.config.js passe au projet navigateur les options de cet environnement', () => {
const navigateur = configuration.test.projects.find(({ test: { name } }) => name === 'navigateur');
assert.deepEqual(navigateur.test.browser.provider.options, optionsChromium(process.env));
});
});

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}"/);
});
});

View file

@ -3,12 +3,13 @@
// Chaque fichier source du projet s'ouvre sur l'en-tête de licence : la ligne
// du titulaire, puis celle de la licence, après un éventuel shebang ou la
// déclaration <!doctype html>. Les deux lignes sont des commentaires du
// déclaration <!doctype html>, et après la marque d'ordre d'octets que
// portent les scripts PowerShell. Les deux lignes sont des commentaires du
// langage du fichier ; le texte ne varie pas. package.json déclare la même
// licence et le même titulaire, que reprennent les outils d'emballage.
import assert from 'node:assert/strict';
import { readdirSync, readFileSync } from 'node:fs';
import { extname, join, relative, sep } from 'node:path';
import { basename, extname, join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, test } from './lanceur.js';
@ -17,8 +18,11 @@ const RACINE = fileURLToPath(new URL('..', import.meta.url));
const TITULAIRE = /^© \d{4} TechnoLibre \(http:\/\/www\.technolibre\.ca\)$/;
const LICENCE = 'License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)';
// Marqueurs de commentaire, par extension de fichier source. Un commentaire
// de ligne n'a pas de fermant.
// Marqueurs de commentaire, par extension de fichier source, ou par nom pour
// un fichier sans extension. Un commentaire de ligne n'a pas de fermant. Un
// ouvrant est une chaîne qui ouvre la ligne, ou un motif : REM ouvre une
// ligne de commandes Windows, précédé ou non de @, quelle que soit sa casse,
// et suivi d'un blanc — @REMARQUE est une commande.
const COMMENTAIRE = new Map([
['.js', { ouvrant: '//', fermant: null }],
['.cjs', { ouvrant: '//', fermant: null }],
@ -27,8 +31,16 @@ const COMMENTAIRE = new Map([
['.svelte', { ouvrant: '<!--', fermant: '-->' }],
['.html', { ouvrant: '<!--', fermant: '-->' }],
['.css', { ouvrant: '/*', fermant: '*/' }],
['.ps1', { ouvrant: '#', fermant: null }],
['.nix', { ouvrant: '#', fermant: null }],
['.cmd', { ouvrant: /^@?rem(?=\s)/i, fermant: null }],
['Makefile', { ouvrant: '#', fermant: null }],
]);
// Clé d'un fichier dans COMMENTAIRE : son nom quand il y figure, son
// extension sinon.
const cle = (chemin) => (COMMENTAIRE.has(basename(chemin)) ? basename(chemin) : extname(chemin));
// Périmètre : les fichiers de la racine et les arbres des sources, des
// épreuves, des scripts et de la coquille. Les dépendances et les sorties de
// construction n'y entrent pas, ni les données d'épreuve, qui sont des
@ -55,7 +67,7 @@ function fichiersSources() {
}
}
return chemins
.filter((chemin) => COMMENTAIRE.has(extname(chemin)))
.filter((chemin) => COMMENTAIRE.has(cle(chemin)))
.filter((chemin) => !chemin.startsWith(DONNEES_EPREUVE))
.sort();
}
@ -69,8 +81,9 @@ function fichiersSources() {
// invalidée dans une feuille, une commande dans un script.
function enCommentaire(premiere, seconde, { ouvrant, fermant }) {
const suite = seconde.trim();
if (!premiere.startsWith(ouvrant)) return false;
if (fermant === null) return suite.startsWith(ouvrant);
const ouvre = (ligne) => (typeof ouvrant === 'string' ? ligne.startsWith(ouvrant) : ouvrant.test(ligne));
if (!ouvre(premiere)) return false;
if (fermant === null) return ouvre(suite);
const courant = !premiere.includes(fermant) && suite.endsWith(fermant);
const repete =
premiere.trimEnd().endsWith(fermant) &&
@ -79,21 +92,22 @@ function enCommentaire(premiere, seconde, { ouvrant, fermant }) {
return courant || repete;
}
// Vrai quand le contenu s'ouvre sur l'en-tête, après un éventuel shebang ou
// la déclaration <!doctype html> : deux lignes de commentaire du langage de
// l'extension, dont le texte, débarrassé des marqueurs, est celui de
// l'en-tête.
function porteLEnTete(contenu, extension) {
let lignes = contenu.split('\n');
// Vrai quand le contenu s'ouvre sur l'en-tête, après une éventuelle marque
// d'ordre d'octets, un shebang ou la déclaration <!doctype html> : deux
// lignes de commentaire du langage de la clé, extension ou nom, dont le
// texte, débarrassé des marqueurs, est celui de l'en-tête. Les fins de ligne
// sont celles d'Unix ou de Windows.
function porteLEnTete(contenu, cleFichier) {
let lignes = contenu.replace(/^\uFEFF/, '').split(/\r?\n/);
if (lignes[0].startsWith('#!')) lignes = lignes.slice(1);
if (/^<!doctype html>$/i.test(lignes[0])) lignes = lignes.slice(1);
const [premiere = '', seconde = ''] = lignes;
const texte = (ligne) =>
ligne
.replace(/^\s*(?:\/\/|#|<!--|\/\*|\*)?\s*/, '')
.replace(/^\s*(?:\/\/|#|<!--|\/\*|\*|@?rem(?=\s))?\s*/i, '')
.replace(/\s*(?:-->|\*\/)?\s*$/, '');
return (
enCommentaire(premiere, seconde, COMMENTAIRE.get(extension)) &&
enCommentaire(premiere, seconde, COMMENTAIRE.get(cleFichier)) &&
TITULAIRE.test(texte(premiere)) &&
texte(seconde) === LICENCE
);
@ -104,7 +118,7 @@ describe('licence', () => {
const fichiers = fichiersSources();
assert.ok(fichiers.length > 0, 'aucun fichier source examiné');
const sansEnTete = fichiers
.filter((chemin) => !porteLEnTete(readFileSync(chemin, 'utf8'), extname(chemin)))
.filter((chemin) => !porteLEnTete(readFileSync(chemin, 'utf8'), cle(chemin)))
.map((chemin) => relative(RACINE, chemin));
assert.deepEqual(sansEnTete, []);
});
@ -129,6 +143,16 @@ describe('licence', () => {
['.html', `<!-- ${titulaire}\n ${LICENCE}\n-->\n`, false],
['.css', `/* ${titulaire} */\n${LICENCE}\n`, false],
['.css', `/* ${titulaire} */\n * ${LICENCE} */\n`, false],
['.ps1', `\uFEFF# ${titulaire}\r\n# ${LICENCE}\r\n`, true],
['.ps1', `# ${titulaire}\n# ${LICENCE}\n`, true],
['.nix', `# ${titulaire}\n# ${LICENCE}\n`, true],
['.cmd', `@REM ${titulaire}\r\n@REM ${LICENCE}\r\n@echo off\r\n`, true],
['.cmd', `REM ${titulaire}\r\nrem ${LICENCE}\r\n`, true],
['Makefile', `# ${titulaire}\n# ${LICENCE}\n`, true],
['.ps1', `\uFEFF# ${titulaire}\r\nWrite-Host '${LICENCE}'\r\n`, false],
['.cmd', `@REM ${titulaire}\r\n@echo ${LICENCE}\r\n`, false],
['.cmd', `@REMARQUE ${titulaire}\r\n@REM ${LICENCE}\r\n`, false],
['Makefile', `# ${titulaire}\n\t${LICENCE}\n`, false],
];
const ecarts = cas
.filter(([extension, contenu, admis]) => porteLEnTete(contenu, extension) !== admis)

View file

@ -16,6 +16,7 @@
// d'accord avec la première.
import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
import { optionsChromium } from './test/chromium.js';
import { coeursDisponibles, reglagesSeries } from './test/machine.js';
// Les séries node suivent le nombre de cœurs de la machine (test/machine.js) :
@ -69,7 +70,7 @@ export default defineConfig({
test: {
name: 'navigateur',
include: ['src/**/*.navigateur.test.js'],
browser: { enabled: true, headless: true, provider: playwright(),
browser: { enabled: true, headless: true, provider: playwright(optionsChromium(process.env)),
instances: [{ browser: 'chromium' }] } } },
],
},