[ADD] install: Node 26 and system packages, per family and under Nix
./install.sh installs what is missing through apt, dnf, zypper or pacman, then Node of the .node-version major from nodejs.org, SHA-256 checked, outside the repository, then npm ci and Electron's binary, which its package no longer fetches at install; ldd refuses a missing library. ./install_dev.sh adds Playwright's Chromium, fonts, and podman on amd64. Under NixOS a pinned shell.nix provides Node, Electron and Chromium, which the browser tests then launch. Build-script tests run on Linux only, where § 16 builds. Checked: ten library tests, three mutations caught; local install. --- FR --- [ADD] installation : Node 26 et paquets du système, par famille et Nix ./install.sh installe ce qui manque par apt, dnf, zypper ou pacman, puis Node de la majeure de .node-version, de nodejs.org, somme SHA-256 vérifiée, hors du dépôt, puis npm ci et le binaire d'Electron, que son paquet ne télécharge plus à l'installation ; ldd refuse une bibliothèque absente. ./install_dev.sh ajoute le Chromium de Playwright, des polices, et podman sur amd64. Sous NixOS, un shell.nix épinglé fournit Node, Electron et Chromium, que lancent alors les épreuves du navigateur. Les épreuves du script de construction ne se jouent que sous Linux (§ 16). Vérifié : dix épreuves de la bibliothèque, trois mutations attrapées ; installation locale. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
This commit is contained in:
parent
67368b719f
commit
d83362166a
11 changed files with 931 additions and 6 deletions
1
.node-version
Normal file
1
.node-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
26
|
||||
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."
|
||||
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."
|
||||
|
|
@ -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
|
||||
|
|
|
|||
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 ]; };
|
||||
}
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
|
@ -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