Python 源码保护的自动化构建方案

标签:Docker, Python

Python 源码保护的方案主要有代码混淆、修改解释器和编译成二进制这三种,其他方式基本没有保护效果。而这三种方案中,最安全的就是用 Cython 来编译 py 文件(但是需要注意兼容性)。

使用的方法也很简单,先编写一个 setup.py 文件:
from distutils.core import setup
import glob

from Cython.Build import cythonize


py_files = glob.glob('*/**/*.py', recursive=True)  # 第一层的 py 文件不编译,因为 so 文件不支持直接用 python -m 来执行
setup(
    ext_modules=cythonize(
        py_files,
        nthreads=4  # 同时用 4 个进程来编译 py 到 c,根据自己的 CPU 核数来设置
    )
)
需要的话,可以参考文档来添加 compiler_directives

然后执行 python setup.py build_ext --inplace -j 4 就可以开始编译了,其中 --inplace 是让编译生成的 c 和 so 文件与 py 文件放在一起,-j 4 是同时使用 4 个进程来编译 c 到 so。
编译完成后,删掉其中的 py 和 c 文件,就可以发布了。执行时可以和普通的 Python 项目一样,使用 python -m app 之类的方式来运行即可。
如果第一层的 app.py 文件也很敏感,可以把它放在下层的包里,app.py 里只写一句 import xxx.app 即可。

这些过程手动处理起来比较麻烦,可以用 GitLab CI 来自动构建,几行代码即可搞定。但是随着项目的增长,又出现了新的问题:编译时间越来越长,甚至长达半小时之久。

于是我又研究了一下 Cython 的编译缓存,发现它会检查需要编译的 py 文件是否已经生成了对应的 so 文件;如果有且这个文件的修改时间比 py 文件更晚,它就会跳过编译这个 py 文件。而我在用 docker 来编译的时候,只 COPY 了源代码,并没有带上 so,自然就需要重新编译了。
可是如果直接把 so 文件也 COPY 过去,它的修改时间又比 py 文件晚了,即使改了代码也不会被重新编译,这显然也不行。

要解决这个矛盾的话,我想到了挂载一个 volume,把源码和 so 文件都保存在宿主机上,这样文件的修改时间就不会丢失了。
而在编译前还需要做些同步的准备工作,即比较需要编译的新源码文件夹和 volume 里的上次编译的文件夹,删除已经不存在的 py、c 和 so 文件,复制新增或修改过的 py 文件。这样新的 py 文件修改时间会比 so 更晚,会被重新编译;而其他 py 文件则会跳过编译。
这个解决方案看上去很完美,但是 docker build 并不支持挂载 volume,真是让人崩溃。
无奈之下我只能用 docker run 来挂载 volume。编译之后,再把 volume 里需要的文件(即第一层的 py 文件和后续层的 so 文件)复制到镜像里,然后用 docker commit 来保存新的镜像。注意:如果是本地用 docker 来构建的话,是可以在 Dockerfile 里使用 COPYADD的;但 GitLab CI 用的是 docker in docker,无法直接访问宿主机里的路径。
为此还需要写 2 个 Dockerfile,一个用来编译,一个用来打包:
build:
  script:
    - docker build -f Dockerfile_compile -t xxx-compile .
    - docker rm xxx_compile || true
    - docker run --name xxx_compile -v $CACHE_DIR:/root/xxx xxx-compile python /root/setup.py build_ext --inplace -j 4
    - docker commit xxx_compile xxx-compile
    - docker rm xxx_compile || true
    - docker build -f Dockerfile_package -t $IMAGE_NAME .
Dockerfile_package 所做的是从 xxx-compile 里复制 /usr/local/lib 和编译生成的文件到新的镜像,这样可以减少镜像的大小,避免保存中间层。
测试一下发现编译时间从半小时缩短到半分钟,搞定!

做到这里我又想到了新的方案,实际上不需要保存 py 和 so 的修改时间,只要保证不想编译的 py 文件有个更晚的 so 文件即可。
于是按最初的方案直接构建,保存成中间镜像。在第二步构建时,把中间镜像里的文件 COPY 到另一个文件夹,然后进行一次同步,把不需要编译的 so 文件 cp 到对应的路径。等待编译完成后,也保存成中间镜像。在第三步构建时,再把中间镜像里的文件 COPY 到新镜像即可。
这个方案既不需要暴露宿主机的文件夹,也不需要用到 docker commit 这种有代码洁癖的人不愿意使用的语句,完美!

0条评论 你不来一发么↓

    想说点什么呢?