Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ GitHub Releases page; `0.8.0` is the new starting line.

## Unreleased

- **Windows updates avoid encoded PowerShell.** Native updates now launch the signed Inno installer directly with Restart Manager flags instead of a `powershell.exe -EncodedCommand` helper, reducing antivirus command-line heuristic false positives. Windows bootstrap installs use visible `/SILENT` progress instead of fully suppressed setup, and the installer build signs bundled PE files plus Inno's setup/uninstaller/temp copies when signing credentials are configured.

## 0.27.0 (2026-05-31)

### What changed in this release
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ pythinker --version
installs to `%ProgramFiles%\Pythinker` and writes PATH to HKLM (requires admin).

**Upgrade:** `pythinker update` from inside the running app — it downloads
the newest installer, verifies SHA-256, and re-runs it silently
(`/VERYSILENT /SUPPRESSMSGBOXES /NORESTART`).
the newest installer, verifies SHA-256, and launches the signed Inno installer
with visible progress (`/SILENT /NORESTART /CURRENTUSER /CLOSEAPPLICATIONS /NORESTARTAPPLICATIONS`).

**Uninstall:** Apps & Features → *Pythinker Code* → Uninstall reverts both
the files and the PATH edit.
Expand Down
10 changes: 8 additions & 2 deletions docs/public/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,14 @@ try {
OK "Checksum OK"

Step "Running Pythinker installer"
$args = @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/CURRENTUSER')
$process = Start-Process -FilePath $installerPath -ArgumentList $args -Wait -PassThru
$installerArgs = @(
'/SILENT',
'/NORESTART',
'/CURRENTUSER',
'/CLOSEAPPLICATIONS',
'/NORESTARTAPPLICATIONS'
)
$process = Start-Process -FilePath $installerPath -ArgumentList $installerArgs -Wait -PassThru
if ($process.ExitCode -ne 0) {
Fail "installer exited with code $($process.ExitCode)"
}
Expand Down
62 changes: 53 additions & 9 deletions packages/windows-installer/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
Steps:
1. Validate version, generate versioninfo.generated.txt.
2. Run PyInstaller using pythinker.spec -> dist/pythinker/
3. Sign dist/pythinker/pythinker.exe (no-op if cert env unset).
4. Compile installer.iss with iscc -> dist/PythinkerSetup-<Version>.exe
5. Sign the resulting setup .exe.
6. Write SHA256 next to the installer.
3. Sign bundled PE files (.exe/.dll/.pyd) when cert env is configured.
4. Compile installer.iss with iscc -> dist/PythinkerSetup-<Version>.exe.
When signing is configured, Inno signs Setup, Uninstall, and temp copies.
5. Write SHA256 next to the installer.
#>
[CmdletBinding()]
param(
Expand Down Expand Up @@ -42,6 +42,26 @@ Set-Content -Path $verOut -Value $verTemplate -Encoding UTF8

Write-Host "build.ps1: building Pythinker $Version"

$signScript = Join-Path $here 'sign\sign.ps1'
$signingConfigured = `
-not [string]::IsNullOrWhiteSpace($env:WINDOWS_CERT_PFX_BASE64) -and `
-not [string]::IsNullOrWhiteSpace($env:WINDOWS_CERT_PASSWORD)

if (-not $signingConfigured) {
Write-Warning "build.ps1: WINDOWS_CERT_PFX_BASE64 / WINDOWS_CERT_PASSWORD not set; Windows artifacts will be unsigned"
}

function Invoke-PythinkerSign {
param(
[Parameter(Mandatory = $true)]
[string] $Path
)

if ($signingConfigured) {
& $signScript $Path
}
}

# --- 2. PyInstaller --------------------------------------------------------
if (-not $SkipFreeze) {
Push-Location $here
Expand All @@ -58,8 +78,15 @@ if (-not (Test-Path $frozenExe)) {
throw "build.ps1: frozen binary not found at $frozenExe"
}

# --- 3. sign inner exe -----------------------------------------------------
& (Join-Path $here 'sign\sign.ps1') $frozenExe
# --- 3. sign bundled PE files ---------------------------------------------
if ($signingConfigured) {
$bundleRoot = Join-Path $dist 'pythinker'
$peFiles = Get-ChildItem $bundleRoot -Recurse -File |
Where-Object { $_.Extension -in @('.exe', '.dll', '.pyd') }
foreach ($file in $peFiles) {
Invoke-PythinkerSign $file.FullName
}
}

# --- 4. Inno Setup compile -------------------------------------------------
if (-not $SkipInstaller) {
Expand All @@ -70,7 +97,20 @@ if (-not $SkipInstaller) {
if (-not $iscc) {
throw "build.ps1: iscc.exe not found. Install Inno Setup 6."
}
& $iscc.Source "/DAppVersion=$Version" (Join-Path $here 'installer.iss')
$isccArgs = @("/DAppVersion=$Version")
if ($signingConfigured) {
# Keep quoted paths out of /SPythinkerSign itself. Windows PowerShell 5.1
# can mangle nested quotes when passing native arguments to iscc.exe.
$env:PYTHINKER_INNO_SIGN_SCRIPT = "`"$signScript`""
$signCommand = 'cmd.exe /D /C powershell.exe -NoProfile -NonInteractive -File %PYTHINKER_INNO_SIGN_SCRIPT% $f'
$isccArgs += "/SPythinkerSign=$signCommand"
$isccArgs += "/DUseInnoSignTool=1"
}
$isccArgs += (Join-Path $here 'installer.iss')

Write-Host "build.ps1: invoking Inno Setup with arguments:"
foreach ($arg in $isccArgs) { Write-Host " $arg" }
& $iscc.Source @isccArgs
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if ($LASTEXITCODE -ne 0) { throw "Inno Setup compile failed ($LASTEXITCODE)" }
}

Expand All @@ -79,8 +119,12 @@ if (-not (Test-Path $installer)) {
throw "build.ps1: installer not produced at $installer"
}

# --- 5. sign installer -----------------------------------------------------
& (Join-Path $here 'sign\sign.ps1') $installer
# --- 5. sign installer when compilation was skipped ------------------------
if ($SkipInstaller) {
Invoke-PythinkerSign $installer
} elseif ($signingConfigured) {
Write-Host "build.ps1: installer, uninstaller, and setup temp copies signed by Inno Setup"
}

# --- 6. SHA-256 -----------------------------------------------------------
$hash = (Get-FileHash $installer -Algorithm SHA256).Hash.ToLower()
Expand Down
33 changes: 24 additions & 9 deletions packages/windows-installer/installer.iss
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
LicenseFile=assets\LICENSE.rtf
ChangesEnvironment=yes
CloseApplications=force
CloseApplications=yes
RestartApplications=no
#ifdef UseInnoSignTool
SignTool=PythinkerSign
SignedUninstaller=yes
#endif

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Expand Down Expand Up @@ -82,6 +86,20 @@ begin
';' + UpperCase(OrigPath) + ';') = 0;
end;

function PathWithoutEntry(OrigPath, Param: string): string;
var
BoundedPath: string;
begin
BoundedPath := ';' + OrigPath + ';';
StringChangeEx(BoundedPath, ';' + Param + ';', ';', True);
while Pos(';;', BoundedPath) > 0 do
StringChangeEx(BoundedPath, ';;', ';', True);
if BoundedPath = ';' then
Result := ''
else
Result := Copy(BoundedPath, 2, Length(BoundedPath) - 2);
end;

procedure AddToPath(Param, RootHive: string);
var
OrigPath, NewPath: string;
Expand All @@ -97,10 +115,11 @@ begin
end;
if not RegQueryStringValue(Root, Subkey, 'Path', OrigPath) then
OrigPath := '';
OrigPath := PathWithoutEntry(OrigPath, Param);
if OrigPath = '' then
NewPath := Param
else
NewPath := OrigPath + ';' + Param;
NewPath := Param + ';' + OrigPath;
RegWriteExpandStringValue(Root, Subkey, 'Path', NewPath);
end;

Expand All @@ -118,9 +137,7 @@ begin
Subkey := 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
end;
if not RegQueryStringValue(Root, Subkey, 'Path', OrigPath) then exit;
StringChangeEx(OrigPath, ';' + Param, '', True);
StringChangeEx(OrigPath, Param + ';', '', True);
StringChangeEx(OrigPath, Param, '', True);
OrigPath := PathWithoutEntry(OrigPath, Param);
RegWriteExpandStringValue(Root, Subkey, 'Path', OrigPath);
end;

Expand All @@ -130,11 +147,9 @@ var
begin
if CurStep = ssPostInstall then begin
AppDir := ExpandConstant('{app}');
if WizardIsTaskSelected('modifypath')
and NeedsAddPath(AppDir, 'HKCU') then
if WizardIsTaskSelected('modifypath') then
AddToPath(AppDir, 'HKCU');
if WizardIsTaskSelected('modifypathmachine')
and NeedsAddPath(AppDir, 'HKLM') then
if WizardIsTaskSelected('modifypathmachine') then
AddToPath(AppDir, 'HKLM');
end;
end;
Expand Down
10 changes: 8 additions & 2 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,14 @@ try {
OK "Checksum OK"

Step "Running Pythinker installer"
$args = @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/CURRENTUSER')
$process = Start-Process -FilePath $installerPath -ArgumentList $args -Wait -PassThru
$installerArgs = @(
'/SILENT',
'/NORESTART',
'/CURRENTUSER',
'/CLOSEAPPLICATIONS',
'/NORESTARTAPPLICATIONS'
)
$process = Start-Process -FilePath $installerPath -ArgumentList $installerArgs -Wait -PassThru
if ($process.ExitCode -ne 0) {
Fail "installer exited with code $($process.ExitCode)"
}
Expand Down
Loading
Loading