[ADD] run: make and ./run.sh, Electron start check, container matrix
make with no target does what ./run.sh does: Electron on a graphical session, Vite's server without one or where Electron is not published (s390x, ppc64le). Every npm, npx and node recipe goes through the project's Node. make essai_demarrage starts the real Electron through Playwright and reads its window title, on Chromium's headless Ozone platform when there is no display: no X server is needed. scripts/verifier_systemes.sh replays install and tests in podman over the QEMU catalogue of TODO, and stops only its own containers. Checked: 17 Linux images and Nix pass all six steps; pwsh parses. --- FR --- [ADD] lancement : make et ./run.sh, essai d'Electron, matrice make sans cible fait ce que fait ./run.sh : Electron sur une session graphique, le serveur de Vite sans elle ou là où Electron n'est pas publié (s390x, ppc64le). Chaque recette npm, npx ou node passe par le Node du projet. make essai_demarrage lance la vraie coquille Electron par Playwright et lit le titre de sa fenêtre, sur la plateforme Ozone headless de Chromium sans écran : aucun serveur X n'est requis. scripts/verifier_systemes.sh rejoue installation et épreuves sous podman sur le catalogue QEMU de TODO, et n'arrête que ses conteneurs. Vérifié : 17 images Linux et Nix passent les six étapes ; pwsh analyse. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
This commit is contained in:
parent
d83362166a
commit
b9090508ff
6 changed files with 635 additions and 8 deletions
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`.
|
||||
|
|
|
|||
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[@]}"}
|
||||
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);
|
||||
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"
|
||||
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}"/);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue