Skip to content

packaging/windows: add NSIS installer .exe alongside the portable .zip - #740

Merged
mrjimenez merged 3 commits into
amule-project:masterfrom
got3nks:feat/windows-installer
May 27, 2026
Merged

packaging/windows: add NSIS installer .exe alongside the portable .zip#740
mrjimenez merged 3 commits into
amule-project:masterfrom
got3nks:feat/windows-installer

Conversation

@got3nks

@got3nks got3nks commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #738. The portable .zip works well for technical users but leaves the rest with no desktop / Start Menu entry and no obvious way to keep aMule running across reboots, which the upstream ticket flags as a measurable contributor to network health (clients that aren't running can't seed). This PR adds a Windows installer .exe that wraps the existing portable tree as its payload — no second build, no second dependency resolution.

What's in the installer

NSIS 3.x with MUI2. Pages: Welcome, License (project COPYING), Components, Directory, Start Menu, Install, Finish. Three components surfaced on the Components page: aMule core (required, SectionIn RO), desktop shortcut (checked by default), "Start aMule when I log in" (unchecked by default — opt-in writes HKCU\Software\Microsoft\Windows\CurrentVersion\Run\aMule). Start Menu folder gets shortcuts for aMule (GUI), aMule Daemon, aMuleGUI (remote), and the uninstaller. Per-machine install (RequestExecutionLevel admin, SetShellVarContext all so shortcuts land under All Users, not the elevated admin's per-user profile). $PROGRAMFILES64\aMule default — overridable on the Directory page.

Full Add/Remove Programs entry under HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\aMule with DisplayName, DisplayVersion, Publisher, URLInfoAbout, DisplayIcon, InstallLocation, UninstallString, QuietUninstallString, EstimatedSize (computed via ${GetSize}), NoModify=1, NoRepair=1.

A sharing-violation probe refuses to install or uninstall while amule.exe / amuled.exe in the target dir is open — works regardless of which user owns the running process. The uninstaller removes the Run key value only when it still points inside the install dir, so a user's hand-set autostart value isn't clobbered. The 32-bit installer stub is used for BOTH x64 and ARM64 payloads — it runs natively on x64 and under WOW64 emulation on Windows-on-ARM. A native arm64-unicode stub would require an NSIS distribution that ships one (Chocolatey and Homebrew don't as of NSIS 3.10), so for a reproducible CI matrix we use the universally-available 32-bit stub; the installer is short-lived bootstrap code, so the emulation cost is irrelevant.

Recipe + CI integration

packaging/windows/build.sh gets a new installer subcommand on top of the existing default action. It consumes the portable tree from a prior plain build.sh invocation (no duplicated build logic) and runs makensis with the required defines. Auto-discovers makensis at the standard install dir (C:\Program Files (x86)\NSIS) if it isn't on $PATH, since MSYS2's path-type: minimal drops the Windows PATH entries that Chocolatey writes — self-contained for CI and dev machines alike. Auto-signs the produced .exe when WIN_CERT_PFX_BASE64 + WIN_CERT_PASSWORD are set; the existing sign subcommand also picks up the installer when present in dist/. Default no-argument invocation is unchanged — installer is opt-in.

sign.sh now dispatches on file extension: .zip keeps the existing extract → sign → re-zip dance; .exe (installer) gets signed in place.

.github/workflows/packaging.yml gets a windows-installer job that needs: windows-zip and downloads the portable .zip artifact as the installer payload, rather than rebuilding the portable tree a second time. Coupling is correct: with no payload there's nothing to wrap, so a windows-zip failure should skip the installer. NSIS isn't packaged for the clangarm64 MSYS2 environment, so the job installs Windows-native NSIS via Chocolatey (no-op when the runner already ships it). Smoke test silent-installs to C:\amule-installtest, asserts bin\amule.exe + Uninstall.exe are present, then silent-uninstalls and asserts the dir is gone.

Coexistence with the portable .zip

Different artifact name (aMule-<ver>-Setup-<arch>.exe vs aMule-<ver>-Windows-<arch>.zip), different default install path. The two are independent shipping channels — neither replaces the other.

Test plan

  • x64 installer builds, silent-installs, silent-uninstalls clean in CI
  • ARM64 installer builds, silent-installs, silent-uninstalls clean in CI
  • Manual install on Windows 11 ARM64: MUI2 pages render, components default state correct, install completes
  • Start Menu folder + shortcuts created with correct targets (C:\Program Files\aMule\bin\{amule,amuled,amulegui}.exe)
  • Desktop shortcut created when checked
  • Autostart-on-login Run-key value written when ticked; aMule launches at login after reboot
  • Uninstaller removes install tree, shortcuts, ARP entry, Run-key value, HKLM Software\aMule
  • All Users vs per-user Start Menu placement re-verified after the SetShellVarContext all fix
  • Sharing-violation guard refuses install while amule.exe is open

Refs amule-project#738.

The existing portable .zip works well for technical users but leaves
the non-technical audience with no desktop / Start Menu entry and no
obvious way to keep aMule running across reboots, which the upstream
ticket flags as a measurable contributor to network health (clients
that aren't running can't seed). This adds a Windows installer .exe
that wraps the same portable tree without re-implementing the build.

`packaging/windows/installer.nsi` — NSIS 3.x / MUI2 script.

  * Pages: Welcome, License (project COPYING), Components, Directory,
    Start Menu Folder, Install, Finish.
  * Components: aMule core (required, SectionIn RO), desktop shortcut
    (checked by default), "Start aMule when I log in" (unchecked by
    default — opt-in writes HKCU\…\Run\aMule, per-user so each account
    on a shared machine decides independently).
  * Start Menu folder with shortcuts for aMule (GUI), aMule Daemon,
    aMuleGUI (remote), and the uninstaller.
  * Full Add/Remove Programs entry under HKLM (DisplayName,
    DisplayVersion, Publisher, URLInfoAbout, DisplayIcon,
    InstallLocation, UninstallString, QuietUninstallString,
    EstimatedSize via ${GetSize}, NoModify=1, NoRepair=1).
  * Pre-install / pre-uninstall guard refuses to proceed while
    amule.exe / amuled.exe in the install dir is open (sharing-
    violation probe rather than process enumeration so it works
    regardless of which user owns the running process).
  * Uninstaller removes the Run-key value only when it still points
    inside the install dir so a user's hand-set value isn't wiped.
  * Per-machine install (RequestExecutionLevel admin), $PROGRAMFILES64
    default, ARM64 vs x64 selected by !define ARCH so the same script drives
    both shippable architectures via a shared 32-bit installer stub
    (NSIS arm64-unicode stub isn't shipped by Chocolatey or Homebrew
    as of 3.10, and an x86 installer running under WOW64 emulation
    for ~30 seconds during install is a non-issue).

`packaging/windows/build.sh` — new `installer` subcommand that
verifies the portable tree from a prior plain `build.sh` invocation
exists, copies docs/COPYING into the tree for the MUI2 license page,
and runs makensis with the four required defines. Auto-discovers
`makensis` at the standard install dir if it isn't on $PATH (MSYS2's
default path-type=minimal drops Windows PATH entries, so the bash
session won't find a choco-installed makensis without help). Auto-
signs the produced .exe when both WIN_CERT_PFX_BASE64 and
WIN_CERT_PASSWORD are populated. Default no-argument invocation
still builds only the portable .zip.

`packaging/windows/sign.sh` — extended to dispatch on extension so
`.exe` artifacts (the installer) are signed in place while the
existing `.zip` path stays unchanged (extract → sign every .exe +
.dll → re-zip). `build.sh sign` now also signs the installer .exe
when present in `dist/`.

`.github/workflows/packaging.yml` — new `windows-installer` job
that depends on `windows-zip` and consumes its portable .zip
artifact as the installer payload, rather than rebuilding the
portable tree a second time. The coupling is correct: with no
payload there's nothing to wrap, so a `windows-zip` failure should
skip the installer. NSIS isn't packaged for the clangarm64 MSYS2
environment, so the job installs Windows-native NSIS via Chocolatey
(no-op when the runner already ships it); the toolchain runs fine
under x64 emulation on Windows-on-ARM since it's a build tool, not
part of the runtime payload. Smoke test silent-installs to
C:\amule-installtest, verifies amule.exe and Uninstall.exe landed,
then silent-uninstalls and verifies the dir is gone.

`packaging/windows/README.md` — new "Installer (.exe)" section
covering the invocation, output path, what the installer does,
admin / per-machine semantics, coexistence with the portable .zip,
and the shared signing flow.
@Vollstrecker

Copy link
Copy Markdown
Collaborator

cpack has nsis support, why not using that?
And why shipping all that MSys crap if we can build native on Win?

@got3nks

got3nks commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

Both fair questions — quick unpacking:

CPack NSIS: Considered it. The catch is the feature surface #738 actually asks for — Components page with an opt-in "Start aMule when I log in" checkbox, sharing-violation probe that refuses to install while amule.exe is open, conditional HKCU\…\Run\aMule write, uninstaller that only removes the Run-key value when it still points inside $INSTDIR (so a user's hand-set value isn't clobbered). None of that maps cleanly to CPack's NSIS template. You can inject custom logic via CPACK_NSIS_EXTRA_INSTALL_COMMANDS / CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS / a CPackConfig.cmake override, but those are string-glued NSIS fragments that grow into a maintenance burden faster than a direct .nsi.

The other thing CPack would (theoretically) buy us is "install rules already in CMake → no duplication." The existing portable build already runs cmake --install --prefix amule-portable-<arch> to produce the staged tree, and the installer just wraps that tree — it doesn't reimplement install logic. So there's no install-logic duplication to eliminate either way.

Net: for this feature set, a direct .nsi (~300 lines, mostly declarative MUI2 page + CreateShortcut calls) ends up cleaner than CPack + escape-hatch macros. Happy to revisit if CPack's NSIS generator grows native Components-page support.

On "shipping MSys": worth a precision — we don't actually ship any MSYS subsystem files (no msys-2.0.dll or POSIX-emulation runtime). What's in bin/ is 38 DLLs / 49 MB, of which 36 are third-party libraries aMule links against (wxWidgets, OpenSSL, libcurl + its transport stack, libupnp, crypto++, libmaxminddb, image codecs for libgd, zlib/lzma/iconv, etc.) — same set we'd ship regardless of toolchain — and 2 are the mingw-w64/LLVM C++ runtime (libc++.dll, libwinpthread-1.dll). MSYS2 is the build environment that packages the mingw-w64 toolchain and these dep builds; the produced binaries are plain Windows PEs, no POSIX shim.

Native Windows build: Different concern, and a much bigger one. Switching to MSVC + vcpkg would replace those 2 runtime DLLs with vcruntime140.dll / msvcp140.dll (or assume VCRedist), but the 36 third-party DLLs are the same files either way. It would mean dep rebuild paths for everything in vcpkg, code adjustments for the POSIX-ish bits the codebase assumes (paths, wchar_t, errno), and a parallel CI matrix — a multi-PR effort separate from "ship an installer."

For this PR specifically, the installer wraps the output of whatever Windows build the project ships. If upstream moves to MSVC later, the same .nsi keeps working: paths and DLL names in the payload would change, but the script structure stays.

@Vollstrecker

Copy link
Copy Markdown
Collaborator

Native Windows build: Different concern, and a much bigger one. Switching to MSVC + vcpkg

Don't say vcpkg, that crap is broken by design and shouldn't be used by anyone using more than 3 braincells.
But as you say that, I really have to finish my stuff, so we can at least test native Win builds.

@got3nks
got3nks marked this pull request as draft May 27, 2026 12:38
…r-data removal

Two follow-ups from PR review:

* Add CheckPriorInstall — silently runs the old install's
  Uninstall.exe before staging the new portable tree, so removed
  files / orphaned DLLs from the older version don't accumulate.
  Reads prior location from HKLM\Software\aMule\InstallLocation
  (recorded during the original install). Silent mode respects
  section defaults: un.SecRemoveUserData is /o (unchecked), so an
  upgrade preserves %APPDATA%\aMule.

* Add un.SecRemoveUserData — opt-in (Section /o, unchecked by
  default) uninstaller section that nukes %APPDATA%\aMule on
  explicit request. Default-off matches the common Windows
  convention of "remove the app, keep my data." MUI_UNPAGE_COMPONENTS
  shows the checkbox alongside the (RO) main uninstall section.

Refs amule-project#740 (comment).
@got3nks
got3nks marked this pull request as ready for review May 27, 2026 12:40
@got3nks

got3nks commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 6cf207a12 addressing the upgrade-flow and remove-user-data points from the review above. Quick rundown:

Upgrade flow — new CheckPriorInstall function runs at the very start of SecCore. Reads HKLM\Software\aMule\InstallLocation (recorded during a prior install); if a previous install is found with its Uninstall.exe still present, runs it silently via ExecWait '"$0\Uninstall.exe" /S _?=$0' before staging the new payload. That clears orphaned DLLs / removed files that File /r (overwrite-only) would otherwise leave behind. If the silent uninstall fails (typically because amule.exe is still running at the old path), the new install aborts with a clear message rather than producing a half-overwritten tree.

Important consequence of the silent uninstall: NSIS /S respects each section's default state. The new "Remove user data" section is declared Section /o (optional, unchecked by default), so an upgrade preserves %APPDATA%\aMule — config, ED2K servers, Kad nodes, partfiles, IP filters all stay in place. The data is only ever touched if a user explicitly checks the box in an interactive uninstall.

Opt-in user-data removal — added MUI_UNPAGE_COMPONENTS to the uninstaller and split it into two sections: the mandatory un.SecUninstall (SectionIn RO, removes binaries / shortcuts / regkeys) and the optional un.SecRemoveUserData (/o, unchecked, removes %APPDATA%\aMule if ticked). Default matches the common Windows convention of "remove the app, keep my data." Tooltip on the data section lists what gets deleted so it's clear before clicking.

Autostart toggle in Preferences — agreed it's the right UX, but it needs Preferences-dialog work + a 3-OS backend abstraction (Windows registry, macOS LaunchAgents, Linux ~/.config/autostart) + a new amule --configure-autostart on|off CLI surface. Filed as #742 so this PR stays scoped to packaging mechanics.

New test-plan items still pending (CI for 6cf207a12 is in flight; will verify on Windows 11 ARM64 when artifacts are ready):

  • Upgrade-over-existing (d978a13446cf207a12): prior install auto-detected and silently removed before new payload stages; %APPDATA%\aMule preserved
  • Opt-in user-data removal: Components page in uninstaller shows two sections (mandatory + optional); default is data-preserved; checking the box wipes %APPDATA%\aMule

…omponents page

NSIS dispatches install-vs-uninstall sections based on the `un.`
prefix on the DISPLAY NAME (first arg to Section), not on the
section ID. The `Section /o "Remove user data..." un.SecRemoveUserData`
declaration put the prefix on the ID and left the name bare, so NSIS
treated it as an installer section — the checkbox appeared on the
install-time Components page where it had no business being.

Worse, if a user had ticked it during install, the section body
(SetShellVarContext current + RMDir /r "$APPDATA\aMule") would have
wiped user data while installing.

Fix: move the `un.` prefix onto the display name. NSIS strips it
from the rendered label, so the user still sees "Remove user data..."
in the uninstaller's Components page, but the section is correctly
classified as uninstaller-only.

The MUI_DESCRIPTION_TEXT ${un.SecRemoveUserData} reference and the
section's ID stay the same — only the first arg to Section moves.
@got3nks
got3nks marked this pull request as draft May 27, 2026 13:12
@got3nks
got3nks marked this pull request as ready for review May 27, 2026 13:32
@got3nks

got3nks commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

cb09afba8: one-line follow-up to 6cf207a12. I had put NSIS's un. uninstaller-section prefix on the section ID instead of the display name, so the "Remove user data" section leaked into the install-side Components page and was missing from the uninstaller. NSIS dispatches on the display name prefix; moving un. there (where it's stripped from the rendered label) puts the section in the right place.

Re-verified end-to-end on Windows 11 ARM64:

  • Install-side Components page: no "Remove user data" checkbox
  • Uninstall-side: mandatory "Uninstall" + opt-in "Remove user data" (default unchecked)
  • Unchecked uninstall → %APPDATA%\aMule preserved (34 files)
  • Checked uninstall → %APPDATA%\aMule deleted
  • Upgrade-over-existing silently removes old install, preserves user data

All test-plan items are green from my side — ready for merge.

@mrjimenez
mrjimenez merged commit 6517840 into amule-project:master May 27, 2026
7 checks passed
mrjimenez pushed a commit that referenced this pull request May 27, 2026
…r-data removal

Two follow-ups from PR review:

* Add CheckPriorInstall — silently runs the old install's
  Uninstall.exe before staging the new portable tree, so removed
  files / orphaned DLLs from the older version don't accumulate.
  Reads prior location from HKLM\Software\aMule\InstallLocation
  (recorded during the original install). Silent mode respects
  section defaults: un.SecRemoveUserData is /o (unchecked), so an
  upgrade preserves %APPDATA%\aMule.

* Add un.SecRemoveUserData — opt-in (Section /o, unchecked by
  default) uninstaller section that nukes %APPDATA%\aMule on
  explicit request. Default-off matches the common Windows
  convention of "remove the app, keep my data." MUI_UNPAGE_COMPONENTS
  shows the checkbox alongside the (RO) main uninstall section.

Refs #740 (comment).
@got3nks
got3nks deleted the feat/windows-installer branch May 27, 2026 15:15
mrjimenez pushed a commit to mrjimenez/amule that referenced this pull request Aug 1, 2026
Adds GenericName[ru] and Comment[ru] to org.amule.aMule.desktop and org.amule.aMule.gui.desktop.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build and publish a Windows installer

3 participants