やりたいこと
Pythonはスクリプト言語でソースコードを読み込みながら実行します。したがってPythonで作ったプログラムを直接顧客に渡すとソースコードも一緒に渡さないといけません。PyArmorというツールを使うとPythonのソースコードを難読化することができますが、Python 3.11に未対応だったり、商用利用では有償です。そこで代替案としてCythonを使って自分が書いた部分全体を共有ライブラリ化して渡すという方法を試してみます。
環境
- OS: Ubuntu 22.04, Debian 11などのLinux
- Python: 3.11
- Cython: 3.0.0a11
- 依存管理: PDM 2.4以降 (PDMについてはこちら1を参照)
Cythonは安定板の0.29系統ではなく3系を使用します。0.29系と比べて多くの最新のPythonの構文にも対応しています。ただ、現状ではまだmatch文には対応していません。
手順
上記のレポジトリに必要なファイルをまとめてあります。通常の開発時にはそのままPythonのプロジェクトとしていつも通りに開発します。一通り開発が完了したらpdm run python setup.py build
を実行します。これでbuild/lib.linux-x86_64-cpython-311
以下に各.py
ファイルに対応する.so
ファイルができています。このプロジェクトは
src
└── app
├── __init__.py
└── backend
├── __init__.py
└── rand.py
のような構成でしたので
build/lib.linux-x86_64-cpython-311
└── app
├── __init__.cpython-311-x86_64-linux-gnu.so
├── backend
│ ├── __init__.cpython-311-x86_64-linux-gnu.so
│ └── rand.cpython-311-x86_64-linux-gnu.so
└── version.cpython-311-x86_64-linux-gnu.so
のようになりました。
ちなみにsetup.py
は以下の通りです。こちら2のほぼコピペです。
# coding: utf-8
import os
from Cython.Build import cythonize
from setuptools import find_packages, setup
EXCLUDE_FILES = []
def get_ext_paths(root_dir: str, exclude_files: list[str]):
"""get filepaths for compilation"""
paths = []
for root, dirs, files in os.walk(root_dir):
for filename in files:
if os.path.splitext(filename)[1] != ".py":
continue
file_path = os.path.join(root, filename)
if file_path in exclude_files:
continue
paths.append(file_path)
return paths
setup(
packages=find_packages(),
ext_modules=cythonize(
get_ext_paths("src", EXCLUDE_FILES),
compiler_directives={"language_level": 3},
build_dir="build",
),
)
あとはbuild/lib.linux-x86_64-cpython-311
をPYTHONPATH
にセットすれば別途用意したmainのスクリプトからimportすることができます。ユーザーに渡すときにはsrc
以下の内容をごっそりbuild/lib.linux-x86_64-cpython-311
のものと置き換えてしまうという方法もあります。なお、PDMはルートの__init__.py
を探すのでこのファイルだけはテキストの.py
のままにしておく必要があります。参考としてDockerfileを載せておきます。
#----------cython-builder----------#
FROM python:3.11 as cython-builder
WORKDIR /project
RUN pip install -U pip setuptools wheel
RUN pip install "cython>=3.0.0a11"
COPY src /project/src
COPY setup.py /project/setup.py
RUN python setup.py build
RUN mv src raw
RUN mv build/lib.linux-x86_64-cpython-311 cython
#----------builder----------#
FROM python:3.11 as builder
WORKDIR /project
RUN pip install -U pip setuptools wheel
RUN pip install pdm
COPY pyproject.toml pdm.lock /project/
RUN pdm sync --prod --no-self
# cython: use cython to obfuscate code
# raw: no obfuscation
ARG BUILD_TYPE="cython"
COPY --from=cython-builder /project/${BUILD_TYPE} /project/src
# root __init__.py needs to be a text file for PDM to read it
COPY --from=cython-builder /project/raw/app/__init__.py /project/src/app/__init__.py
RUN pdm sync --prod --no-editable
#----------runner----------#
FROM python:3.11-slim as runner
WORKDIR /project
COPY --from=builder /project/.venv /project/.venv
COPY main.py /project/main.py
ENV PATH /project/.venv/bin:$PATH
CMD ["python", "main.py"]