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
12 changes: 4 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,9 @@ jobs:
echo "=== Release assets ==="
ls -la release-assets/
test -f "release-assets/PDFReader-by-Sparsh-Windows.zip" || { echo "Missing Windows ZIP"; exit 1; }
test -f "release-assets/PDFReader-by-Sparsh-Setup.exe" || { echo "Missing Windows Setup.exe"; exit 1; }
test -f "release-assets/PDFReader-by-Sparsh-macOS-Apple-Silicon.zip" || { echo "Missing macOS Apple Silicon ZIP"; exit 1; }
test -f "release-assets/PDFReader-by-Sparsh-macOS-Intel.zip" || { echo "Missing macOS Intel ZIP"; exit 1; }
if test -f "release-assets/PDFReader-by-Sparsh-Setup.exe"; then
echo "Windows Setup.exe: present"
else
echo "Windows Setup.exe: not built (Inno Setup may be unavailable)"
fi
echo "All required assets verified"

- name: Generate release notes
Expand All @@ -172,14 +168,14 @@ jobs:
Release ${GITHUB_REF_NAME}.

### Assets for Windows
- **\`PDFReader-by-Sparsh-Setup.exe\`** — ✅ **Recommended for normal use.** Inno Setup installer with PDF file association, Start Menu shortcut, desktop shortcut, and Add/Remove Programs entry. Requires admin rights.
- \`PDFReader-by-Sparsh-Windows.zip\` — Updater package only. Do not download unless you are upgrading from v1.0.0/v1.0.1 (see SUPPORT.md recovery guide) or need a portable copy.
- **\`PDFReader-by-Sparsh-Setup.exe\`** — ✅ **Recommended for normal use and in-app updates.** Inno Setup installer with PDF file association, Start Menu shortcut, desktop shortcut, and Add/Remove Programs entry. Requires admin rights.
- \`PDFReader-by-Sparsh-Windows.zip\` — Portable/manual recovery package. Use only if you need a portable copy or are following SUPPORT.md recovery steps.

### Assets for macOS
- \`PDFReader-by-Sparsh-macOS-Apple-Silicon.zip\` — macOS Apple Silicon app bundle.
- \`PDFReader-by-Sparsh-macOS-Intel.zip\` — macOS Intel app bundle.

Packaged builds include the tag-injected app version and canonical updater asset names.
Packaged builds include the tag-injected app version and canonical release asset names.
NOTE_EOF

- name: Publish release
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## v1.1.10 — Installer-Based Windows Updater — 2026-06-17

- **Version:** Bumped `__version__` to `1.1.10-dev`.
- **Windows updater uses Setup.exe** — in-app updates now select `PDFReader-by-Sparsh-Setup.exe` on Windows instead of the ZIP package.
- **Program Files update fix** — Windows updates are applied by Inno Setup so UAC elevation, app closing, file replacement, shortcuts, and file associations are handled by the installer rather than a hand-written `xcopy` batch script.
- **Portable ZIP preserved** — `PDFReader-by-Sparsh-Windows.zip` remains available for portable/manual recovery use, but it is no longer the normal Windows in-app update path.
- **Release workflow hardening** — `PDFReader-by-Sparsh-Setup.exe` is now a required release asset. The release fails if the installer is missing.
- **Updater diagnostics** — installer launch and exit/failure details are written to `%TEMP%\PDFReader-Updates\updater-debug.log`.
- **Regression coverage** — updater asset selection, update method routing, release asset consistency, and the standalone asset-flow script now expect the installer-first Windows path.

## v1.1.1 — Stability and UX Hardening — 2026-06-16

- **Version:** Bumped `__version__` to `1.1.1-dev`.
Expand Down
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ PDFReader by Sparsh is a **stable, local-first desktop PDF utility** built with

The app is intentionally local-first: PDFs are opened, rendered, searched, merged, split, annotated, and compressed on your computer — no uploads, no accounts, no telemetry.

**v1.1.1** is the current stable release for Windows. macOS builds are published for source-build testing but are not stable — the primary target is Windows. See the [changelog](CHANGELOG.md) and [roadmap](ROADMAP.md) for what's new and what's next.
**v1.1.10** is the current stable release for Windows. macOS builds are published for source-build testing but are not stable — the primary target is Windows. See the [changelog](CHANGELOG.md) and [roadmap](ROADMAP.md) for what's new and what's next.

## Download

Get the latest builds from the [Releases page](https://github.com/sparshsam/pdfreader-by-sparsh/releases/latest).

| Platform | Recommended Download | Alternative | Notes |
|---|---|---|---|---|
| Windows | `PDFReader-by-Sparsh-Setup.exe` | `PDFReader-by-Sparsh-Windows.zip` | **Stable and tested.** Use Setup.exe for normal installation (requires admin — see [Windows installer notes](SUPPORT.md#windows-installer)). ZIP remains for updater/portable/manual use. |
| Windows | `PDFReader-by-Sparsh-Setup.exe` | `PDFReader-by-Sparsh-Windows.zip` | **Stable and tested.** Use Setup.exe for normal installation and in-app updates (requires admin — see [Windows installer notes](SUPPORT.md#windows-installer)). ZIP remains for portable/manual recovery use. |
| macOS | — | — | **Not currently stable.** macOS builds are published for source-build testing only. The app may exhibit UI issues, missing features, or crashes. Run from source for the best macOS experience (see [Build From Source](#build-from-source)). |

Windows may show a SmartScreen warning because community builds are not code-signed. macOS may show a Gatekeeper warning because the Mac builds are not Apple-notarized. Only run software from sources you trust.
Expand Down Expand Up @@ -80,7 +80,7 @@ Packaged builds check the latest GitHub Release for updates. Source builds are i
| Desktop integration | Windows installer with `.pdf` file association, Start Menu, and desktop shortcut |
| Dark mode | System-aware dark theme (Catppuccin Mocha) with Auto/Light/Dark toggle via View → Theme |
| Recent files | Quick access to the last 10 opened PDFs via File → Open Recent |
| Auto-update | Packaged builds check GitHub Releases and update from canonical release ZIP assets |
| Auto-update | Packaged builds check GitHub Releases and Windows installs update through the canonical Setup.exe asset |
| Release engineering | Tag-driven GitHub Release publishing, PyInstaller packaging, Windows/macOS GitHub Actions builds, Inno Setup installer, self-update mechanism with diagnostic logging |

## Screenshots
Expand Down Expand Up @@ -189,11 +189,16 @@ https://api.github.com/repos/sparshsam/pdfreader-by-sparsh/releases/latest
It expects these exact asset names on the latest GitHub Release:

```text
PDFReader-by-Sparsh-Setup.exe
PDFReader-by-Sparsh-Windows.zip
PDFReader-by-Sparsh-macOS-Apple-Silicon.zip
PDFReader-by-Sparsh-macOS-Intel.zip
```

On Windows, in-app updates use `PDFReader-by-Sparsh-Setup.exe` so the installer
handles UAC elevation and replacement under `C:\Program Files`. The ZIP is kept
for portable use and manual recovery.

See [RELEASE.md](RELEASE.md) for release instructions, version injection, updater discovery, and validation.

## Use as Default PDF App
Expand Down Expand Up @@ -273,7 +278,7 @@ sudo pacman -S tesseract tesseract-data-eng
- [x] README features table synced with code
- [x] README tech stack expanded

### ✓ v1.1.1 — Stability and UX Hardening (Current Stable — Windows)
### ✓ v1.1.1 — Stability and UX Hardening

- [x] Open file — single picker, no cascading fallbacks, re-entrant guard
- [x] New Tab — creates blank tab without file dialog
Expand All @@ -283,6 +288,13 @@ sudo pacman -S tesseract tesseract-data-eng
- [x] Windows publisher docs — "Unknown Publisher" explained
- [x] 9 new regression tests (28 total, all passing)

### ✓ v1.1.10 — Installer-Based Windows Updater (Current Stable — Windows)

- [x] Windows in-app updates use `PDFReader-by-Sparsh-Setup.exe`
- [x] Inno Setup handles UAC elevation and Program Files replacement
- [x] Portable ZIP remains available for manual recovery
- [x] Release workflow requires the Windows installer asset

### Near-Term
Items in active or planned development.

Expand Down
31 changes: 19 additions & 12 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ https://api.github.com/repos/sparshsam/pdfreader-by-sparsh/releases/latest
The release workflow must attach these assets:

```text
PDFReader-by-Sparsh-Windows.zip (updater package — do not download unless instructed)
PDFReader-by-Sparsh-Setup.exe (Windows installer — recommended for normal use)
PDFReader-by-Sparsh-Setup.exe (Windows installer and in-app updater package)
PDFReader-by-Sparsh-Windows.zip (portable/manual recovery package)
PDFReader-by-Sparsh-macOS-Apple-Silicon.zip
PDFReader-by-Sparsh-macOS-Intel.zip
```

The updater uses only `PDFReader-by-Sparsh-Windows.zip`. The `-Setup.exe` is the recommended download for normal Windows users.
Do not rename or remove the canonical ZIP assets without updating `main.py`.
The Windows updater uses `PDFReader-by-Sparsh-Setup.exe` so Inno Setup owns
elevation and replacement under `C:\Program Files`. The ZIP remains available
for portable/manual recovery use, but it is not the normal Windows in-app update
path.

Do not rename or remove the canonical assets without updating `main.py`.

## How to Cut a Release

Expand All @@ -43,7 +47,7 @@ Do not rename or remove the canonical ZIP assets without updating `main.py`.

5. GitHub Actions runs `.github/workflows/release.yml`.
6. The workflow builds Windows, macOS Apple Silicon, and macOS Intel packages.
7. The workflow creates the GitHub Release and attaches the canonical ZIP assets.
7. The workflow creates the GitHub Release and attaches the canonical release assets.

## Auto-Update Discovery

Expand All @@ -53,6 +57,8 @@ The app's updater:
2. Reads `tag_name`.
3. Compares `tag_name` against the packaged app's injected `__version__`.
4. Selects the platform asset by exact canonical filename.
- Windows: `PDFReader-by-Sparsh-Setup.exe`
- macOS: the matching architecture ZIP
5. Downloads and applies the package for supported packaged builds.

Source builds usually run with a `-dev` version and are not the primary auto-update target. Developers should update source builds with `git pull` and rebuild locally.
Expand All @@ -63,13 +69,14 @@ After publishing a tag:

- [ ] The release workflow completed successfully.
- [ ] The GitHub Release exists for the pushed tag.
- [ ] The release contains `PDFReader-by-Sparsh-Windows.zip` (updater).
- [ ] The release contains `PDFReader-by-Sparsh-Setup.exe` (Windows updater).
- [ ] The release contains `PDFReader-by-Sparsh-Windows.zip` (portable/recovery).
- [ ] The release contains `PDFReader-by-Sparsh-macOS-Apple-Silicon.zip`.
- [ ] The release contains `PDFReader-by-Sparsh-macOS-Intel.zip`.
- [ ] Downloaded packaged builds show the tag-injected version in **Help > About**.
- [ ] `releases/latest` returns the new tag and all assets (including Setup.exe).
- [ ] `releases/latest` returns the new tag and all canonical assets.
- [ ] An older packaged build detects the newer version.
- [ ] The updater selects the correct asset for Windows.
- [ ] The updater selects `PDFReader-by-Sparsh-Setup.exe` for Windows.
- [ ] The updater selects the Apple Silicon asset on arm64 macOS.
- [ ] The updater selects the Intel asset on Intel macOS.

Expand Down Expand Up @@ -108,7 +115,7 @@ Resolution:

The updater now binds immutable metadata directly to the `QNetworkReply` with
`asset_name` and `latest_tag` properties. The download-finished handler reads
those reply properties, saves Windows updates only as
`PDFReader-by-Sparsh-Windows.zip`, and fails loudly if metadata is missing.
Windows ZIP updates are routed only when the canonical Windows asset name is
present, preventing silent manual-install fallbacks.
those reply properties, saves Windows installer updates as
`PDFReader-by-Sparsh-Setup.exe`, and fails loudly if metadata is missing.
Windows installer updates are routed only when the canonical Windows installer
asset name is present, preventing silent manual-install fallbacks.
96 changes: 89 additions & 7 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@
)


__version__ = "1.1.2-dev"
__version__ = "1.1.10-dev"
GITHUB_REPO = "sparshsam/pdfreader-by-sparsh"
WINDOWS_UPDATE_ASSET = "PDFReader-by-Sparsh-Windows.zip"
WINDOWS_INSTALLER_ASSET = "PDFReader-by-Sparsh-Setup.exe"
WINDOWS_PORTABLE_ASSET = "PDFReader-by-Sparsh-Windows.zip"
WINDOWS_UPDATE_ASSET = WINDOWS_INSTALLER_ASSET
MACOS_APPLE_SILICON_UPDATE_ASSET = "PDFReader-by-Sparsh-macOS-Apple-Silicon.zip"
MACOS_INTEL_UPDATE_ASSET = "PDFReader-by-Sparsh-macOS-Intel.zip"
IPC_SERVER_NAME = "PDFReaderBySparsh-IPC"
Expand Down Expand Up @@ -3089,7 +3091,9 @@ def _show_update_failed_diagnostic(self, log_content: str):
@staticmethod
def _select_update_apply_method(system, asset_name, dest):
suffix = Path(dest).suffix.lower()
if system == "Windows" and asset_name == WINDOWS_UPDATE_ASSET and suffix == ".zip":
if system == "Windows" and asset_name == WINDOWS_INSTALLER_ASSET and suffix == ".exe":
return "windows_installer", ""
if system == "Windows" and asset_name == WINDOWS_PORTABLE_ASSET and suffix == ".zip":
return "windows_zip", ""
if system == "Darwin" and suffix == ".zip":
return "macos_zip", ""
Expand Down Expand Up @@ -3210,7 +3214,7 @@ def _get_platform_asset(self, assets):
assets_by_name = {a.get("name", ""): a for a in assets}
system = platform.system()
if system == "Windows":
asset = assets_by_name.get(WINDOWS_UPDATE_ASSET)
asset = assets_by_name.get(WINDOWS_INSTALLER_ASSET)
if asset:
return asset["browser_download_url"], asset["name"]
elif system == "Darwin":
Expand Down Expand Up @@ -3328,9 +3332,11 @@ def _start_download(self, asset_url, asset_name, latest_tag):
validation_errors.append("missing asset name")
if not latest_tag:
validation_errors.append("missing release tag")
if system == "Windows" and asset_name != WINDOWS_UPDATE_ASSET:
if system == "Windows" and asset_name not in (WINDOWS_INSTALLER_ASSET, WINDOWS_PORTABLE_ASSET):
validation_errors.append(
f"Windows updater expected {WINDOWS_UPDATE_ASSET}, got {asset_name or '<missing>'}"
"Windows updater expected "
f"{WINDOWS_INSTALLER_ASSET} or {WINDOWS_PORTABLE_ASSET}, "
f"got {asset_name or '<missing>'}"
)
if validation_errors:
message = "Cannot start update download:\n\n" + "\n".join(validation_errors)
Expand Down Expand Up @@ -3468,7 +3474,9 @@ def _apply_update(self, dest: Path, latest_tag: str, asset_name: str):
method, diagnostic = self._select_update_apply_method(system, asset_name, dest)
self._log_update(f"selected_apply_method={method or 'unsupported'}")

if method == "windows_zip":
if method == "windows_installer":
self._apply_update_windows_installer(dest, latest_tag)
elif method == "windows_zip":
self._apply_update_windows_zip(dest, latest_tag)
elif method == "macos_zip":
self._apply_update_macos(dest, latest_tag)
Expand All @@ -3477,6 +3485,80 @@ def _apply_update(self, dest: Path, latest_tag: str, asset_name: str):
QMessageBox.critical(self, "Update Error", diagnostic)
return

@staticmethod
def _powershell_single_quote(value):
return str(value).replace("'", "''")

def _apply_update_windows_installer(self, dest, tag):
"""Update Windows installs through Inno Setup instead of in-place ZIP copy."""
current_exe = Path(sys.executable)
installer = Path(dest)
log_path = self._updater_log_path()

ps_command = (
f"$installer = '{self._powershell_single_quote(installer)}'; "
f"$exe = '{self._powershell_single_quote(current_exe)}'; "
"$args = @('/SP-', '/SILENT', '/SUPPRESSMSGBOXES', "
"'/CLOSEAPPLICATIONS', '/RESTARTAPPLICATIONS', '/NORESTART'); "
f"Add-Content -LiteralPath '{self._powershell_single_quote(log_path)}' "
f"-Value '[installer] starting {tag}'; "
"try { "
"$p = Start-Process -FilePath $installer -ArgumentList $args -Verb RunAs -Wait -PassThru; "
f"Add-Content -LiteralPath '{self._powershell_single_quote(log_path)}' "
"-Value \"[installer] exit_code=$($p.ExitCode)\"; "
"if ((Test-Path -LiteralPath $exe) -and ($p.ExitCode -eq 0)) { "
"Start-Process -FilePath $exe "
"} "
"} catch { "
f"Add-Content -LiteralPath '{self._powershell_single_quote(log_path)}' "
"-Value \"[installer] failed=$($_.Exception.Message)\"; "
"exit 1 "
"}"
)

try:
self._log_update(f"success=launching Windows installer updater: {installer}")
subprocess.Popen( # nosec B603, B607 — Windows self-update
[
"powershell.exe",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
ps_command,
],
cwd=str(installer.parent),
)
except Exception as exc:
self._log_update(f"failure=could not launch installer updater: {exc}")
QMessageBox.critical(
self, "Update Error",
"<h3>Update Could Not Start</h3>"
"<p>PDFReader was unable to launch the installer.</p>"
"<hr>"
"<p><b>What happened:</b><br>"
f"{exc}</p>"
"<p><b>What you can do:</b><br>"
"Download the latest Setup.exe manually from the GitHub releases page "
"and run it as Administrator.</p>"
"<hr>"
f"<p style='font-size:11px;color:#888;'>Installer: {installer}</p>",
)
return

QMessageBox.information(
self,
"Update Starting",
"<h3>Update Download Complete</h3>"
"<p>PDFReader will now close and run the installer.</p>"
"<p>It will reopen automatically after the installer completes.</p>"
"<hr>"
"<p style='font-size:12px;color:#888;'>"
"If you see a UAC (User Account Control) prompt, click <b>Yes</b> "
"to allow the update to complete.</p>",
)
QTimer.singleShot(500, self.close)

# ------------------------------------------------------------------
def _apply_update_windows_zip(self, dest, tag):
"""Replace the running app via ZIP extract + batch updater (onedir mode).
Expand Down
Loading