feat(windows): icon, windowed exe, no-admin installer

- PyInstaller exe built windowed (console=False) with package/testium.ico
  as the embedded icon (BMP entries for shell compatibility).
- Suppress stray subprocess console windows in the frozen Windows build via
  paths.no_window_kwargs() (CREATE_NO_WINDOW); wheel/source unchanged.
  Applied to py_process, lua_process, bins probes, sys_app_path_win.
- New per-user Inno Setup installer (package/innosetup/): no admin,
  version-scoped AppId/dir so versions install side-by-side, one Start
  Menu entry per version, .ico shipped for shortcut/uninstall icons.
- DESIGN.md + release_note.txt updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:57:48 +02:00
parent e0802a9a72
commit 8c4e1b56b5
10 changed files with 191 additions and 6 deletions

View File

@@ -0,0 +1,31 @@
# Build the Testium installer from testium.iss (needs Inno Setup 6 / ISCC.exe).
# Install ISCC without admin: winget install --id JRSoftware.InnoSetup -e
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
# 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 ISCC.exe: PATH, then the usual install dirs.
$iscc = (Get-Command ISCC.exe -ErrorAction SilentlyContinue).Source
if (-not $iscc) {
foreach ($p in @(
"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe",
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
"$env:ProgramFiles\Inno Setup 6\ISCC.exe")) {
if (Test-Path $p) { $iscc = $p; break }
}
}
if (-not $iscc) {
throw "ISCC.exe not found. Install Inno Setup 6:`n winget install --id JRSoftware.InnoSetup -e"
}
Write-Host "Using ISCC: $iscc"
& $iscc (Join-Path $scriptDir 'testium.iss')
if ($LASTEXITCODE -ne 0) { throw "ISCC failed with exit code $LASTEXITCODE" }
Write-Host "`nInstaller built in: $(Join-Path $scriptDir 'dist')"

View File

@@ -0,0 +1,127 @@
; Inno Setup script: wraps the PyInstaller testium.exe into a per-user installer.
; Build with Inno Setup 6: ISCC.exe testium.iss (or ./build.ps1).
#define MyAppName "Testium"
#define MyAppExeName "testium.exe"
#define MyAppPublisher "Testium"
#define MyAppURL "https://github.com/"
; Read version from src/VERSION so the installer never drifts from the build.
#define VerFile FileOpen("..\..\src\VERSION")
#define MyAppVersion Trim(FileRead(VerFile))
#expr FileClose(VerFile)
#if MyAppVersion == ""
#error Could not read version from ..\..\src\VERSION
#endif
[Setup]
; Version-scoped AppId: each version is a distinct app, installable side-by-side.
AppId={{B7E6F1C2-9A4D-4E3B-8F71-7C2D5A6E0B14}_{#MyAppVersion}
AppName={#MyAppName} {#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
UninstallDisplayName={#MyAppName} {#MyAppVersion}
WizardStyle=modern
; Per-version install dir so versions never overwrite each other.
DefaultDirName={autopf}\{#MyAppName}\{#MyAppVersion}
; Shared "Testium" Start Menu folder; shortcuts below are named per version.
DefaultGroupName={#MyAppName}
UninstallDisplayIcon={app}\testium.ico
DisableProgramGroupPage=yes
; Per-user install, no admin ever: installs under %LOCALAPPDATA%, no UAC prompt.
PrivilegesRequired=lowest
ArchitecturesInstallIn64BitMode=x64compatible
OutputDir=dist
OutputBaseFilename=testium-{#MyAppVersion}-setup
SetupIconFile=..\testium.ico
Compression=lzma2/max
SolidCompression=yes
; Tell Explorer to refresh the environment after a PATH change.
ChangesEnvironment=yes
[Languages]
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
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
[Files]
Source: "..\pyinstaller\dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
; Ship the .ico so shortcuts/uninstall reference it directly, not the embedded one.
Source: "..\testium.ico"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
; Per-version names so each install shows separately in the Start Menu.
Name: "{group}\{#MyAppName} {#MyAppVersion}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\testium.ico"
Name: "{group}\{cm:UninstallProgram,{#MyAppName} {#MyAppVersion}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName} {#MyAppVersion}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\testium.ico"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent
[Code]
const
EnvKey = 'Environment';
// True if Param is not already a full segment of the per-user PATH.
function NeedsAddPath(Param: string): Boolean;
var
OrigPath: string;
begin
if not RegQueryStringValue(HKEY_CURRENT_USER, EnvKey, 'Path', OrigPath) then
begin
Result := True;
exit;
end;
Result := Pos(';' + Uppercase(Param) + ';', ';' + Uppercase(OrigPath) + ';') = 0;
end;
// On install: append {app} to the per-user PATH if the task is selected.
procedure CurStepChanged(CurStep: TSetupStep);
var
Path: string;
begin
if CurStep = ssPostInstall then
begin
if WizardIsTaskSelected('addtopath') and NeedsAddPath(ExpandConstant('{app}')) then
begin
if not RegQueryStringValue(HKEY_CURRENT_USER, EnvKey, 'Path', Path) then
Path := '';
if (Path <> '') and (Copy(Path, Length(Path), 1) <> ';') then
Path := Path + ';';
Path := Path + ExpandConstant('{app}');
RegWriteStringValue(HKEY_CURRENT_USER, EnvKey, 'Path', Path);
end;
end;
end;
// On uninstall: strip {app} back out of the per-user PATH.
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
Path, AppDir, Segment: string;
P: Integer;
begin
if CurUninstallStep = usUninstall then
begin
if RegQueryStringValue(HKEY_CURRENT_USER, EnvKey, 'Path', Path) then
begin
AppDir := ExpandConstant('{app}');
Segment := ';' + AppDir;
P := Pos(Uppercase(Segment), Uppercase(Path));
if P > 0 then
Delete(Path, P, Length(Segment))
else
begin
P := Pos(Uppercase(AppDir) + ';', Uppercase(Path));
if P = 1 then
Delete(Path, 1, Length(AppDir) + 1);
end;
RegWriteStringValue(HKEY_CURRENT_USER, EnvKey, 'Path', Path);
end;
end;
end;

View File

@@ -94,11 +94,11 @@ exe = EXE(
upx=not os.environ.get("TESTIUM_NO_UPX"),
upx_exclude=[],
runtime_tmpdir=None,
console=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
ico='../testium.png'
ico='../testium.ico'
)

BIN
package/testium.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB