Skip to content
PyArmorDEMO

Chapter 08

Packaging

⏱ 15 minutes · You will learn what a protected application consists of, and ship it two ways: as an archive, and as a single-folder executable built with PyInstaller. (The third way, a Docker image, is the next chapter.)

What you are shipping

A protected Python application is always these four parts:

PartWhere it comes fromProtected?
Your protected codebuild/protected/app/✓ obfuscated by PyArmor
The PyArmor runtimebuild/protected/pyarmor_runtime_000000/a compiled binary
Third-party dependenciesrequirements.txt (FastAPI, uvicorn, pydantic, ...)✗ plain, as published on PyPI
A Python interpreterthe target machine, a bundle, or a container imagen/a

The interpreter has to be the same Python minor version that built the code (chapter 06). Every packaging method below is a different way to deliver those four parts together.

Option 1: an archive

The simplest delivery: the protected folder, plus a pinned dependency list.

Terminal
mkdir -p dist
tar -czf dist/pyarmor-demo-protected.tar.gz -C build/protected .

What it does: packs app/ and pyarmor_runtime_000000/ into one compressed archive (tar is built into macOS, Linux and Windows 10 and later). Why: this is what you hand to a customer, or copy to a server you manage. Expected: dist/pyarmor-demo-protected.tar.gz, a few hundred KB.

On the target, the recipient unpacks it, installs dependencies with pip install --require-hashes -r requirements.txt into a Python 3.12 environment on the same OS and CPU architecture, and runs python -m app.

Option 2: a single-folder executable with PyInstaller

PyInstaller is a separate tool. It bundles a Python interpreter, your code and every dependency into one folder (or one file) with an executable. The person running it does not need Python installed.

The two tools have different jobs:

ToolJob
PyArmormakes your code hard to read
PyInstallerputs code, interpreter and dependencies into one deliverable

PyInstaller alone does not protect code: it stores ordinary Python bytecode, which can be extracted and decompiled. And PyInstaller cannot see the imports inside obfuscated code, so it would leave modules out. pyarmor gen --pack solves this: it lets PyInstaller analyse the original scripts, then swaps in the obfuscated versions.

PyInstaller needs a script to start from, and python -m app is not a script. The repository includes run_api.py, which just calls the same main().

Terminal
uv run --with pyinstaller==6.22.3 pyarmor gen --pack onedir --exclude ".venv" --exclude ".venv/*" --exclude app/frontend -r run_api.py app

What it does: uv run --with adds PyInstaller 6.22.3 for this one command, without adding it to the project. PyArmor obfuscates run_api.py and the app package, then runs PyInstaller in onedir mode with the obfuscated code and the runtime. Why --exclude? In pack mode, PyArmor also obfuscates any imported module it finds under the current folder, and .venv/ (and app/frontend/node_modules/) is under it. Without the excludes it tries to obfuscate libraries from your virtual environment and fails with ERROR out of license (a library module is "big" for the trial). Expected: PyInstaller's build log, then a dist/run_api/ folder (about 35 MB) with the run_api executable and an _internal/ folder that contains pyarmor_runtime_000000/.

Run the bundle:

Terminal
./dist/run_api/run_api              # Windows: .\dist\run_api\run_api.exe

What it does: starts the protected API from the bundle, with no Python environment needed. Expected: Uvicorn running on http://127.0.0.1:8300. In another terminal, uv run python scripts/smoke.py --expect-build protected passes.

Clean up: rm -rf dist .pyarmor (the .pyarmor/ folder holds pack-mode work files).

Which one?

You ship toUse
Servers you runa Docker image (chapter 09)
A customer's servera Docker image, or an archive and install instructions
Desktop users without Pythona PyInstaller bundle