Compare commits
3 commits
de46c582f4
...
b9090508ff
| Author | SHA1 | Date | |
|---|---|---|---|
| b9090508ff | |||
| d83362166a | |||
| 67368b719f |
26 changed files with 1891 additions and 30 deletions
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
* text=auto eol=lf
|
||||
*.cmd text eol=crlf
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
26
|
||||
108
Makefile
Normal file
108
Makefile
Normal 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
|
||||
50
README.md
50
README.md
|
|
@ -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
6
install.cmd
Normal 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
73
install.sh
Executable 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
6
install_dev.cmd
Normal 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
61
install_dev.sh
Executable 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
6
run.cmd
Normal 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
67
run.sh
Executable 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[@]}"}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
62
scripts/essai_demarrage.js
Normal file
62
scripts/essai_demarrage.js
Normal 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);
|
||||
22
scripts/installation/avec_node.sh
Executable file
22
scripts/installation/avec_node.sh
Executable 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 "$@"
|
||||
497
scripts/installation/commun.sh
Normal file
497
scripts/installation/commun.sh
Normal 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 ' "$@")"
|
||||
}
|
||||
183
scripts/installation/commun.test.js
Normal file
183
scripts/installation/commun.test.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
43
scripts/installation/shell.nix
Normal file
43
scripts/installation/shell.nix
Normal 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
244
scripts/verifier_systemes.sh
Executable 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"
|
||||
33
scripts/windows/avec_node.ps1
Normal file
33
scripts/windows/avec_node.ps1
Normal 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
171
scripts/windows/commun.ps1
Normal 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 }
|
||||
}
|
||||
}
|
||||
23
scripts/windows/installer.ps1
Normal file
23
scripts/windows/installer.ps1
Normal 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'
|
||||
38
scripts/windows/lancer.ps1
Normal file
38
scripts/windows/lancer.ps1
Normal 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
12
test/chromium.js
Normal 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
27
test/chromium.test.js
Normal 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
112
test/installation.test.js
Normal 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}"/);
|
||||
});
|
||||
});
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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' }] } } },
|
||||
],
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue