Use Cython
to Protect Source Code¶
When you use uv pip install
to install your project into pyembed/python
, your Python code is distributed as-is.
If you want to protect your source code, you can use Cython
or Nuitka
to compile your Python code into binary modules.
We recommend Cython
because it integrates well with the setuptools
used by PyTauri.
Usage¶
First, ensure that you meet the prerequisites for Cython
.
Then, add Cython
as a build dependency:
[build-system]
requires = [
# ...
"Cython>=3",
]
build-backend = "setuptools.build_meta"
Add the following content to setup.py
. Pay attention to the following parameters and their comments:
USE_CYTHON
SRC_PREFIX
INCLUDE_FILE_PATTERNS
EXCLUDE_FILE_PATTERNS
"""See: <https://setuptools-rust.readthedocs.io/en/latest/setuppy_tutorial.html>"""
from collections.abc import Sequence
from importlib.util import cache_from_source
from logging import getLogger
from os import getenv
from os.path import abspath, normcase
from pathlib import Path
from Cython.Build import ( # pyright: ignore[reportMissingTypeStubs]
cythonize, # pyright: ignore[reportUnknownVariableType]
)
from setuptools import setup
from setuptools.command.install import install as _install
from setuptools_rust import RustExtension
########## Inputs ##########
PYTAURI_STANDALONE = getenv("PYTAURI_STANDALONE") == "1"
"""Instead of building pytauri as a extension module file, it will be loaded in memory through Rust's `append_ext_mod`"""
USE_CYTHON = getenv("USE_CYTHON") == "1"
"""Whether to use Cython to compile the Python code into C extension module to protect the source code."""
SRC_PREFIX = "python/"
# The glob pattern of the source files you want to protect,
# or you can set it to `"tauri_app/**/*.py"` to protect all Python files in the project.
INCLUDE_FILE_PATTERNS = ("tauri_app/private.py",)
# Usually we dont need to protect the `**/__init__.py` files.
# NOTE: you must exclude `**/__main__.py`: <https://groups.google.com/g/cython-users/c/V-i0a8r-x00>.
EXCLUDE_FILE_PATTERNS = ("tauri_app/**/__init__.py", "tauri_app/**/__main__.py")
##############################
_logger = getLogger(__name__)
class install(_install): # noqa: N801
"""Subclass `setuptools.command.install` to exclude protected files in the Wheel.
ref: <https://setuptools.pypa.io/en/latest/userguide/extension.html>
"""
def run(self) -> None:
"""Remove protected files after installation and before writing into the Wheel,
to prevent them from being packaged into the Wheel."""
super().run() # pyright: ignore[reportUnknownMemberType]
# skip if `pip install -e`
build_py_obj = self.distribution.get_command_obj("build_py")
build_py_obj.ensure_finalized()
if build_py_obj.editable_mode:
return
# ref: <https://github.com/pypa/setuptools/blob/6ead555c5fb29bc57fe6105b1bffc163f56fd558/setuptools/_distutils/command/install_lib.py#L115-L124>
assert self.install_lib is not None
install_lib = Path(self.install_lib)
def norm_files_set(patterns: Sequence[str]) -> set[str]:
"""Normalized set of file paths"""
files_set: set[str] = set()
for pattern in patterns:
for file in install_lib.glob(pattern):
files_set.add(normcase(abspath(file)))
return files_set
include_files_set = norm_files_set(INCLUDE_FILE_PATTERNS)
exclude_files_set = norm_files_set(EXCLUDE_FILE_PATTERNS)
for file in include_files_set.difference(exclude_files_set):
protected_file = Path(file)
_logger.info(f"Removing protected file: {protected_file}")
# remove the protected files from the Wheel
protected_file.unlink()
# remove C files generated by Cython
protected_file.with_suffix(".c").unlink(missing_ok=True)
# why `.cpp`: <https://cython.readthedocs.io/en/latest/src/userguide/wrapping_CPlusPlus.html#specify-c-language-in-setup-py>
protected_file.with_suffix(".cpp").unlink(missing_ok=True)
# remove *potential* python bytecode files (e.g., `pip install --compile-bytecode`)
# ref:
# - <https://docs.python.org/3/using/cmdline.html#cmdoption-O>
# - <https://peps.python.org/pep-0488/>
# - <https://docs.python.org/3/library/importlib.html#importlib.util.cache_from_source>
for optimization in ("", 1, 2):
bytecode_file = cache_from_source(
protected_file, optimization=optimization
)
Path(bytecode_file).unlink(missing_ok=True)
setup(
####################
# pyo3 extension module
####################
rust_extensions=[
RustExtension(
# set `target` the same as `[project.entry-points.pytauri.ext_mod]` in `pyproject.toml`
target="tauri_app.ext_mod",
# It is recommended to set other features in `Cargo.toml`, except following features:
features=[
# see: <https://pyo3.rs/v0.23.3/building-and-distribution.html#the-extension-module-feature>,
# required to build the extension module
"pyo3/extension-module",
# This feature tells Tauri to use embedded frontend assets instead of using a frontend development server.
# Usually this feature is enabled by `tauri-cli`, here we enable it manually.
"tauri/custom-protocol",
],
)
]
if not PYTAURI_STANDALONE
else [],
####################
# Cython
####################
cmdclass={"install": install} if USE_CYTHON else {},
# See: <https://cython.readthedocs.io/en/latest/src/quickstart/build.html#building-a-cython-module-using-setuptools>
ext_modules=cythonize( # pyright: ignore[reportUnknownArgumentType]
module_list=[SRC_PREFIX + pattern for pattern in INCLUDE_FILE_PATTERNS],
exclude=[SRC_PREFIX + pattern for pattern in EXCLUDE_FILE_PATTERNS],
)
if USE_CYTHON
else [],
)
Add
*.c
(generated byCython
) to.gitignore
to avoid committing them if you don't want to.
When building a standalone binary (uv pip install
), you only need to add the environment variable USE_CYTHON=1
to compile your Python code with Cython
.
Check the following directories, and you will find that the *.py
source files matching INCLUDE_FILE_PATTERNS
have been compiled into *.so
/*.pyd
files. Additionally, the *.c
files generated by Cython
(which could also leak your source code) are not included:
- Windows:
src-tauri\pyembed\python\Lib\site-packages\{your_package}
- Unix:
src-tauri/pyembed/python/lib/python{version}/site-packages/{your_package}
Tip
During development, you can still use uv pip install -e
to install your project in development mode. This means you can modify *.py
files and see the changes immediately (without compiling with Cython
). That's awesome 🎉!
FAQ¶
The Size of the Binary Modules¶
It is not recommended to set INCLUDE_FILE_PATTERNS
to a wildcard like **/*.py
. Compiling a binary module can increase your application size from a few kilobytes for *.py
files to several hundred kilobytes. Of course, if you don't mind the size increase, this is not an issue.
If you do want to do this and still care about size, check out the Cython
tutorial (NOTE: it is complex and requires manual steps): https://cython.readthedocs.io/en/latest/src/userguide/source_files_and_compilation.html#shared-utility-module
If possible, consider consolidating the code you want to compile into a single file. This not only reduces size but also allows Cython
to better optimize and accelerate them through inlining: https://cython.readthedocs.io/en/latest/src/userguide/faq.html#what-is-better-a-single-big-module-or-multiple-separate-modules.
Why Hack the setuptools.command.install.install
¶
Setuptools does not provide an API to exclude certain files from the Wheel: https://github.com/pypa/setuptools/issues/511.