← All posts
Automating Python Compilation with Py2Native in Your CI/CD Pipeline

Automating Python Compilation with Py2Native in Your CI/CD Pipeline

python compiler cython code protection open source

Automating Python Compilation with Py2Native in Your CI/CD Pipeline

Automating Python code compilation starts with removing the manual uv run py2native build ... from release day. If you are still shipping .py files or running builds by hand before every release, you are inviting the same set of mistakes: a missed flag, a stale artifact, a wrong platform, or source code that accidentally lands somewhere it should not.

Py2Native turns plain Python into native machine code from a single CLI command. The next step is making that command run reliably without you. This article shows a one-time setup, real CI/CD workflows, failure handling, and the habits that keep automated compilation boring and predictable.

The Manual Compilation Bottleneck

A common release-day routine looks like this:

  1. Open a terminal on the right machine.
  2. Reinstall or update the toolchain.
  3. Run the build command with the correct globs and flags.
  4. Hope the output still matches the last release.
  5. Copy the artifact somewhere before someone overwrites it.

That flow breaks when the person who owns the process is unavailable, when CI runs on a different OS, or when the build depends on a secret that should never be on a developer laptop.

The manual Cython/C-extension route is even harder: you maintain .pyx files, write build glue, and manage platform-specific extension details. Py2Native removes that ceremony. You write ordinary Python, point the compiler at your sources, and get a native executable, shared library, wheel, or embedded deployment directory. In automation terms, that means the CI pipeline only has to invoke one command.

One-Time Setup: Preparing Your Project for Automated Builds

Make Py2Native reproducible in your project by adding it with uv:

uv add py2native

Commit your uv.lock file. That gives CI a consistent dependency graph instead of resolving whatever is newest at build time.

Next, decide which output you want in the pipeline:

