How To: Protect a PyInstaller Application¶
PyInstaller is the most popular Python packaging tool. PyLocket supports both --onefile and --onedir modes.
Prerequisites¶
- PyLocket CLI installed and authenticated (
pylocket login) - An app registered with PyLocket (
pylocket apps create) - PyInstaller installed (
pip install pyinstaller)
Recommended Build Mode¶
The correct PyInstaller mode depends on the target platform:
| Platform | PyInstaller Flags | Output | Why |
|---|---|---|---|
| macOS | --onedir --windowed |
.app |
Produces a proper .app bundle with Contents/Frameworks/Python.framework/ physically present (~6.5 MB dylib). The .app appears as a single file in Finder and is the standard format for macOS distribution. |
| Windows | --onefile --windowed |
.exe |
Single .exe with the Python runtime and all DLLs compressed inside. Extracts to a temp directory at launch. |
| Linux | --onefile |
binary | Single self-contained binary. Same extraction behaviour as Windows. |
Do not use --onefile on macOS
--onefile on macOS compresses everything — including the Python runtime and all native libraries — into a binary blob inside the executable. The resulting .app bundle does not contain Contents/Frameworks/Python.framework/ as a physical directory. Python is buried inside the compressed executable and unusable by the system. This produces a non-standard .app that can cause code-signature failures and "damaged" errors after protection. Always use --onedir --windowed for macOS.
Build¶
Use the platform-specific flags from the Recommended Build Mode table above:
Output: dist/MyApp.app (a complete .app bundle with Python.framework in Contents/Frameworks/)
PyInstaller 6+ creates symlinked .app bundles
Starting with PyInstaller 6, macOS .app bundles are built with symbolic links to reduce file size. For example, Contents/Frameworks/Python.framework/Python is a symlink to Versions/Current/Python rather than a copy of the file.
This is fine for running the app locally, but symbolic links can cause errors when you ZIP and upload the .app for protection. You may see errors like:
The fix: After building, use rsync to create a standalone copy that replaces all symlinks with real files:
# Build as usual
pyinstaller --onedir --windowed myapp.py
# Create a portable copy with symlinks resolved
rsync -aL dist/MyApp.app/ dist/MyApp_Standalone.app/
The -L flag tells rsync to follow every symbolic link and copy the actual file it points to. The resulting .app will be larger (because duplicated files are no longer shared via symlinks) but is fully portable and safe to ZIP and upload.
Then upload the standalone copy:
Recommended Project Layout¶
Organise your build outputs so each platform artifact lands in a dedicated folder. This makes it easy to upload each one to PyLocket as a separate build:
dist/
PyLocket/
Windows/
MyApp.exe # PyInstaller --onefile (Windows)
MacOS/
MyApp.app/ # PyInstaller --onedir --windowed (recommended)
Contents/
MacOS/
MyApp # Main executable
Frameworks/
Python.framework/ # Bundled Python runtime
Resources/
icon.icns
Info.plist
Linux/
myapp # PyInstaller --onefile (Linux)
Your build.py (or CI script) can produce this layout by passing --distpath dist/PyLocket/Windows (etc.) to PyInstaller.
Protect¶
Upload the raw executable for each platform. Do not upload installer packages (.msi, .dmg, Inno Setup .exe, NSIS .exe). Upload the executable inside the installer instead.
# Windows
pylocket protect --app <APP_ID> --artifact dist/PyLocket/Windows/MyApp.exe --platform win-x64
# macOS (.app bundle from --onedir --windowed)
pylocket protect --app <APP_ID> --artifact dist/PyLocket/MacOS/MyApp.app --platform macos-universal2
# Linux
pylocket protect --app <APP_ID> --artifact dist/PyLocket/Linux/myapp --platform linux-x64
Download¶
Output Structure¶
dist/protected/
├── myapp.exe # (or MyApp.app / myapp)
├── <native runtime> # Platform-specific runtime library
└── <protection manifest> # Signed protection manifest
Code Signing (macOS)¶
Protection invalidates the original code signature because the inner binary is modified. PyLocket strips the invalid signature so macOS shows "unidentified developer" rather than "damaged and can't be opened".
After downloading the protected .app, re-sign it with your Apple Developer ID certificate:
If you distribute outside the Mac App Store, also notarize the app:
# Create a ZIP for notarization
ditto -c -k --keepParent MyApp.app MyApp.zip
# Submit for notarization
xcrun notarytool submit MyApp.zip \
--apple-id you@example.com \
--team-id TEAMID \
--password @keychain:notarytool \
--wait
# Staple the ticket to the app
xcrun stapler staple MyApp.app
PyLocket never handles your signing identity
Re-signing requires your private key (certificate). This step must be performed on your machine or in your CI pipeline after downloading the protected artifact. PyLocket does not store, transmit, or access your code-signing credentials.
Automate in CI
Add the re-signing step to your CI/CD pipeline after pylocket fetch:
macOS Universal2 Builds¶
The --target-arch universal2 flag produces a single binary that runs natively on both Intel and Apple Silicon Macs. This is the recommended approach for macOS distribution.
Universal2 Dependency Verification
All native dependencies (.dylib, .so files) must have universal2 builds available. If any dependency is single-architecture only, PyInstaller will either fail or silently fall back to a single-architecture build.
Prerequisites:
- Your Python interpreter must be universal2 (the standard python.org installer is universal2; Homebrew Python 3.10+ also supports it).
- All native-extension packages must ship universal2 wheels on PyPI.
Two-step install for universal2 dependencies:
# Step 1: Install everything normally (arm64 wheels) into a clean venv
pip install -r requirements.txt
# Step 2: Overwrite native-extension packages with universal2 builds
pip install \
--platform macosx_11_0_universal2 \
--target "$(python -c 'import site; print(site.getsitepackages()[0])')" \
--no-deps \
<native-package-1> <native-package-2>
Replace <native-package-1> etc. with the names of packages that contain .so or .dylib files (e.g. numpy, cryptography, pillow).
Check before building
Run file dist/MyApp.app/Contents/MacOS/MyApp after building. A universal2 binary will show both x86_64 and arm64 architectures.
Using a .spec File¶
If you use a .spec file for custom PyInstaller configuration, the workflow is the same — just build with the spec file first:
Then protect the output as shown above.
Common Options¶
Hidden Imports¶
If your application uses hidden imports (common with dynamic imports, plugins, etc.), ensure they are included in your PyInstaller build before protecting:
PyLocket protects whatever PyInstaller packages. If a module is missing from the PyInstaller output, it will also be missing from the protected output.
Data Files¶
Non-Python data files (images, configs, etc.) bundled by PyInstaller are not encrypted by PyLocket — only Python bytecode is protected. If you need to protect data files, encrypt them separately at the application level.
Console vs. Windowed¶
Both --console and --windowed (or --noconsole) modes work with PyLocket:
# Console app
pyinstaller --onefile myapp.py
# Windowed app (no console window)
pyinstaller --onefile --windowed myapp.py
macOS .app bundles require --windowed
On macOS, --windowed is required to produce a .app bundle. Without it, PyInstaller outputs a bare binary instead.
Troubleshooting¶
| Problem | Solution |
|---|---|
Unsupported artifact type |
Ensure you are uploading the .exe (Windows), the binary (Linux/macOS), or a .zip of the onedir output |
| Missing modules at runtime | Add --hidden-import flags to your PyInstaller command |
| Large artifact upload timeout | For artifacts over 100 MB, ensure a stable connection. The CLI retries up to 3 times automatically. |
| Protected app crashes on startup | Verify the --python version matches your build environment. |
| macOS: "damaged and can't be opened" | The .app has an invalid code signature. Right-click the app and select Open, or run xattr -cr MyApp.app in Terminal. |
macOS: empty Frameworks/ directory |
You used --onefile mode. Rebuild with --onedir --windowed for a complete .app bundle with Python.framework. |
macOS: FileExistsError: File exists: 'Versions/Current/...' |
Your .app contains symbolic links (PyInstaller 6+). Create a portable copy with rsync -aL YourApp.app/ YourApp_Standalone.app/ and upload that instead. See PyInstaller 6+ symlinks above. |
See Also¶
- Basic Tutorial — Full walkthrough of the protection workflow
- CLI Reference — All
pylocket protectoptions - Cross-Platform Builds — Building for multiple platforms