世事如茶,初入喉肠涩且苦,再回味时甘且甜
本文从易到难,简单梳理了.so
文件编译的几种方案:最原始的编译方法、不删除中间文件的编译方法和相对集成的编译方案。推荐直接食用第三种集成方案。
相关代码已开源,仓库地址:https://github.com/itlubber/py2so
.py
文件编译为 .so
文件,实现源代码加密的同时,还能加速代码的运行速度。so文件
编译主要通过 cython
库实现,而 cython
库又依赖于 C
的环境,所以在编译之前,需要保证服务器或者本地环境中有相关的环境。本文默认使用 linux
环境进行编译,windows
环境的编译基本暂不涉及。anaconda
安装的 python
基本上环境都可以直接编译打包,如果中途报错可以再回头来安装相应的环境或包。linux
的包管理器安装 python-devel
和 gcc
,再通过 python
的包管理器 pip
或 pip3
安装 cython
库。如果服务器上已安装 python-devel
和 gcc
,直接通过 pip
安装 cython
即可。sudo
进行提权。ubuntu
环境准备apt-get install python-devel
apt-get install gcc
pip install cython
centos
环境准备yum install python-devel
yum install gcc
pip install cython
debug
通了再来吧,求求了。cython
编译还是有局限滴,支持不了辣么多骚操作。so文件
只能在与编译时相同的 python版本
和 编译平台
上运行。例如在 ubuntu 18.04 LTS
上使用 python 3.8
编译完成的项目,迁移到 ubuntu 18.04 LTS
上使用 python 3.6
执行或者迁移到 centos 7
上使用 python 3.8
执行都是无法执行的。-
main.py
# -*- coding: utf-8 -*-
'''
@Time : 2022/8/23 13:12
@Author : itlubber
@Site :
'''
# import sys
# sys.path.append('build/lib.macosx-10.9-x86_64-3.8') # 将编译后的包路径添加到python的包路径中
from example.your_codes import itlubber_py2so # 导入需要的包
def read_data(file):
'''
读取文件中的内容
'''
with open(file, 'r') as f:
return f.readline()
if __name__ == '__main__':
msg = read_data('example/README.md') # 读取非 python 文件中的内容
print(itlubber_py2so(msg)) # 传入文件内容, 打印 itlubber_py2so 返回的结果
-
example/your_codes.py
# -*- coding: utf-8 -*-
'''
@Time : 2022/8/23 13:12
@Author : itlubber
@Site :
'''
def itlubber_py2so(msg):
'''
根据传入 msg, 返回处理后的结果
'''
return 'py2so : {}'.format(msg)
-
example/README.md
https:///python-py2so
执行 main.py
主程序的结果如下:
example
包下的 your_codes.py
编译为 so文件
,并在主程序 main.py
中使用。python
版本和编译的平台,只能编译设定的 .py
文件,其他文件需要手动复制过去或者重新设定python的包路径。(1) 在 main.py
同级目录下方新建 setup.py
文件,具体内容如下:
from distutils.core import setup
from Cython.Build import cythonize
# 程序执行命令: python setup.py build_ext
setup(ext_modules = cythonize(['example/your_codes.py']))
(2) 执行编译命令
python setup.py build_ext
(3) 编译结果
(4) 编译测试
main.py
复制到编译后的 example
同级目录,将 example/README.md
复制到编译后的 example
目录下方。完成复制后执行 main.py
文件。main.py
中添加编译后的包路径后执行 main.py
脚本。为了方便本文采用添加包路径进行测试。修改 main.py
中的内容如下:import sys
sys.path.append('build/lib.macosx-10.9-x86_64-3.8') # 将编译后的包路径添加到python的包路径中
from example.your_codes import itlubber_py2so # 导入需要的包
def read_data(file):
'''
读取文件中的内容
'''
with open(file, 'r') as f:
return f.readline()
if __name__ == '__main__':
msg = read_data('example/README.md') # 读取非 python 文件中的内容
print(itlubber_py2so(msg)) # 传入文件内容, 打印 itlubber_py2so 返回的结果
执行 main.py
可以发现执行结果与未编译前的结果一致,即编译成功。
半集成编译方案
.py
文件,以及除 python
文件外不需要被复制的文件。具体内容如下:# -*- coding: utf-8 -*-
'''
@Time : 2022/8/23 13:12
@Author : itlubber
@Site :
@File : py2so.py
'''
import os
import sys
import time
import shutil
from distutils.core import setup
from Cython.Build import cythonize
currdir = os.path.abspath('.')
build_dir = 'build'
build_tmp_dir = build_dir '/temp'
def build_list(basepath, name='', excepts=[], copyOther=False, delC=False, starttime=None):
'''
获取py文件的路径
:param basepath: 根路径
:param name: 文件/夹
:param copy: 是否copy其他文件
:param excepts: 不拷贝的文件
:return: py文件的迭代器
'''
excepts_files = [os.path.join(currdir, f) for f in excepts]
fullpath = os.path.join(basepath, name)
for fname in os.listdir(fullpath):
ffile = os.path.join(fullpath, fname)
if os.path.isdir(ffile) and fname != build_dir and not fname.startswith('.'):
for f in build_list(basepath, name=fname, copyOther=copyOther, delC=delC, starttime=starttime):
yield f
elif os.path.isfile(ffile):
ext = os.path.splitext(fname)[1]
if ext == '.c':
if delC and os.stat(ffile).st_mtime > starttime:
os.remove(ffile)
elif os.path.splitext(fname)[1] not in('.pyc', '.pyx'):
if os.path.splitext(fname)[1] in ('.py', '.pyx') and not fname.startswith('__'):
yield os.path.join(name, fname)
elif ffile not in excepts_files and copyOther:
dstdir = os.path.join(basepath, build_dir, name)
if not os.path.isdir(dstdir):
os.makedirs(dstdir)
shutil.copyfile(ffile, os.path.join(dstdir, fname))
else:
pass
def copy_file(source):
dist_dir = os.path.join(currdir, build_dir, source)
source_dir = os.path.join(currdir, source)
if not os.path.isdir(source_dir):
if not os.path.exists(os.path.dirname(dist_dir)):
os.makedirs(os.path.dirname(dist_dir))
shutil.copyfile(source_dir, dist_dir)
else:
for file in os.listdir(source_dir):
copy_file(os.path.join(source_dir, file))
if __name__ == '__main__':
starttime=time.time()
# 不编译的 python 文件
excepts_build = ['py2so.py', 'main.py']
# 不复制到编译后的非 python 文件
excepts_files = ['README.md', 'LICENSE', '.gitignore', 'setup.py']
# 编译 python 文件
module_list = list(build_list(currdir, starttime=starttime))
module_list = [py for py in module_list if py not in excepts_build]
setup(ext_modules = cythonize(module_list), script_args=['build_ext', '-b', build_dir, '-t', build_tmp_dir])
# 拷贝其他文件
list(build_list(currdir, excepts=excepts_files, copyOther=True, starttime=starttime))
# 拷贝不编译的py文件
for file in excepts_build:
copy_file(file)
# 删除编译产生的中间文件
module_list = list(build_list(currdir, delC=True, starttime=starttime))
if os.path.exists(build_tmp_dir):
shutil.rmtree(build_tmp_dir)
print ('complate! time:', time.time()-starttime, 's')
build
下方,编译完成后中间文件全部删除,且非 .py
文件也会复制到对应位置。python py2so.py
执行 build
目录下方的 main.py
可以发现执行结果与未编译前的结果一致,即编译成功:
集成编译方案
so文件
,非常方便,但有部分 bug
待修复 QaQ ,使用时需要把脚本放置在整个项目文件夹的上级目录而非同级目录(后续有时间会修复这个 bug
,感觉还是同级目录方便;同时结果文件存放的路径不能为 build
,这个后续会默认放 build
,暂时先这样吧 QaQ)# -*- coding: utf-8 -*-
'''
@Time : 2022/8/23 13:12
@Author : itlubber
@Site :
@File : py2so_ensemble.py
'''
import os
import sys
import shutil
import argparse
import platform
from distutils.core import setup
from Cython.Build import cythonize
from distutils.extension import Extension
def getfiles_inpath(dir_path, includeSubfolder=True, path_type=0, ext_names='*'):
'''
获得指定目录下的所有文件,
:param dir_path: 指定的目录路径
:param includeSubfolder: 是否包含子文件夹里的文件,默认 True
:param path_type: 返回的文件路径形式
0 绝对路径,默认值
1 相对路径
2 文件名
:param ext_names: '*' | string | list
可以指定文件扩展名类型,支持以列表形式指定多个扩展名。默认为 '*',即所有扩展名。
举例:'.txt' 或 ['.jpg','.png']
:return: 以 yield 方式返回结果
'''
if type(ext_names) is str:
if ext_names != '*':
ext_names = [ext_names]
if type(ext_names) is list:
for i in range(len(ext_names)):
ext_names[i] = ext_names[i].lower()
def keep_file_byextname(file_name):
if type(ext_names) is list:
if file_name[0] == '.':
file_ext = file_name
else:
file_ext = os.path.splitext(file_name)[1]
#
if file_ext.lower() not in ext_names:
return False
else:
return True
return True
if includeSubfolder:
len_of_inpath = len(dir_path)
for root, dirs, files in os.walk(dir_path):
for file_name in files:
if not keep_file_byextname(file_name):
continue
if path_type == 0:
yield os.path.join(root, file_name)
elif path_type == 1:
yield os.path.join(
root[len_of_inpath:].lstrip(os.path.sep), file_name)
else:
yield file_name
else:
for file_name in os.listdir(dir_path):
filepath = os.path.join(dir_path, file_name)
if os.path.isfile(filepath):
if not keep_file_byextname(file_name):
continue
if path_type == 0:
yield filepath
else:
yield file_name
def make_dir(dirpath):
'''
创建目录,支持多级目录,若目录已存在自动忽略
'''
dirpath = dirpath.strip().rstrip(os.path.sep)
if dirpath:
if not os.path.exists(dirpath):
os.makedirs(dirpath)
def get_encfile_list(opts):
will_compile_files = []
if opts.directory:
if not os.path.exists(opts.directory):
print('No such Directory, please check or use the Absolute Path')
sys.exit(1)
pyfiles = getfiles_inpath(dir_path=opts.directory,
includeSubfolder=True,
path_type=1,
ext_names='.py')
# ignore __init__.py file
pyfiles = [pyfile for pyfile in pyfiles if not pyfile.endswith('__init__.py')]
# filter maintain files
opts.excludeFiles = []
if opts.ignore:
for path_assign in opts.ignore.split(','):
if not path_assign[-1:] in ['/', '\']: # if last char is not a path sep, consider it's assign a file
opts.excludeFiles.append(path_assign)
else:
assign_dir = path_assign.strip('/').strip('\')
tmp_dir = os.path.join(opts.rootName, assign_dir)
files = getfiles_inpath(dir_path=tmp_dir,
includeSubfolder=True,
path_type=1)
for file in files:
fpath = os.path.join(assign_dir, file)
opts.excludeFiles.append(fpath)
tmp_files = list(set(pyfiles) - set(opts.excludeFiles))
will_compile_files = []
for file in tmp_files:
will_compile_files.append(os.path.join(opts.directory, file))
elif opts.file:
if opts.file.endswith('.py'):
will_compile_files.append(opts.file)
else:
print('Make sure you give the right name of py file')
else:
print('no -f or -d param')
sys.exit()
return will_compile_files
def clear_builds(opts):
if os.path.isdir('build'):
shutil.rmtree('build')
if os.path.isdir('tmp_build'):
shutil.rmtree('tmp_build')
if os.path.isdir(opts.output):
shutil.rmtree(opts.output)
def clear_tmps(opts):
if os.path.isdir('build') and opts.output != 'build':
shutil.rmtree('build')
if os.path.isdir('tmp_build') and opts.output != 'tmp_build':
shutil.rmtree('tmp_build')
def pyencrypt(files):
extentions = []
print(files)
for full_filename in files:
filename = full_filename[:-3].replace(os.path.sep, '.')
extention = Extension(filename, [full_filename])
extention.cython_c_in_temp = True
extentions.append(extention)
setup(
script_args=['build_ext'],
ext_modules=cythonize(extentions, quiet=False, language_level=3, nthreads=1, build_dir='tmp_build'),
)
def gen_project(opts, will_compile_files):
make_dir(opts.output)
for file in getfiles_inpath('build', True, 1, ['.so', '.pyd']):
src_path = os.path.join('build', file)
mid_path = os.path.sep.join(file.split(os.path.sep)[1:-1])
file_name_parts = os.path.basename(src_path).split('.')
file_name = '.'.join([file_name_parts[0]] file_name_parts[-1:])
dest_path = os.path.join(opts.output, mid_path, file_name)
make_dir(os.path.dirname(dest_path))
shutil.copy(src_path, dest_path)
# 非编译文件拷贝至结果路径
not_compile_files = get_not_compile_files(opts, will_compile_files)
for not_compile_file in not_compile_files:
dest_path = os.path.join(opts.output, not_compile_file)
filepath, filename = os.path.split(dest_path)
make_dir(filepath)
shutil.copyfile(not_compile_file, dest_path)
if opts.remove:
clear_tmps(opts)
print('py2so encrypt build complate.')
def get_not_compile_files(opts, will_compile_files):
'''
获取非编译文件
'''
files = getfiles_inpath(dir_path=opts.directory,
includeSubfolder=True,
path_type=1,
ext_names='*')
files = [os.path.join(opts.directory, file) for file in files if not file.endswith('.pyc')]
not_compile_files = list(set(files) - set(will_compile_files))
return not_compile_files
if __name__ == '__main__':
# 不支持 windows 系统编译
if platform.system() == 'Windows':
print('只支持linux,windows下可以使用pyinstaller打包exe')
sys.exit()
# 编译参数
parser = argparse.ArgumentParser(description='py2so options:')
# -d 和 -f 二选一
exptypegroup = parser.add_mutually_exclusive_group()
exptypegroup.add_argument('-f', '--file', help='python文件 (如果使用-f, 将编译单个python文件)', default='')
exptypegroup.add_argument('-d', '--directory', help='python项目路径 (如果使用-d参数, 将编译整个python项目)', default='')
parser.add_argument('-o', '--output', help='编译完成后整个项目输出的文件路径', default='itlubber_py2so')
parser.add_argument('-i', '--ignore', help='''标记你不想编译的文件或文件夹路径。注意: 文件夹需要以路径分隔符号(`/`或`\`,依据系统而定)结尾,并且需要和-d参数一起使用。例: -i main.py,mod/__init__.py,exclude_dir/''')
parser.add_argument('-r', '--remove', help='清除所有中间文件,只保留加密结果文件,默认True', action='store_true', default=True)
opts = parser.parse_args()
# 获取所有待编译 python 文件
will_compile_files = get_encfile_list(opts)
# 清空上一次运行生成的临时文件
clear_builds(opts)
# 编译 python 文件
pyencrypt(will_compile_files)
# 将编译好的工程输出到结果文件夹
gen_project(opts, will_compile_files)
itlubber_py2so
目录下方的 main.py
可以发现执行结果,理论上与编译前结果应该一致QAQ-
https://github.com/sixgad/py2so -
https://github.com/ArvinMei/py2so -
https://github.com/itlubber/itlubber_py/blob/main/itlubber/py2so.py -
https://github.com/itlubber/itlubber_py/blob/main/itlubber/py2so_signal.py