Packaging & compiled apps

py-rizmi is tested end-to-end inside compiled binaries. Here's what matters.

Nuitka (recommended)

nuitka --standalone --onefile \
    --include-package=py_rizmi \
    --output-dir=dist main.py

# PyQt6 toolkit apps: add --enable-plugin=pyqt6
  • sqlite3 is stdlib — no extra data files to declare.
  • Compiles your embedded public key and fingerprint constants into native C. Patching them means rebuilding the binary — a far higher bar than editing an archive.
  • An e2e test in the repo builds a Nuitka standalone and runs the whole activate → tamper → detect cycle inside it on every release.

PyInstaller

pyinstaller --onefile main.py
# plain import works; no hidden-import needed for py_rizmi core
PyInstaller archives are trivially extractable — bytecode and embedded constants can be read and patched with off-the-shelf tools. Functional, but choose Nuitka when licensing strength matters.

The one rule that makes both work

Never derive state paths from __file__. Nuitka onefile extracts to a temp dir wiped at exit; PyInstaller's __file__ points inside _MEIPASS. py-rizmi already handles this: writable state always resolves to the platformdirs user-data location:

OSState DB location
Linux~/.local/share/<AppName>/state.db
macOS~/Library/Application Support/<AppName>/state.db
Windows%LOCALAPPDATA%\<AppName>\state.db

What lives where

Compiled into the binarystate.db (writable)
Vendor public key✓ as a constant + pinned fingerprintnever
Private keynever (your issuing machine only)never
Licenses / trial keys / clock marksHMAC-verified rows + fallback file

Release checklist

  • rizmi doctor run -a MyApp -P pub.pem --fingerprint <hex> passes on a clean VM
  • First run starts a trial; second run continues it (not restarted)
  • System clock rolled back → blocked with clock_tampering
  • state.db deleted → protection intact via fallback marks
  • License issued for another machine → rejected hwid_mismatch