gestion_table_tournante_libre/scripts/windows/commun.ps1
Mathieu Benoit 67368b719f [ADD] scripts: Windows install and run through PowerShell 5.1
install.cmd, install_dev.cmd and run.cmd hand over to PowerShell
scripts that place Node of the .node-version major under %LOCALAPPDATA%,
checked against the SHA-256 that nodejs.org publishes, then run npm ci:
no administrator right is needed. The scripts stay within Windows
PowerShell 5.1, shipped with Windows, and carry a UTF-8 byte order mark,
without which 5.1 reads them in the system code page and mangles
accents. The licence guard now reads .ps1, .cmd, .nix and Makefile.

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

--- FR ---

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

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

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

Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 05:51:31 -04:00

171 lines
7 KiB
PowerShell

# © 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 }
}
}