Automate Python Compilation with Py2Native
If you have ever tried to protect a Python program by hand, you know the drill: write Cython syntax, keep .pyx and .pxd files in sync, invoke cythonize, figure out compiler flags for the local CPython installation, and then debug linker errors that differ on Windows, Linux, and macOS. That is the hard way.
Py2Native removes that loop. One command takes plain Python modules and compiles them to native machine code:
uv run py2native build main.py *.py
There is no manual Cython step, no .pyx files to maintain, and no C compiler configuration. The goal of this article is to make that command repeatable: set up a project once, wire the build into CI/CD, and ship compiled binaries without manual intervention.
One-Time Setup: Preparing Your Project for Automated Compilation
A Py2Native project is just a normal Python project. You do not need special annotations, Cython syntax, or a custom build script.
A minimal project might look like this:
your-project/
├── main.py
├── config.py
├── helpers.py
└── pyproject.toml
main.py is the entry point. Additional modules are compiled alongside it.
Add Py2Native to your development environment with uv, not pip:
uv add --dev py2native
Verify that the CLI works with a simple build:
uv run py2native build main.py
Py2Native defaults to executable mode, producing a standalone native binary. For distribution, you have two optional build outputs:
# Create a self-contained deployment directory
uv run py2native build main.py '*.py' --embed dist/app
# Create a wheel containing compiled artifacts
uv run py2native build main.py '*.py' --wheel dist
If you want to distribute code as an importable Python package while still hiding the source, use library mode:
uv run py2native build main.py '*.py' --library --wheel dist
Library mode creates a shared library plus the __init__.py and __main__.py files that bridge Python’s import system to the native library.
For Pro users, license verification setup is also a one-time task. Generate an EC P-256 keypair:
uv run py2native keygen private.pem public.pem
Sign a JWT payload to create a license file:
uv run py2native sign --private private.pem \
'{"iss":"RSJ Software GmbH","aud":"TimestampGIT"}' \
license.dat
Keep private.pem out of source control. The public key is what gets embedded in the executable. The complete Pro workflow is covered in more detail in the JWT license verification article.
The Automated Pipeline: Building Native Binaries in CI/CD
Once the project builds locally, the CI job is a short shell command. GitHub Actions works well because the runners already include C compilers and CPython headers.
Here is a cross-platform GitHub Actions workflow:
name: compile-native
on:
push:
branches: [main]
pull_request:
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.12"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Install project dependencies
run: uv sync
- name: Compile Python to native code
shell: bash
run: uv run py2native build main.py '*.py' --wheel dist
- name: Upload compiled artifact
uses: actions/upload-artifact@v4
with:
name: native-${{ matrix.os }}
path: dist/*
enable-cache: true tells the uv action to cache uv-managed files across runs. Py2Native still needs to download a Python distribution and build dependencies on a cold cache, but subsequent builds are faster.
For a self-contained deployment directory instead of a wheel, change the build step to:
uv run py2native build main.py '*.py' --embed dist/app
The same command works in GitLab CI, Jenkins, or any shell runner. A generic build script keeps the pipeline portable:
#!/usr/bin/env bash
set -euo pipefail
uv sync
uv run py2native build main.py '*.py' \
--wheel dist \
--embed dist/app
Pro builds: embed license verification
In Pro Edition builds, pass the license and public key files to the build command:
uv run py2native build main.py '*.py' \
--license license.dat \
--public public.pem \
--wheel dist
The Pro plugin bakes the signature verification code and public key into the executable. Only the public key is stored in the final artifact.
Py2Native Pro generates the bridge declaration for the verifier. The generated _p2n_bootstrap.pxd interface looks like this:
# Generated by py2nativepro during the Pro build
cdef _runtime_verify_es256_jwt(token, expected_iss=*, expected_aud=*)
Your application code calls the verifier through that generated bridge:
from _p2n_bootstrap cimport _runtime_verify_es256_jwt
def require_valid_license(token: str) -> bool:
license = _runtime_verify_es256_jwt(
token,
expected_iss="RSJ Software GmbH",
expected_aud="TimestampGIT",
)
return license
You do not manage the underlying bridge or verification code by hand. The Pro plugin supplies it during the compilation pipeline, so the build remains a single uv run py2native build call.
In CI, read the license and public key from secrets, not from committed files:
- name: Compile with license verification
shell: bash
env:
LICENSE_DAT: ${{ secrets.LICENSE_DAT }}
PUBLIC_PEM: ${{ secrets.PUBLIC_PEM }}
run: |
printf '%s' "$LICENSE_DAT" > license.dat
printf '%s' "$PUBLIC_PEM" > public.pem
uv run py2native build main.py '*.py' \
--license license.dat \
--public public.pem \
--wheel dist
Monitoring and Failure Handling in Automated Builds
Py2Native returns a non-zero exit code when compilation, linking, or license verification fails. In a shell script, set -euo pipefail makes the job stop immediately.
Common failure points to monitor:
- Missing C compiler or CPython headers — install the platform toolchain, or use a runner image that already has one.
- Unsupported Python version — Py2Native supports CPython 3.11 through 3.15. A matrix build with an unsupported version fails early.
- Duplicate module names — two source files with the same module name cause a build error.
- License verification errors — an invalid or expired JWT fails the Pro build when
--licenseis present.
After the build step, add a smoke test. Run the produced binary with a simple input or flag to verify that it starts and executes:
build/app/your-executable --smoke-test
If the smoke-test command returns non-zero, fail the job. This catches linking problems and missing runtime dependencies before artifacts are published.
Platform differences are normal. A Windows build produces a .pyd, Linux produces a .so, and macOS produces a .dylib. Py2Native’s platform plugins handle those naming and linking differences automatically, so use a CI matrix to build all three targets from the same source tree.
Best Practices for Automating Python Compilation
Keep the build reproducible. Pin the Python version, uv version, and Py2Native version in your project configuration. Commit the pyproject.toml generated by uv add --dev py2native so every runner resolves the same dependency set.
Use glob patterns deliberately. Include all application modules, but exclude tests, fixtures, and configuration files that do not belong in the binary:
uv run py2native build main.py 'app/*.py' --base .
Store private keys and license files in CI secrets. Never commit private.pem or license.dat to the repository. The public key and license verification logic are embedded into the binary; the private key remains only in the signing environment.
Compile on every push to catch breakages early. Publish artifacts only from tagged releases. This gives you continuous protection without shipping unstable binaries.
For package distribution, prefer library mode. It creates a wheel that installs like a normal Python package while hiding the source behind a compiled shared library:
uv run py2native build main.py 'app/*.py' --library --wheel dist
FAQ
Can I automate Py2Native compilation in any CI/CD system?
Yes. Py2Native is a command-line tool that works in any CI/CD environment. As long as the runner can execute uv and has a C compiler, you can add uv run py2native build main.py *.py to the pipeline. The examples in this article use GitHub Actions, but the same command applies to GitLab CI, Jenkins, and shell-based runners.
How do I handle multiple Python files or packages in automated builds?
Py2Native accepts glob patterns, so you can pass *.py or specific file patterns. For packages, include all application modules and avoid duplicate module names. The build command compiles all specified sources into a single binary or shared library.
Does Py2Native require any special build configuration or Cython knowledge?
No. Py2Native hides Cython completely. You write plain Python, and the tool handles transpilation, C compilation, and linking automatically. There are no .pyx or .pxd files to maintain, and no manual cythonize step.
Can I include license verification in automated builds with Py2Native Pro?
Yes. With the Pro plugin you can generate a keypair, sign a license, and pass --license and --public flags to the build command. The verification logic and public key are baked into the binary, so automated builds produce protected executables that check for a valid license at runtime.
Conclusion: Set It and Forget It
Py2Native turns Python code protection from a manual chore into a single pipeline step. Write plain Python, run one command, and get a native binary. No .pyx files to maintain, no platform-specific linker flags to remember, and no last-minute build debugging.
Start with the open-source Community edition at Py2Native. If you need runtime license enforcement, the Pro plugin adds JWT verification with the same automated build command.
Related posts
- Secure JWT License Verification with Py2Native Pro
- Py2Native vs Nuitka: Which Python Compiler Protects Your Code Better?
- Python Code Obfuscation Tools: What Are Your Options?