Multi-OS Publishing¶
This tutorial walks you through publishing a protected Python application for Windows, macOS, and Linux. By the end, you will have platform-specific protected builds that customers can download, activate with a single license key, and run on any supported OS.
Time required: ~30 minutes
Prerequisites¶
| Requirement | Notes |
|---|---|
| PyLocket CLI installed | pip install pylocket |
| PyLocket account | Sign up |
| Python 3.12+ | Same version used to build your app |
| Build tool | PyInstaller (recommended), cx_Freeze, or Briefcase |
| Access to each target OS | Native hardware, VMs, or CI runners |
What You'll Build¶
Your Python App
│
├── Build on Windows → myapp.exe → Protect → Protected myapp.exe
├── Build on macOS → MyApp.app → Protect → Protected MyApp.app
└── Build on Linux → myapp.elf (renamed) → Protect → Protected myapp.elf
All three platform builds live under one app in PyLocket. A single license key works across all platforms.
Step 1: Understand What Gets Protected¶
PyLocket encrypts the Python bytecode inside your built executable and repacks it in the same format. The output is a working executable — not a .bin blob.
What to upload¶
Upload the raw build output from your build tool:
| Build Tool | What to Upload | File Extension |
|---|---|---|
PyInstaller --onefile (Windows/Linux CLI tools) |
The single executable | .exe (Windows) or .elf (Linux, rename the extensionless binary) |
PyInstaller --onedir --windowed (macOS GUI apps) |
The .app bundle |
.app |
PyInstaller --onedir (Windows/Linux) |
ZIP the output directory | .zip |
| cx_Freeze | ZIP the output directory | .zip |
| Briefcase (macOS) | The .app bundle directly |
.app |
| Briefcase (other) | ZIP the output directory | .zip |
python -m build |
The wheel file | .whl |
| Generic archive | ZIP or tar of your project | .zip, .tar.gz, .tgz, .tar.bz2 |
macOS .app bundles must be built with --onedir --windowed
Do not use --onefile to build a macOS .app. --onefile --windowed compresses
the Python runtime and all native libraries into a blob inside the executable, so the
resulting bundle has no physical Contents/Frameworks/Python.framework/. PyLocket
cannot protect that layout and will reject the upload, and even if it were protected
the non-standard bundle causes code-signature failures and "damaged" errors on the
end user's Mac.
Always build macOS GUI apps with:
A bare macOS CLI binary built with --onefile (no .app wrapper) is fine. The
restriction applies only to .app bundles. See
Protect a PyInstaller Application
for the full per-platform build matrix.
Recommended for end-user distribution
Use executables (.exe on Windows, .app on macOS) when distributing to end
users. These produce standalone applications that do not require Python on the
customer's machine. Use --onefile on Windows and Linux; use --onedir --windowed
for the macOS .app.
macOS .app bundles download as .app.zip
A macOS .app is a folder (a bundle), not a single file. A download is a single
file, so PyLocket delivers a protected .app as a .app.zip. Most browsers
expand it back into the runnable .app automatically; if not, one double-click in
Finder does it. The PyLocket Installer and Hub unzip and install it for the
end-user with no manual step. See Distributing macOS .app bundles for the full end-user flow.
Supported file extensions (complete list)¶
.exe .app .elf .zip .tar.gz .tgz .tar.bz2 .whl .egg .py .pyz .pyw
Any file extension not in this list will be rejected on upload. Extensionless files are also rejected: a PyInstaller --onefile binary on Linux or macOS must be renamed to .elf before upload (the name is just a transport convention; the engine reads the real format from the file itself).
Maximum upload size: 50 GB
Individual uploads are limited to 50 GB. This accommodates large applications bundled with libraries like PyTorch, TensorFlow, and GUI frameworks (PyQt/PySide). If your artifact exceeds this limit, consider splitting it into smaller components or contact support.
Raw Python files (.py, .pyz, .pyw)
Raw Python scripts are supported but are a niche use case. The protected output requires Python on the end user's machine. See Protecting Raw Python Scripts for details.
What NOT to upload¶
Do not upload installer packages:
.dmg,.pkg(macOS installers).msi(Windows installer).deb,.rpm(Linux packages).AppImage
These are containers that wrap your executable. Upload the executable inside them, then optionally repackage the protected output into an installer afterward (see Step 7).
Protected output structure¶
Protection produces a single artifact in the same format you uploaded: a protected myapp.exe stays a myapp.exe, and a .app bundle stays a .app (delivered as .app.zip). The native runtime library and the protection manifest are embedded inside the artifact, so there are no extra files to distribute.
Step 2: Choose Your Build Strategy¶
| Strategy | Best For | Pros | Cons |
|---|---|---|---|
| PyInstaller per platform | Desktop apps, end users | Native executables, no Python needed on user's machine | Must build on each OS |
| Cross-platform wheel | Developer tools, CLI apps | Single artifact, works everywhere Python is installed | Requires Python on user's machine |
| ZIP archive | Internal tools, testing | Simple, no build step | Requires Python on user's machine |
This tutorial focuses on PyInstaller per platform — the most common approach for distributing to end users.
Step 3: Build Platform-Specific Artifacts¶
Option A: Build Locally on Each OS¶
For a GUI app distributed as a .app bundle (the usual case), use
--onedir --windowed. Do not use --onefile for a .app:
For a bare command-line binary (no .app bundle), --onefile is fine:
Option B: CI/CD Matrix Build (Recommended)¶
Use GitHub Actions to build on all three platforms automatically:
# .github/workflows/build.yml
name: Build & Protect
on:
push:
tags: ["v*"]
jobs:
build:
strategy:
matrix:
include:
- os: windows-latest
platform: win-x64
artifact: dist/myapp.exe
build: pyinstaller --onefile myapp.py
# The Linux onefile binary is extensionless; rename it to .elf for upload.
- os: ubuntu-latest
platform: linux-x64
artifact: dist/myapp.elf
build: pyinstaller --onefile myapp.py && mv dist/myapp dist/myapp.elf
# macOS .app bundles must use --onedir --windowed, never --onefile.
- os: macos-latest
platform: macos-arm64
artifact: dist/MyApp.app
build: pyinstaller --onedir --windowed myapp.py
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pyinstaller -r requirements.txt
- run: ${{ matrix.build }}
- uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.platform }}
path: ${{ matrix.artifact }}
Step 4: Register Your App¶
Create a single app in PyLocket — all platform builds live under it:
Note the APP_ID in the output. You'll use it for all platform builds.
Or use the Developer Portal: go to Apps → Create App.
Step 5: Protect Each Platform Build¶
Upload and protect each platform's artifact:
Each command returns a BUILD_ID. Protection runs in the cloud and typically takes 1–5 minutes.
Using the Portal¶
- Go to Apps → select your app → Upload Build
- Select the artifact file (the raw executable or zipped directory)
- Set the Platform dropdown (Windows, macOS, Linux)
- Click Upload Build
- Repeat for each platform
Upload the executable, not the installer
Upload myapp.exe, not myapp.msi. Upload the macOS binary, not the .dmg. PyLocket protects the Python bytecode inside the executable — it cannot process installer containers.
Step 6: Download Protected Output¶
Check build status and download the protected artifact:
# Check status
pylocket status --build <BUILD_ID>
# Download when the build is ready
pylocket fetch --build <BUILD_ID> --out dist/protected/
The output directory contains a single protected artifact in the same format you uploaded:
The native runtime and the protection manifest are embedded inside the artifact, so this one file is everything you ship.
Step 7: Package for Distribution (Optional)¶
The protected output can be distributed as-is, or wrapped in platform-specific installers:
Simplest: Distribute the protected .exe directly. It is a single self-contained file.
Professional: Create an .msi installer using WiX Toolset or Inno Setup that installs the protected .exe.
For a .app bundle: the protected output is the .app itself (the runtime
and manifest live inside the bundle). PyLocket delivers it as .app.zip; you can
distribute that zip directly, and the end-user's browser or Finder expands it back
into the runnable .app. To remove even the unzip step, point customers at the
PyLocket Installer or Hub.
For CLI binaries: ZIP the protected binary and distribute directly.
Professional: Create a .dmg disk image:
Step 8: Set Up Licensing¶
License keys are platform-independent. One key works on Windows, macOS, and Linux.
Each activation on a different OS counts as a separate device toward the device limit. For example, with a 3-device license, a customer can activate on their Windows desktop, MacBook, and Linux server.
# Create a license (or use Stripe integration for automatic creation).
# Billable: records the per-license fee on your next invoice. --yes skips
# the confirmation prompt.
pylocket licenses create --app <APP_ID> --device-limit 3 --yes
On the Free plan you are not charged, but the license is issued as a trial and
counts against your free-tier allowance; without --expires it expires in
30 days.
See Configure Licensing for device limits, expiration, and offline grace periods.
Step 9: Distribute to Customers¶
Delivery Page¶
If you use Stripe Checkout, customers are redirected to the PyLocket delivery page after purchase. When your app has builds for multiple platforms, the delivery page shows labeled download buttons for each:
See Distribute Your App for Stripe, Gumroad, Paddle, and other storefront integrations.
Your Own Download Server¶
Host the protected artifacts on your own infrastructure (your website, GitHub Releases, S3, etc.) and provide per-platform download links to customers.
Step 10: The End-User Experience¶
Here's what your customer sees after purchasing:
- Downloads the protected app from the delivery page or your website
- Runs the executable — it looks and works like any normal application
- License prompt appears on first launch — customer enters their license key
- Activation contacts PyLocket's server, validates the key, and registers the device
- App runs normally — protected functions are decrypted on demand, transparently
- Offline use — after activation, the app works offline for a configurable grace period
- Device limit — if the customer exceeds their device limit, activation is denied; you can reset devices from the portal
Step 11: Automate with CI/CD¶
Complete GitHub Actions workflow that builds, protects, and publishes for all three platforms:
# .github/workflows/release.yml
name: Release
on:
push:
tags: ["v*"]
jobs:
build:
strategy:
matrix:
include:
- os: windows-latest
platform: win-x64
artifact: dist/myapp.exe
build: pyinstaller --onefile myapp.py
# The Linux onefile binary is extensionless; rename it to .elf for upload.
- os: ubuntu-latest
platform: linux-x64
artifact: dist/myapp.elf
build: pyinstaller --onefile myapp.py && mv dist/myapp dist/myapp.elf
# macOS .app bundles must use --onedir --windowed, never --onefile.
- os: macos-latest
platform: macos-arm64
artifact: dist/MyApp.app
build: pyinstaller --onedir --windowed myapp.py
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pyinstaller pylocket -r requirements.txt
- run: ${{ matrix.build }}
# Protect and download the protected artifact (--out waits for completion)
- run: |
pylocket protect \
--app ${{ secrets.PYLOCKET_APP_ID }} \
--artifact ${{ matrix.artifact }} \
--platform ${{ matrix.platform }} \
--python 3.12 \
--out dist/protected/
shell: bash
env:
PYLOCKET_TOKEN: ${{ secrets.PYLOCKET_TOKEN }}
- uses: actions/upload-artifact@v4
with:
name: protected-${{ matrix.platform }}
path: dist/protected/
Troubleshooting¶
| Question | Answer |
|---|---|
Can I upload a .dmg or .msi? |
No. Upload the executable inside it. Wrap the protected output in an installer afterward. |
| Can I cross-compile? | PyInstaller does not support cross-compilation. Build on each target OS or use CI/CD runners. |
| Do I need three separate apps? | No. One app, multiple platform builds. |
| One license for all platforms? | Yes. License keys are platform-independent. Each OS activation counts as a separate device. |
| What if I only have one platform? | That's fine — upload a single build. The delivery page shows one download button. |
What does --python do? |
Optional hint for the Python version used to build the artifact. The engine detects the version from the artifact itself and only falls back to the flag when detection fails. |
My build is an --onedir output |
ZIP the entire output directory and upload the .zip. |
My macOS .app was rejected as an unrecognised artifact |
It was almost certainly built with --onefile --windowed. A macOS .app must be built with --onedir --windowed. Rebuild and re-upload. See Protect a PyInstaller Application. |
Next Steps¶
| Topic | Link |
|---|---|
| Format-specific protection guides | PyInstaller, cx_Freeze, Briefcase, Wheel |
| Distribution strategies | Distribution Strategies |
| CI/CD integration | CI/CD Integration |
| Licensing configuration | Configure Licensing |
| Supported platforms reference | Supported Platforms |
| Marketplace distribution | Marketplace Distribution |