Python2编译So

首先确认项目环境

  • Windows:编译后为Pyd文件
  • Linux:编译后为So文件

架构和操作系统兼容

  • .so 文件是二进制文件,它们是针对特定的操作系统和 CPU 架构编译的。
  • 例如,在 Linux x86-64 上编译的 .so 文件不能在 Windows 或 macOS 上使用,也不能在 ARM 架构的设备上使用。
  • 即使是相同的操作系统,如果 Python 解释器的位数(32 位或 64 位)不同,也可能导致不兼容。

以Linux的Ubuntu为例

  • 安装包和依赖文件
1
2
3
4
5
6
7
8
# 首先安装Cython,python2安装3.0以下的版本,3.0以上不支持部分python2
pip install cython<3.0

# 安装python-dev, python3 就装:python3-dev
apt-get install python-dev

# 验证安装
Cython -V

编写编译脚本

  • 新建:setup.py,将以下代码Copy进项目根目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -*- coding: utf-8 -*-

import sys
from distutils.core import setup


try:
from Cython.Build import cythonize
except:
print("你没有安装Cython,请安装 pip install Cython")
print("本项目需要 Visual Studio 2022 的C++开发支持,请确认安装了相应组件")

arg_list = sys.argv
f_name = arg_list[1]
sys.argv.pop(1)

setup(
name="mymodule",
ext_modules=cythonize(f_name, compiler_directives={'language_level': 2,})
)
  • 新建:setup_main.py,将以下代码Copy进项目根目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# -*- coding: utf-8 -*-
import os

# 项目根目录下不用(能)转译的py文件(夹)名,用于启动的入口脚本文件一定要加进来
ignore_files = ['build', 'dist', 'data', 'package', 'venv', '__pycache__', '.git', 'setup.py', 'setup_main.py', '__init__.py']
# 项目子目录下不用(能)转译的'py文件(夹)名
ignore_names = ['__init__.py']
# 不需要原样复制到编译文件夹的文件或者文件夹
ignore_move = ['venv', '__pycache__', 'server.log', 'setup.py', 'setup_main.py']
# 需要编译的文件夹绝对路径
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 将以上不需要转译的文件(夹)加上绝对路径
ignore_files = [os.path.join(BASE_DIR, x) for x in ignore_files]
# 是否将编译打包到指定文件夹内 (True),还是和源文件在同一目录下(False),默认True
package = True
# 打包文件夹名 (package = True 时有效)
package_name = "dist"
# 打包文件夹路径 (package = True 时有效)
package_path = os.path.join(BASE_DIR, package_name)
# 若没有打包文件夹,则生成一个
if not os.path.exists(package_path):
os.mkdir(package_path)
translate_pys = []


# 编译需要的py文件
def translate_dir(path):
pathes = os.listdir(path)
# if path != BASE_DIR and path != '__init__.py' in pathes:
# with open(os.path.join(path, '__init__.py'), 'w', encoding='utf8') as f:
# pass
for p in pathes:
if p in ignore_names:
continue
if p.startswith('__') or p.startswith('.') or p.startswith('build'):
continue
f_path = os.path.join(path, p)
if f_path in ignore_files:
continue
if os.path.isdir(f_path):
translate_dir(f_path)
else:
if not f_path.endswith('.py') and not f_path.endswith('.pyx'):
continue
if f_path.endswith('__init__.py') or f_path.endswith('__init__.pyx'):
continue
with open(f_path, 'r') as f:
content = f.read()
if not content.startswith('# cython: language_level=3'):
content = '# cython: language_level=3\n' + content
with open(f_path, 'w') as f1:
f1.write(content)
os.system('python setup.py ' + f_path + ' build_ext --inplace')
translate_pys.append(f_path)
f_name = '.'.join(f_path.split('.')[:-1])
py_file = '.'.join([f_name, 'py'])
c_file = '.'.join([f_name, 'c'])
# print(f"f_path: {f_path}, c_file: {c_file}, py_file: {py_file}")
if os.path.exists(c_file):
os.remove(c_file)


# 移除编译临时文件
def remove_dir(path, rm_path=True):
if not os.path.exists(path):
return
pathes = os.listdir(path)
for p in pathes:
f_path = os.path.join(path, p)
if os.path.isdir(f_path):
remove_dir(f_path, False)
os.rmdir(f_path)
else:
os.remove(f_path)
if rm_path:
os.rmdir(path)


# 移动编译后的文件至指定目录
def mv_to_packages(path=BASE_DIR):
pathes = os.listdir(path)
for p in pathes:
if p.startswith('.'):
continue
if p in ignore_move:
continue
f_path = os.path.join(path, p)
if f_path == package_path:
continue
p_f_path = f_path.replace(BASE_DIR, package_path)
if os.path.isdir(f_path):
if not os.path.exists(p_f_path):
os.mkdir(p_f_path)
mv_to_packages(f_path)
else:
if not f_path.endswith('.py') or f_path not in translate_pys:
with open(f_path, 'rb') as f:
content = f.read()
with open(p_f_path, 'wb') as f:
f.write(content)
if f_path.endswith('.pyd') or f_path.endswith('.so'):
os.remove(f_path)


# 将编译后的文件重命名成:源文件名+.pyd,否则编译后的文件名会类似:myUtils.cp39-win_amd64.pyd
def batch_rename(src_path):
filenames = os.listdir(src_path)
same_name = []
count = 0
for filename in filenames:
old_name = os.path.join(src_path, filename)
if old_name == package_path:
continue
if os.path.isdir(old_name):
batch_rename(old_name)
if filename[-4:] == ".pyd" or filename[-3:] == ".so":
old_pyd = filename.split(".")
new_pyd = str(old_pyd[0]) + "." + str(old_pyd[2])
else:
continue
change_name = new_pyd
count += 1
new_name = os.path.join(src_path, change_name)
if change_name in filenames:
same_name.append(change_name)
continue
os.rename(old_name, new_name)


def run():
translate_dir(BASE_DIR)
remove_dir(os.path.join(BASE_DIR, 'build'))
if package:
mv_to_packages()
batch_rename(os.path.join(BASE_DIR, package_name))


if __name__ == '__main__':
run()

运行示例

  • 单个文件编译:python2 setup.py xxx.py build_ext –inplace
  • 所有文件编译:python2 setup_main.py build_ext –inplace

一些坑

  • Python2 默认使用ASCII编码,需要在每个项目文件都添加头文件指定编码
1
# -*- coding: utf-8 -*-
  • 如果代码中有流式输入,类似 sys.stdin 的语法,也要指定编码
  • 不然会报错:SyntaxError: Non-ASCII character ‘\xe7’ in file
1
2
reload(sys)
sys.setdefaultencoding('utf-8')
  • 不能用python3-dev打包python2的项目,会存在依赖问题
  • python2需要安装3.0以下版本的Cython

Python2编译So
https://blogs.nover.fun/post/2025/04/Python2编译So/
作者
Nover
发布于
2025年4月9日
许可协议