# Native executable
uv run py2native build main.py src/*.py

# Native library plus wheel
uv run py2native build main.py src/*.py --library --wheel dist/

# Embedded uv-managed deployment directory
uv run py2native build main.py src/*.py --embed deploy/

A small build script keeps the flags in one place:

#!/usr/bin/env bash
# scripts/build.sh
set -euo pipefail

uv run py2native build \
  main.py \
  src/*.py \
  --wheel dist/ \
  --license license.dat \
  --public public.pem

The script reads the license and public key from the repository workspace. In CI, the license file is written from a secret.

Pro license setup in CI

If you use the Pro plugin, license verification belongs in the automated path. Generate the keypair once in a controlled environment:

uv run py2native keygen private.pem public.pem

Sign a license to a JWT file:

uv run py2native sign --private private.pem \
  '{"sub":"acme","exp":1798761600}' \
  license.dat

Store private.pem only in your CI secret store. The public key can live in the repository because it is designed to be distributed. During the build, pass both the license and public key:

uv run py2native build main.py src/*.py \
  --license license.dat \
  --public public.pem

The Pro plugin verifies the license before compilation. For runtime license checks inside a binary, see How to Verify JWT Licenses in Python Compiled Binaries with Py2Native Pro.

The Automated Pipeline: CI/CD Integration Examples

Py2Native is a command-line tool, so any CI/CD platform that can run shell commands can build native artifacts.

GitHub Actions

This workflow runs on version tags, restores the uv cache, builds a wheel, and stores the artifact:

name: build-native

on:
  push:
    tags: ["v*"]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: astral-sh/setup-uv@v5

      - name: Cache uv
        uses: actions/cache@v4
        with:
          path: ~/.cache/uv
          key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}

      - name: Install dependencies
        run: uv sync --locked

      - name: Write Pro license file
        run: printf '%s' "${{ secrets.PY2NATIVE_LICENSE }}" > license.dat

      - name: Build native wheel
        run: uv run py2native build main.py src/*.py --wheel dist/ --license license.dat --public public.pem

      - name: Upload wheel
        uses: actions/upload-artifact@v4
        with:
          name: native-wheel-linux
          path: dist/

The important part is the build step: same command as local development, but running from a clean runner.

GitLab CI

GitLab follows the same pattern with stages for build, test, and artifact upload:

image: python:3.12

stages: [build, test, publish]

before_script:
  - curl -LsSf https://astral.sh/uv/install.sh | sh
  - export PATH="$HOME/.cargo/bin:$PATH"
  - uv sync --locked

build:
  stage: build
  script:
    - printf '%s' "$PY2NATIVE_LICENSE" > license.dat
    - uv run py2native build main.py src/*.py --library --wheel dist/ --license license.dat --public public.pem
  artifacts:
    paths:
      - dist/
    expire_in: 7 days

For a private artifact repository, upload the resulting wheel from the dist/ directory as a normal package. The build job is complete when the wheel contains the compiled shared library and the generated __init__.py/__main__.py import bridge.

Monitoring and Failure Handling in Automated Builds

A successful pipeline is quiet. A useful pipeline is noisy when it fails.

Notify the right people

In GitHub Actions, add a failure-only notification:

- name: Notify Slack on failure
  if: failure()
  run: |
    curl -sS -X POST \
      -H 'Content-Type: application/json' \
      --data '{"text":"Py2Native build failed: ${{ github.run_id }}"}' \
      "${{ secrets.SLACK_WEBHOOK_URL }}"

The same pattern works in GitLab or any other CI system. The message should include a link to the failed run and the artifact name or build matrix leg.

Keep logs close to artifacts

Tee the build output into a log and upload it with the artifact:

uv run py2native build main.py src/*.py --wheel dist/ \
  --license license.dat --public public.pem \
  2>&1 | tee build.log

On failure, the log tells you whether the problem is in source globbing, Cython generation, C compilation, linking, or license verification.

Common failures and responses

Failure What to check
Missing C compiler Install MSVC on Windows, GCC on Linux, or Clang on macOS on the runner.
Cython/source errors Run the same command locally with the same Python version; check for identically named modules across source files.
License verification failure Ensure license.dat is valid, not expired, and was signed by the private key that matches public.pem.
Transient network failure during dependency download Retry the build step instead of failing the whole pipeline.

A retry wrapper is useful when uv or the compiler downloads Python distributions:

- name: Build with retry
  uses: nick-fields/retry@v3
  with:
    command: uv run py2native build main.py src/*.py --wheel dist/ --license license.dat --public public.pem
    max_attempts: 3
    timeout_minutes: 10

Best Practices for Reliable Automated Compilation

These habits prevent most CI pain before it starts.

# replace with your real executable name and test flag
./deploy/bin/myapp --self-test

That verifies the native binary runs, not just that it linked.

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]

Py2Native compiles natively on the target platform, so each matrix leg should run on the OS it is intended to produce.

FAQ

Can I automate Py2Native builds on any CI/CD platform?

Yes. Py2Native is a command-line tool, so it works with any CI/CD system that can run shell commands. Install uv, sync your project, and run uv run py2native build with the desired options.

How do I handle Pro license verification in an automated pipeline?

Store your license file as a secret in the CI environment. Write it to a temporary file before the build, then pass it with --license license.dat and your public key with --public public.pem. The Pro plugin verifies the JWT before compilation.

Does Py2Native support cross-compilation for different platforms?

Py2Native compiles natively on the target platform. For cross-platform builds, use CI runners for each operating system and run the build on Windows, Linux, and macOS separately.

What are the system requirements for running Py2Native in CI?

The runner needs CPython 3.11–3.15, including free-threaded 3.14t and 3.15t builds, plus a platform C compiler. Windows runs use MSVC, Linux runs use GCC, and macOS runs use Clang. The runner also needs internet access to download Python distributions and dependencies.

Conclusion

Automating Python code compilation with Py2Native replaces a manual, person-dependent release step with a repeatable pipeline. The workflow is not a bundle of build scripts wrapped around Cython: it is plain Python sources in, native artifacts out, with the same command in local development and CI.

Start by adding Py2Native to your project at Py2Native, then move the build command into a tag-triggered workflow. Pin the lockfile, cache uv, and make the Pro private key a secret. From there, every release produces a protected native binary without anyone touching a compiler.

Related posts

EU label: AI-generated content