Skip to content

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:

pyinstaller --onedir --windowed myapp.py

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

pip install pyinstaller
pyinstaller --onefile myapp.py
# Output: dist/myapp.exe

For a GUI app distributed as a .app bundle (the usual case), use --onedir --windowed. Do not use --onefile for a .app:

pip install pyinstaller
pyinstaller --onedir --windowed myapp.py
# Output: dist/MyApp.app

For a bare command-line binary (no .app bundle), --onefile is fine:

pyinstaller --onefile myapp.py
# Output: dist/myapp
pip install pyinstaller
pyinstaller --onefile myapp.py
# Output: dist/myapp

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:

pylocket apps create --name "MyApp"

Note the APP_ID in the output. You'll use it for all platform builds.

Or use the Developer Portal: go to AppsCreate App.


Step 5: Protect Each Platform Build

Upload and protect each platform's artifact:

pylocket protect \
  --app <APP_ID> \
  --artifact dist/myapp.exe \
  --platform win-x64 \
  --python 3.12
# A onefile binary has no extension; rename it to .elf before upload
mv dist/myapp dist/myapp.elf

pylocket protect \
  --app <APP_ID> \
  --artifact dist/myapp.elf \
  --platform macos-arm64 \
  --python 3.12

For a .app bundle, pass --artifact dist/MyApp.app instead (no rename needed).

# A onefile binary has no extension; rename it to .elf before upload
mv dist/myapp dist/myapp.elf

pylocket protect \
  --app <APP_ID> \
  --artifact dist/myapp.elf \
  --platform linux-x64 \
  --python 3.12

Each command returns a BUILD_ID. Protection runs in the cloud and typically takes 1–5 minutes.

Using the Portal

  1. Go to Apps → select your app → Upload Build
  2. Select the artifact file (the raw executable or zipped directory)
  3. Set the Platform dropdown (Windows, macOS, Linux)
  4. Click Upload Build
  5. 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:

dist/protected/
└── myapp.exe    # Protected executable (myapp.elf on Linux, MyApp.app.zip on macOS)

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:

mkdir -p dmg_contents
cp dist/protected/* dmg_contents/
hdiutil create -volname "MyApp" -srcfolder dmg_contents -ov MyApp.dmg

Simplest: Create a .tar.gz:

tar -czf myapp-linux-x64.tar.gz -C dist/protected/ .

Professional: Create a .deb package with dpkg-deb or an .rpm with rpmbuild that installs the protected binary.


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:

[Windows]  [macOS]  [Linux]

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:

  1. Downloads the protected app from the delivery page or your website
  2. Runs the executable — it looks and works like any normal application
  3. License prompt appears on first launch — customer enters their license key
  4. Activation contacts PyLocket's server, validates the key, and registers the device
  5. App runs normally — protected functions are decrypted on demand, transparently
  6. Offline use — after activation, the app works offline for a configurable grace period
  7. 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