param( [switch]$GuiV6, [switch]$SkipZip, [switch]$SkipInstaller, [switch]$SkipRequirements, [switch]$Sign, [string]$CertThumbprint, [string]$PfxPath, [string]$PfxPassword, [string]$TimestampServer = "http://timestamp.digicert.com" ) $ErrorActionPreference = "Stop" $script:SignatureSummary = "Non signé" function Write-Step { param([string]$Message) Write-Host "" Write-Host "=== $Message ===" -ForegroundColor Cyan } function Require-Path { param( [string]$PathValue, [string]$Label ) if (-not (Test-Path $PathValue)) { throw "$Label introuvable: $PathValue" } } function Invoke-BootstrapPython { param([string[]]$Arguments) if ($script:PythonBootstrap[0] -eq "py") { & py $script:PythonBootstrap[1] @Arguments } else { & $script:PythonBootstrap[0] @Arguments } } function Resolve-BootstrapPython { if (Get-Command py -ErrorAction SilentlyContinue) { try { & py -3.11 --version | Out-Host if ($LASTEXITCODE -eq 0) { return @("py", "-3.11") } } catch {} try { & py -3 --version | Out-Host if ($LASTEXITCODE -eq 0) { return @("py", "-3") } } catch {} } if (Get-Command python -ErrorAction SilentlyContinue) { & python --version | Out-Host if ($LASTEXITCODE -eq 0) { return @("python") } } throw "Python introuvable sur la machine de build Windows." } function Resolve-SignTool { $command = Get-Command signtool.exe -ErrorAction SilentlyContinue if ($command) { return $command.Source } $programFilesX86 = ${env:ProgramFiles(x86)} if ($programFilesX86) { $kitsRoot = Join-Path $programFilesX86 "Windows Kits\10\bin" if (Test-Path $kitsRoot) { $candidates = @( Get-ChildItem -Path $kitsRoot -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match "\\x64\\signtool\.exe$" } | Sort-Object FullName -Descending ) if ($candidates.Count -gt 0) { return $candidates[0].FullName } } } throw "signtool.exe introuvable. Installer Windows SDK ou ajouter signtool.exe au PATH." } function Resolve-InnoCompiler { $command = Get-Command ISCC.exe -ErrorAction SilentlyContinue if ($command) { return $command.Source } $candidates = @() if (${env:ProgramFiles(x86)}) { $candidates += (Join-Path ${env:ProgramFiles(x86)} "Inno Setup 6\ISCC.exe") } if ($env:ProgramFiles) { $candidates += (Join-Path $env:ProgramFiles "Inno Setup 6\ISCC.exe") } if ($env:LOCALAPPDATA) { $candidates += (Join-Path $env:LOCALAPPDATA "Programs\Inno Setup 6\ISCC.exe") $candidates += (Join-Path $env:LOCALAPPDATA "Inno Setup 6\ISCC.exe") } foreach ($candidate in $candidates) { if ($candidate -and (Test-Path $candidate)) { return $candidate } } return $null } function Invoke-CodeSigning { param([string]$FilePath) if (-not $Sign) { Write-Host "Signature Authenticode ignorée. Utiliser -Sign pour signer l'exécutable." return } Require-Path -PathValue $FilePath -Label "Fichier à signer" if ($PfxPath) { Require-Path -PathValue $PfxPath -Label "Certificat PFX" } $signTool = Resolve-SignTool Write-Host "SignTool : $signTool" if ($CertThumbprint -eq "REMPLACER_PAR_L_EMPREINTE_DU_CERTIFICAT") { throw "Empreinte de certificat non renseignée dans build_signing.local.ps1." } $args = @("sign", "/fd", "SHA256", "/tr", $TimestampServer, "/td", "SHA256", "/d", "Anonymisation") if ($PfxPath) { $args += @("/f", $PfxPath) if ($PfxPassword) { $args += @("/p", $PfxPassword) } } elseif ($CertThumbprint) { $args += @("/sha1", ($CertThumbprint -replace "\s", "")) } else { $args += @("/a") } $args += $FilePath & $signTool @args if ($LASTEXITCODE -ne 0) { throw "La signature Authenticode a échoué." } & $signTool verify /pa /v $FilePath if ($LASTEXITCODE -ne 0) { throw "La vérification Authenticode a échoué." } $signature = Get-AuthenticodeSignature $FilePath $subject = "" if ($signature.SignerCertificate) { $subject = $signature.SignerCertificate.Subject } $script:SignatureSummary = "$($signature.Status) - $subject" Write-Host "Signature : $script:SignatureSummary" if ($signature.Status -ne "Valid") { throw "Signature Authenticode non valide : $($signature.Status)" } } $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ProjectRoot = (Resolve-Path (Join-Path $ScriptDir "..")).Path $SigningConfigPath = Join-Path $ProjectRoot "build_signing.local.ps1" $BuildFlavor = if ($GuiV6) { "GUI V6" } else { "GUI historique" } $SpecFileName = if ($GuiV6) { "anonymisation_gui_v6_onefile.spec" } else { "anonymisation_onefile.spec" } $SpecPath = Join-Path $ProjectRoot $SpecFileName $InstallerScriptPath = Join-Path $ProjectRoot "installer\Anonymisation.iss" $BuildInfoPath = Join-Path $ProjectRoot "build_info.py" $ModelPath = Join-Path $ProjectRoot "models\camembert-bio-deid\onnx\model.onnx" $VenvDir = Join-Path $ProjectRoot ".venv_build_win" $VenvPython = Join-Path $VenvDir "Scripts\python.exe" $DistDir = Join-Path $ProjectRoot "dist" $BuildDir = Join-Path $ProjectRoot "build" $ReleaseDir = Join-Path $ProjectRoot "release" $ExePath = Join-Path $DistDir "Anonymisation.exe" $PackageDir = Join-Path $ReleaseDir "Anonymisation-Windows" $ZipPath = Join-Path $ReleaseDir "Anonymisation-Windows.zip" $HashPath = Join-Path $ReleaseDir "Anonymisation.exe.sha256.txt" $InstallerPath = Join-Path $ReleaseDir "Anonymisation-Setup.exe" $ReadmePath = Join-Path $PackageDir "README.txt" if ($GuiV6) { $RequiredSourceFiles = @( "Pseudonymisation_Gui_V6.py", "gui_v6\app.py", "gui_v6\theme.py", "gui_v6\ui_kit.py", "gui_v6\engine_bridge.py", "gui_v6\processing_runner.py", "gui_v6\config_state.py", "gui_v6\license_client.py", "gui_v6\license_store.py", "gui_v6\machine_id.py", "anonymizer_core_refactored_onnx.py", "admin_rules.py", "config_defaults.py", "profile_defaults.py", "gui_batch_paths.py", "manual_masking.py", "pdf_mask_designer.py", "format_converter.py", "camembert_ner_manager.py" ) } else { $RequiredSourceFiles = @( "launcher.py", "Pseudonymisation_Gui_V5.py", "anonymizer_core_refactored_onnx.py", "admin_rules.py", "config_defaults.py", "profile_defaults.py", "gui_batch_paths.py", "manual_masking.py", "pdf_mask_designer.py", "format_converter.py", "camembert_ner_manager.py" ) } Write-Step "Préparation du build Windows" Write-Host "Projet : $ProjectRoot" Write-Host "Variante : $BuildFlavor" Write-Host "Spec PyInstaller : $SpecFileName" Require-Path -PathValue $SpecPath -Label "Spec PyInstaller" Require-Path -PathValue $InstallerScriptPath -Label "Script installateur Inno Setup" Require-Path -PathValue $ModelPath -Label "Modèle ONNX embarqué" foreach ($RelativeSourceFile in $RequiredSourceFiles) { Require-Path -PathValue (Join-Path $ProjectRoot $RelativeSourceFile) -Label "Module source requis" } if (Test-Path $SigningConfigPath) { Write-Step "Configuration locale de signature" . $SigningConfigPath if ($BuildSigningEnabled) { $Sign = $true } if ($BuildSigningCertThumbprint -and -not $CertThumbprint) { $CertThumbprint = $BuildSigningCertThumbprint } if ($BuildSigningPfxPath -and -not $PfxPath) { $PfxPath = $BuildSigningPfxPath } if ($BuildSigningPfxPassword -and -not $PfxPassword) { $PfxPassword = $BuildSigningPfxPassword } if ($BuildSigningTimestampServer -and $TimestampServer -eq "http://timestamp.digicert.com") { $TimestampServer = $BuildSigningTimestampServer } if ($Sign) { Write-Host "Signature activée depuis build_signing.local.ps1" } } Write-Step "Détection de Python" $script:PythonBootstrap = Resolve-BootstrapPython Write-Host "Bootstrap Python : $($script:PythonBootstrap -join ' ')" Write-Step "Environnement virtuel de build" if (-not (Test-Path $VenvPython)) { Write-Host "Création du venv : $VenvDir" Invoke-BootstrapPython -Arguments @("-m", "venv", $VenvDir) } Require-Path -PathValue $VenvPython -Label "Python du venv" Push-Location $ProjectRoot try { Write-Step "Installation des dépendances de build" & $VenvPython -m pip install --upgrade pip setuptools wheel if (-not $SkipRequirements) { & $VenvPython -m pip install -r requirements.txt } & $VenvPython -m pip install pyinstaller Write-Step "Génération de build_info.py" $commit = "local" $branch = "local" if (Get-Command git -ErrorAction SilentlyContinue) { try { $gitCommit = (git rev-parse --short HEAD 2>$null | Out-String).Trim() if ($gitCommit) { $commit = $gitCommit } $gitBranch = (git rev-parse --abbrev-ref HEAD 2>$null | Out-String).Trim() if ($gitBranch) { $branch = $gitBranch } } catch {} } $buildDate = Get-Date -Format "yyyy-MM-dd HH:mm" $buildInfo = @" """Métadonnées de build - généré automatiquement par build_windows_oneclick.ps1.""" BUILD_DATE = "$buildDate" BUILD_COMMIT = "$commit" BUILD_BRANCH = "$branch" BUILD_FLAVOR = "$BuildFlavor" "@ Set-Content -Path $BuildInfoPath -Value $buildInfo -Encoding UTF8 Write-Host "Build info : $buildDate / $branch / $commit" Write-Step "Nettoyage des anciens artefacts" foreach ($PathValue in @($BuildDir, $DistDir, $PackageDir)) { if (Test-Path $PathValue) { Remove-Item -Recurse -Force $PathValue -ErrorAction SilentlyContinue } } if (Test-Path $ZipPath) { Remove-Item -Force $ZipPath -ErrorAction SilentlyContinue } if (Test-Path $HashPath) { Remove-Item -Force $HashPath -ErrorAction SilentlyContinue } if (Test-Path $InstallerPath) { Remove-Item -Force $InstallerPath -ErrorAction SilentlyContinue } Write-Step "Compilation PyInstaller" & $VenvPython -m PyInstaller --clean --noconfirm $SpecPath if ($LASTEXITCODE -ne 0) { throw "PyInstaller a échoué avec le code $LASTEXITCODE." } Write-Step "Vérification de l'exécutable" Require-Path -PathValue $ExePath -Label "Exécutable Windows" $exeSizeMb = [math]::Round((Get-Item $ExePath).Length / 1MB, 1) Write-Host "EXE créé : $ExePath ($exeSizeMb MB)" Write-Step "Signature Authenticode" Invoke-CodeSigning -FilePath $ExePath Write-Step "Préparation du dossier de livraison" New-Item -ItemType Directory -Force -Path $PackageDir | Out-Null Copy-Item $ExePath (Join-Path $PackageDir "Anonymisation.exe") $readme = @" Anonymisation - paquet Windows ================================ Fichier principal : - Anonymisation.exe Conseils de diffusion : - Aucune installation de Python n'est nécessaire pour l'utilisateur final. - Conservez le fichier dans un dossier en écriture (par exemple Bureau ou Documents). - Privilégiez une diffusion par partage réseau interne, Intune, GPO ou portail établissement. - Évitez l'envoi direct par e-mail ou téléchargement public non signé. - Le journal applicatif s'écrit à côté de l'exécutable : anonymisation.log Build : - Date : $buildDate - Branche : $branch - Commit : $commit - Variante : $BuildFlavor - Signature : $script:SignatureSummary "@ Set-Content -Path $ReadmePath -Value $readme -Encoding UTF8 $hash = (Get-FileHash -Algorithm SHA256 $ExePath).Hash Set-Content -Path $HashPath -Value "SHA256 Anonymisation.exe $hash" -Encoding UTF8 Write-Host "SHA256 : $hash" if (-not $SkipZip) { Write-Step "Création de l'archive de livraison" Compress-Archive -Path (Join-Path $PackageDir "*") -DestinationPath $ZipPath -CompressionLevel Optimal Write-Host "Archive créée : $ZipPath" } if (-not $SkipInstaller) { Write-Step "Création de l'installateur Windows" $innoCompiler = Resolve-InnoCompiler if ($innoCompiler) { Write-Host "Inno Setup Compiler : $innoCompiler" $installerVersion = (Get-Date -Format "yyyy.MM.dd.HHmm") & $innoCompiler "/DAppVersion=$installerVersion" $InstallerScriptPath if ($LASTEXITCODE -ne 0) { throw "Inno Setup a échoué avec le code $LASTEXITCODE." } Require-Path -PathValue $InstallerPath -Label "Installateur Windows" $installerSizeMb = [math]::Round((Get-Item $InstallerPath).Length / 1MB, 1) Write-Host "Installateur créé : $InstallerPath ($installerSizeMb MB)" if ($Sign) { Write-Step "Signature Authenticode de l'installateur" Invoke-CodeSigning -FilePath $InstallerPath } } else { Write-Warning "Inno Setup 6 introuvable. Installateur ignoré. Installer Inno Setup puis relancer le build." Write-Warning "Téléchargement officiel : https://jrsoftware.org/isdl.php" } } Write-Step "Build terminé" Write-Host "EXE final : $ExePath" -ForegroundColor Green if (-not $SkipZip) { Write-Host "Archive prête : $ZipPath" -ForegroundColor Green } if ((-not $SkipInstaller) -and (Test-Path $InstallerPath)) { Write-Host "Installateur prêt : $InstallerPath" -ForegroundColor Green } Write-Host "Hash SHA256 : $HashPath" -ForegroundColor Green } finally { Pop-Location }