WP Maintenance Manager ships from a single Python codebase to both Windows and macOS. “Python is cross-platform — write once, run anywhere,” the saying goes. The reality is that the distribution pipeline is completely separate per OS, each with its own pitfalls.
PyInstaller / Inno Setup / Apple Notarization / eSigner — the release cycle is a combination of OS-specific toolchains. Here’s the full picture, plus what to watch out for at each step. (The choice of internal architecture, Flask + browser UI, is covered separately in why we built a desktop app on local Flask + browser UI; this post is about distributing that architecture across two operating systems.)
The per-OS pipeline at a glance
| Step | Mac | Windows |
|---|---|---|
| Build | PyInstaller (--target-arch x86_64) |
PyInstaller |
| Distribution format | .app bundle → .dmg |
folder → .exe installer |
| Installer creation | hdiutil / create_dmg.sh |
Inno Setup (.iss script) |
| Code signing | codesign + Developer ID certificate |
eSigner CSC (cloud signing) |
| Pre-distribution validation | Apple Notarization | SmartScreen reputation buildup |
| Final artifact | WP_Maintenance_Pro_X.X.X.dmg |
WP_Maintenance_Pro_Setup_X.X.X.exe |
Both OSes share PyInstaller, but the path diverges from there. Mac sits inside Apple’s review process; Windows runs through Microsoft’s reputation system. They’re fundamentally different ecosystems.
Mac — PyInstaller → sign → Notarization → DMG
The Intel / Apple Silicon trap
The first trap in Mac PyInstaller builds is architecture. Running pip install + python build_app.py on an Apple Silicon Mac without thinking produces native binaries (like cffi) for arm64 only — which then don’t run on Intel Macs at all.
The fix is to run the entire build through arch -x86_64:
arch -x86_64 pip3 install -r requirements.txt
arch -x86_64 python3 build_app.py
That produces an .app containing only x86_64 binaries, which runs natively on Intel Macs and through Rosetta 2 on Apple Silicon — a unified distribution.
Sign inside-out
The .app PyInstaller produces contains many Mach-O binaries internally (_cffi_backend.so from cryptography, etc.). The straightforward codesign --deep approach has known compatibility issues with Hardened Runtime, so we detect Mach-O binaries with the file command and sign them individually from the inside out:
find "${APP_BUNDLE}" -type f -exec file {} \; \
| grep "Mach-O" | cut -d: -f1 \
| while read bin; do
codesign --force --options=runtime \
--entitlements "${ENTITLEMENTS}" \
--sign "${APP_CERT}" "${bin}"
done
# Finally, sign the whole .app
codesign --force --options=runtime \
--entitlements "${ENTITLEMENTS}" \
--sign "${APP_CERT}" "${APP_BUNDLE}"
Notarize via ZIP, then build the DMG
There’s an empirical rule that submitting a ZIP for notarization is faster than submitting a DMG directly. Apple’s backend goes through a more complex DMG analysis path; ZIP rides a simpler scan path.
ditto -c -k --keepParent "${APP_BUNDLE}" "${ZIP_PATH}"
xcrun notarytool submit "${ZIP_PATH}" \
--keychain-profile "wpmm-notary" --wait
xcrun stapler staple "${APP_BUNDLE}"
# Then build the DMG from the stapled .app
Notarization sometimes stalls at In Progress for days. The cause is often incomplete Apple Developer Program setup — missing Tax Forms or banking information — rather than anything technical. Contacting Apple support occasionally results in a batch of stuck submissions all flipping to Accepted at once. Surprisingly often, the blocker is contractual rather than technical.
Windows — PyInstaller → Inno Setup → eSigner CSC
The Inno Setup script
On Windows, WP_Maintenance_Pro.iss is the Inno Setup script that assembles the installer. User-mode installation (no admin required) into %APPDATA%, shortcut creation, residual-file cleanup on uninstall — all defined here:
[Setup]
AppId={{B3A7F2C1-4E8D-4A9F-B2C3-D5E6F7A8B9C0}
DefaultDirName={userappdata}\{#MyAppName}
PrivilegesRequired=lowest
PrivilegesRequired=lowest installs in user mode so people without admin rights — common in corporate environments — can still install the app. The visible drop in support tickets just from avoiding the UAC dialog is noticeable.
Cloud signing with eSigner CSC
Windows code signing traditionally requires a physical USB token holding the certificate. eSigner CSC (SSL.com’s cloud signing service) lets you sign from automation scripts without any token plugged in:
& "C:\esigner\CodeSignTool.bat" sign `
-input_file_path "WP_Maintenance_Pro_Setup.exe" `
-output_dir_path "signed/" `
-credential_id "${CRED_ID}" `
-username "${USERNAME}" -password "${PASSWORD}" `
-totp_secret "${TOTP_SECRET}"
OV/EV certificate grade differences, and SmartScreen reputation buildup (you get “Unknown publisher” warnings until enough installs accumulate) are each topics of their own — but for “just getting it signed and shipping,” eSigner CSC automation is the practical path.
The cross-cutting challenge — version synchronization and reproducibility
The recurring headache on every release is version number synchronization. version.py, MyAppVersion in WP_Maintenance_Pro.iss, server/wpmm-web/version.json, the LP download links — four or more files all need to match per release. If one drifts, you get bugs like “the new version is live, but the in-app update check doesn’t notice.”
release.py exists as a single-shot updater, but as files get added over time the script needs to be kept in sync. A release checklist remains essential.
Reflection — “the same Python,” distributed completely differently
“Cross-platform” sounds simple, but distribution-side work splits cleanly per OS. Binary building can be unified through PyInstaller, but installers, code signing, and OS-side validation all live in separate ecosystems — you have to follow each one’s conventions.
That said, once the pipeline is built, new releases come out in about 30 minutes — python build_app.py + bash sign_and_notarize.sh + Inno Setup F9 and you’re done. High initial setup cost, but easy to spin afterward — that’s the lived experience of two-OS distribution.