From d5154348f671240e104f1cc5fb80b90275ac561f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fou=C3=A9?= Date: Mon, 15 Jun 2026 09:08:28 +0200 Subject: [PATCH] Midified the package for windows (not a monolitic bin). And added the option to remove older versions in the setup. --- package/innosetup/build.ps1 | 32 ++++++++++--- package/innosetup/testium.iss | 55 +++++++++++++++++++++- package/pyinstaller/testium.spec | 80 +++++++++++++++++++++++--------- 3 files changed, 137 insertions(+), 30 deletions(-) diff --git a/package/innosetup/build.ps1 b/package/innosetup/build.ps1 index f652774..bf3fe80 100644 --- a/package/innosetup/build.ps1 +++ b/package/innosetup/build.ps1 @@ -1,16 +1,36 @@ -# Build the Testium installer from testium.iss (needs Inno Setup 6 / ISCC.exe). +# Build the Windows installer: PyInstaller one-folder build (fast start) + Inno Setup. # Install ISCC without admin: winget install --id JRSoftware.InnoSetup -e $ErrorActionPreference = 'Stop' $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Resolve-Path (Join-Path $scriptDir '..\..') +$pyiDir = Join-Path $repoRoot 'package\pyinstaller' -# The PyInstaller exe must exist first. -$exe = Join-Path $scriptDir '..\pyinstaller\dist\testium.exe' -if (-not (Test-Path $exe)) { - throw "PyInstaller build not found: $exe`nRun package\pyinstaller\build first." +# Locate PyInstaller: PATH first, then the known project venvs. +$pyi = (Get-Command pyinstaller.exe -ErrorAction SilentlyContinue).Source +if (-not $pyi) { + foreach ($p in @( + (Join-Path $repoRoot 'test\tmp\testium_venv\Scripts\pyinstaller.exe'), + (Join-Path $repoRoot 'test\tmp\.venv\Scripts\pyinstaller.exe'))) { + if (Test-Path $p) { $pyi = $p; break } + } +} +if (-not $pyi) { throw "pyinstaller.exe not found (PATH or project venv)." } + +# One-folder PyInstaller build => dist\testium\testium.exe + dist\testium\_internal\. +Write-Host "Building one-folder exe with: $pyi" +Remove-Item -Recurse -Force (Join-Path $pyiDir 'build'), (Join-Path $pyiDir 'dist') -ErrorAction SilentlyContinue +Push-Location $pyiDir +try { + $env:TESTIUM_ONEDIR = '1' + & $pyi 'testium.spec' + if ($LASTEXITCODE -ne 0) { throw "pyinstaller failed with exit code $LASTEXITCODE" } +} finally { + Remove-Item Env:\TESTIUM_ONEDIR -ErrorAction SilentlyContinue + Pop-Location } -# Locate ISCC.exe: PATH, then the usual install dirs. +# Locate ISCC: PATH, then the usual install dirs. $iscc = (Get-Command ISCC.exe -ErrorAction SilentlyContinue).Source if (-not $iscc) { foreach ($p in @( diff --git a/package/innosetup/testium.iss b/package/innosetup/testium.iss index 4ee6e8a..f114b61 100644 --- a/package/innosetup/testium.iss +++ b/package/innosetup/testium.iss @@ -49,9 +49,12 @@ Name: "english"; MessagesFile: "compiler:Default.isl" Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked ; PATH off by default: the exe is windowed (console=False), so CLI shows no output. Name: "addtopath"; Description: "Ajouter Testium au PATH (usage en ligne de commande)"; Flags: unchecked +; Shown only if another version is already installed; unchecked => keep it. +Name: "removeold"; Description: "Désinstaller les autres versions de Testium déjà installées"; Check: OtherVersionsExist; Flags: unchecked [Files] -Source: "..\pyinstaller\dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +; One-folder build: the exe plus its _internal\ tree (fast startup, no re-extract). +Source: "..\pyinstaller\dist\testium\*"; DestDir: "{app}"; Flags: recursesubdirs ignoreversion ; Ship the .ico so shortcuts/uninstall reference it directly, not the embedded one. Source: "..\testium.ico"; DestDir: "{app}"; Flags: ignoreversion @@ -67,6 +70,54 @@ Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}} [Code] const EnvKey = 'Environment'; + UninstallRoot = 'Software\Microsoft\Windows\CurrentVersion\Uninstall'; + AppGuid = '{B7E6F1C2-9A4D-4E3B-8F71-7C2D5A6E0B14}'; + +// Inno's uninstall subkey for *this* version: "{AppId}_is1". +function CurrentUninstallSubkey(): string; +begin + Result := AppGuid + '_{#MyAppVersion}_is1'; +end; + +// Uninstall subkeys of every installed Testium version except this one. +function OtherTestiumSubkeys(): TArrayOfString; +var + names: TArrayOfString; + i: Integer; + prefix, cur: string; +begin + SetArrayLength(Result, 0); + prefix := Uppercase(AppGuid + '_'); + cur := Uppercase(CurrentUninstallSubkey()); + if RegGetSubkeyNames(HKEY_CURRENT_USER, UninstallRoot, names) then + for i := 0 to GetArrayLength(names) - 1 do + if (Pos(prefix, Uppercase(names[i])) = 1) and (Uppercase(names[i]) <> cur) then + begin + SetArrayLength(Result, GetArrayLength(Result) + 1); + Result[GetArrayLength(Result) - 1] := names[i]; + end; +end; + +// Drives the "removeold" task: only offered when another version exists. +function OtherVersionsExist(): Boolean; +begin + Result := GetArrayLength(OtherTestiumSubkeys()) > 0; +end; + +// Silently run each other version's uninstaller. +procedure RemoveOtherVersions(); +var + subs: TArrayOfString; + i, rc: Integer; + cmd: string; +begin + subs := OtherTestiumSubkeys(); + for i := 0 to GetArrayLength(subs) - 1 do + if RegQueryStringValue(HKEY_CURRENT_USER, UninstallRoot + '\' + subs[i], + 'UninstallString', cmd) and (cmd <> '') then + Exec(RemoveQuotes(cmd), '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART', + '', SW_HIDE, ewWaitUntilTerminated, rc); +end; // True if Param is not already a full segment of the per-user PATH. function NeedsAddPath(Param: string): Boolean; @@ -97,6 +148,8 @@ begin Path := Path + ExpandConstant('{app}'); RegWriteStringValue(HKEY_CURRENT_USER, EnvKey, 'Path', Path); end; + if WizardIsTaskSelected('removeold') then + RemoveOtherVersions(); end; end; diff --git a/package/pyinstaller/testium.spec b/package/pyinstaller/testium.spec index b7b476d..8079517 100644 --- a/package/pyinstaller/testium.spec +++ b/package/pyinstaller/testium.spec @@ -79,26 +79,60 @@ a = Analysis( ) pyz = PYZ(a.pure) -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.datas, - [], - name='testium', - debug=False, - bootloader_ignore_signals=False, - strip=False, - # UPX is CPU+IO heavy for a marginal size gain — build_all --ram sets - # TESTIUM_NO_UPX=1 to skip it (much faster on slow/flash storage). - upx=not os.environ.get("TESTIUM_NO_UPX"), - upx_exclude=[], - runtime_tmpdir=None, - console=False, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, - ico='../testium.ico' -) +# TESTIUM_ONEDIR=1 => one-folder build (fast startup), used by the Windows +# installer; default one-file keeps the Linux build_all portable binary. +ONEDIR = bool(os.environ.get("TESTIUM_ONEDIR")) +# UPX skipped via TESTIUM_NO_UPX (build_all --ram) — slow for a marginal gain. +_upx = not os.environ.get("TESTIUM_NO_UPX") + +if ONEDIR: + exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='testium', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=_upx, + upx_exclude=[], + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + ico='../testium.ico' + ) + coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=_upx, + upx_exclude=[], + name='testium', + ) +else: + exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='testium', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=_upx, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + ico='../testium.ico' + )