AutoCython-zhang 2.3.3__tar.gz → 2.3.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython/_version.py +1 -1
- autocython_zhang-2.3.5/AutoCython/compile.py +263 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython_zhang.egg-info/PKG-INFO +6 -3
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython_zhang.egg-info/requires.txt +1 -1
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython_zhang.egg-info/top_level.txt +1 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/PKG-INFO +6 -3
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/README.md +4 -1
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/pyproject.toml +1 -1
- autocython_zhang-2.3.3/AutoCython/compile.py +0 -180
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython/AutoCython.py +0 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython/__init__.py +0 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython/obfuscate.py +0 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython/run_tasks.py +0 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython/tools.py +0 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython_zhang.egg-info/SOURCES.txt +0 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython_zhang.egg-info/dependency_links.txt +0 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython_zhang.egg-info/entry_points.txt +0 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/LICENSE +0 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/MANIFEST.in +0 -0
- {autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/setup.cfg +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
from .obfuscate import obfuscate_source
|
|
9
|
+
|
|
10
|
+
_SUPPORTED_CYTHON_MAJOR = 3
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _strip_binary(path):
|
|
14
|
+
"""strip 符号表,失败静默"""
|
|
15
|
+
try:
|
|
16
|
+
if sys.platform.startswith('win'):
|
|
17
|
+
return
|
|
18
|
+
cmd = ['strip', '-x' if sys.platform == 'darwin' else '--strip-all', path]
|
|
19
|
+
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_cython_major_version():
|
|
25
|
+
"""返回当前运行环境的 Cython 主版本号。"""
|
|
26
|
+
try:
|
|
27
|
+
import Cython
|
|
28
|
+
except ImportError as exc: # pragma: no cover
|
|
29
|
+
raise RuntimeError('编译 Python 源码需要先安装 Cython。') from exc
|
|
30
|
+
|
|
31
|
+
version = getattr(Cython, '__version__', '0')
|
|
32
|
+
major = str(version).split('.', 1)[0]
|
|
33
|
+
try:
|
|
34
|
+
return int(major)
|
|
35
|
+
except ValueError as exc: # pragma: no cover
|
|
36
|
+
raise RuntimeError(f'无法解析 Cython 版本: {version}') from exc
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ensure_supported_cython():
|
|
40
|
+
"""阻断已知会破坏运行时注解的旧版 Cython。"""
|
|
41
|
+
major = _get_cython_major_version()
|
|
42
|
+
if major != _SUPPORTED_CYTHON_MAJOR:
|
|
43
|
+
raise RuntimeError(
|
|
44
|
+
'检测到不受支持的 Cython 版本。'
|
|
45
|
+
'AutoCython 要求 Cython>=3,<4;'
|
|
46
|
+
'因为 Cython<3 可能剥离编译产物中的运行时注解,'
|
|
47
|
+
'导致 Pydantic/FastAPI/dataclass 的字段发现失效。'
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_platform_extension() -> str:
|
|
52
|
+
"""返回当前平台的扩展名"""
|
|
53
|
+
if sys.platform.startswith('win'):
|
|
54
|
+
return '.pyd'
|
|
55
|
+
return '.so'
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _infer_compile_root(abs_file_path: str) -> str:
|
|
59
|
+
"""推导编译根目录。
|
|
60
|
+
|
|
61
|
+
对包内模块,返回最外层 package 目录的父目录,以便保留完整 qualified module name。
|
|
62
|
+
对普通脚本,返回源码所在目录,保持现有行为。
|
|
63
|
+
"""
|
|
64
|
+
source_dir = os.path.dirname(abs_file_path)
|
|
65
|
+
current_dir = source_dir
|
|
66
|
+
top_level_package_dir = None
|
|
67
|
+
|
|
68
|
+
while os.path.isfile(os.path.join(current_dir, '__init__.py')):
|
|
69
|
+
top_level_package_dir = current_dir
|
|
70
|
+
parent_dir = os.path.dirname(current_dir)
|
|
71
|
+
if parent_dir == current_dir:
|
|
72
|
+
break
|
|
73
|
+
current_dir = parent_dir
|
|
74
|
+
|
|
75
|
+
if top_level_package_dir is None:
|
|
76
|
+
return source_dir
|
|
77
|
+
|
|
78
|
+
return os.path.dirname(top_level_package_dir)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _resolve_module_layout(abs_file_path: str):
|
|
82
|
+
"""解析编译布局,保留 package 相对路径与 qualified module name。"""
|
|
83
|
+
compile_root = _infer_compile_root(abs_file_path)
|
|
84
|
+
rel_path = os.path.relpath(abs_file_path, compile_root)
|
|
85
|
+
rel_dir = os.path.dirname(rel_path)
|
|
86
|
+
file_name = os.path.basename(rel_path)
|
|
87
|
+
module_name, ext = os.path.splitext(file_name)
|
|
88
|
+
safe_module_name = module_name.replace('-', '_')
|
|
89
|
+
|
|
90
|
+
safe_rel_parts = [part for part in rel_dir.split(os.sep) if part and part != '.']
|
|
91
|
+
safe_rel_parts.append(safe_module_name + ext)
|
|
92
|
+
safe_rel_path = os.path.join(*safe_rel_parts) if safe_rel_parts else safe_module_name + ext
|
|
93
|
+
|
|
94
|
+
module_parts = [part for part in rel_dir.split(os.sep) if part and part != '.']
|
|
95
|
+
module_parts.append(safe_module_name)
|
|
96
|
+
qualified_module_name = '.'.join(module_parts)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
'compile_root': compile_root,
|
|
100
|
+
'rel_path': rel_path,
|
|
101
|
+
'safe_rel_path': safe_rel_path,
|
|
102
|
+
'safe_module_name': safe_module_name,
|
|
103
|
+
'qualified_module_name': qualified_module_name,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _copy_package_inits(abs_file_path: str, compile_root: str, temp_dir: str) -> None:
|
|
108
|
+
"""复制包链上的 __init__.py,确保 build_ext --inplace 输出落到正确包路径。"""
|
|
109
|
+
current_dir = os.path.dirname(abs_file_path)
|
|
110
|
+
|
|
111
|
+
while True:
|
|
112
|
+
init_path = os.path.join(current_dir, '__init__.py')
|
|
113
|
+
if not os.path.isfile(init_path):
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
rel_init_path = os.path.relpath(init_path, compile_root)
|
|
117
|
+
temp_init_path = os.path.join(temp_dir, rel_init_path)
|
|
118
|
+
os.makedirs(os.path.dirname(temp_init_path), exist_ok=True)
|
|
119
|
+
shutil.copy2(init_path, temp_init_path)
|
|
120
|
+
|
|
121
|
+
if current_dir == compile_root:
|
|
122
|
+
break
|
|
123
|
+
|
|
124
|
+
parent_dir = os.path.dirname(current_dir)
|
|
125
|
+
if parent_dir == current_dir:
|
|
126
|
+
break
|
|
127
|
+
current_dir = parent_dir
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def compile_to_binary(file_path: str, del_source=False, obfuscate=True, obfuscate_seed=None):
|
|
131
|
+
"""
|
|
132
|
+
将指定的 Python 文件(.py)通过 Cython 编译为二进制扩展文件
|
|
133
|
+
|
|
134
|
+
:param file_path: Python 文件路径(可以是相对路径或绝对路径)
|
|
135
|
+
:param del_source: 是否删除源代码
|
|
136
|
+
:param obfuscate: 是否在编译前混淆源码(默认True)
|
|
137
|
+
:param obfuscate_seed: 混淆随机种子(None 表示不固定)
|
|
138
|
+
:return: 生成的二进制文件路径(与输入保持相同的路径类型)
|
|
139
|
+
"""
|
|
140
|
+
_ensure_supported_cython()
|
|
141
|
+
|
|
142
|
+
is_absolute = os.path.isabs(file_path)
|
|
143
|
+
abs_file_path = os.path.abspath(file_path)
|
|
144
|
+
|
|
145
|
+
if not os.path.isfile(abs_file_path):
|
|
146
|
+
raise FileNotFoundError(f"FileNotFoundError: {file_path}.")
|
|
147
|
+
|
|
148
|
+
file_name = os.path.basename(abs_file_path)
|
|
149
|
+
_, ext = os.path.splitext(file_name)
|
|
150
|
+
source_dir = os.path.dirname(abs_file_path)
|
|
151
|
+
|
|
152
|
+
if ext != '.py':
|
|
153
|
+
raise ValueError(f"ValueError: The file {file_path} is not a valid Python file (.py)!")
|
|
154
|
+
|
|
155
|
+
module_layout = _resolve_module_layout(abs_file_path)
|
|
156
|
+
safe_module_name = module_layout['safe_module_name']
|
|
157
|
+
target_ext = get_platform_extension()
|
|
158
|
+
temp_dir = tempfile.mkdtemp()
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
temp_file_path = os.path.join(temp_dir, module_layout['safe_rel_path'])
|
|
162
|
+
os.makedirs(os.path.dirname(temp_file_path), exist_ok=True)
|
|
163
|
+
_copy_package_inits(abs_file_path, module_layout['compile_root'], temp_dir)
|
|
164
|
+
shutil.copy2(abs_file_path, temp_file_path)
|
|
165
|
+
|
|
166
|
+
if obfuscate:
|
|
167
|
+
try:
|
|
168
|
+
with open(temp_file_path, 'r', encoding='utf-8') as f:
|
|
169
|
+
original_source = f.read()
|
|
170
|
+
obfuscated = obfuscate_source(original_source, seed=obfuscate_seed)
|
|
171
|
+
with open(temp_file_path, 'w', encoding='utf-8') as f:
|
|
172
|
+
f.write(obfuscated)
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
raise RuntimeError(f"Obfuscation failed for {file_path}: {exc}") from exc
|
|
175
|
+
|
|
176
|
+
setup_code = f"""
|
|
177
|
+
from setuptools import setup
|
|
178
|
+
from setuptools import Extension
|
|
179
|
+
from Cython.Build import cythonize
|
|
180
|
+
|
|
181
|
+
compiler_directives = {{
|
|
182
|
+
'language_level': '3',
|
|
183
|
+
'annotation_typing': False,
|
|
184
|
+
'always_allow_keywords': True,
|
|
185
|
+
'binding': True,
|
|
186
|
+
'embedsignature': False,
|
|
187
|
+
'wraparound': False,
|
|
188
|
+
}}
|
|
189
|
+
|
|
190
|
+
setup(
|
|
191
|
+
ext_modules=cythonize(
|
|
192
|
+
[
|
|
193
|
+
Extension(
|
|
194
|
+
{repr(module_layout['qualified_module_name'])},
|
|
195
|
+
[{repr(module_layout['safe_rel_path'])}],
|
|
196
|
+
)
|
|
197
|
+
],
|
|
198
|
+
compiler_directives=compiler_directives,
|
|
199
|
+
force=True
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
"""
|
|
203
|
+
setup_path = os.path.join(temp_dir, 'setup.py')
|
|
204
|
+
with open(setup_path, 'w', encoding='utf-8') as f:
|
|
205
|
+
f.write(setup_code)
|
|
206
|
+
|
|
207
|
+
command = [sys.executable, 'setup.py', 'build_ext', '--inplace']
|
|
208
|
+
try:
|
|
209
|
+
result = subprocess.run(
|
|
210
|
+
command,
|
|
211
|
+
cwd=temp_dir,
|
|
212
|
+
stdout=subprocess.PIPE,
|
|
213
|
+
stderr=subprocess.PIPE,
|
|
214
|
+
timeout=300,
|
|
215
|
+
)
|
|
216
|
+
except subprocess.TimeoutExpired:
|
|
217
|
+
raise RuntimeError(f"Compilation timed out after 300s: {file_path}")
|
|
218
|
+
|
|
219
|
+
if result.returncode != 0:
|
|
220
|
+
error_msg = result.stderr.decode('utf-8', errors='replace')
|
|
221
|
+
raise RuntimeError(f"Compilation failed: {error_msg}")
|
|
222
|
+
|
|
223
|
+
temp_output_dir = os.path.dirname(temp_file_path) or temp_dir
|
|
224
|
+
pattern = os.path.join(temp_output_dir, f"{safe_module_name}*{target_ext}")
|
|
225
|
+
matches = glob.glob(pattern)
|
|
226
|
+
if not matches:
|
|
227
|
+
pattern = os.path.join(temp_output_dir, f"*{safe_module_name}*{target_ext}")
|
|
228
|
+
matches = glob.glob(pattern)
|
|
229
|
+
|
|
230
|
+
if not matches:
|
|
231
|
+
raise FileNotFoundError(
|
|
232
|
+
f"FileNotFoundError: The file {file_path} is not a valid Python file (.py)! "
|
|
233
|
+
f"Generated file {target_ext} not found, in {temp_dir} possible file: {os.listdir(temp_dir)}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
generated_file = max(matches, key=os.path.getmtime)
|
|
237
|
+
output_dir = source_dir if is_absolute else (os.path.dirname(file_path) or '.')
|
|
238
|
+
if output_dir and not os.path.exists(output_dir):
|
|
239
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
240
|
+
|
|
241
|
+
output_file_name = os.path.basename(generated_file)
|
|
242
|
+
output_path = os.path.join(output_dir, output_file_name)
|
|
243
|
+
if os.path.exists(output_path):
|
|
244
|
+
os.remove(output_path)
|
|
245
|
+
shutil.move(generated_file, output_path)
|
|
246
|
+
|
|
247
|
+
_strip_binary(output_path)
|
|
248
|
+
|
|
249
|
+
if del_source:
|
|
250
|
+
os.remove(file_path)
|
|
251
|
+
|
|
252
|
+
return output_path
|
|
253
|
+
finally:
|
|
254
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
if __name__ == '__main__': # pragma: no cover
|
|
258
|
+
target_file = 'test/example.py'
|
|
259
|
+
try:
|
|
260
|
+
output_file = compile_to_binary(target_file)
|
|
261
|
+
print(output_file)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
print(e)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoCython-zhang
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.5
|
|
4
4
|
Summary: 自动Cython,使用Cython批量编译.py文件为.pyd文件!
|
|
5
5
|
Author-email: zhang_gavin <qq814608@163.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -14,7 +14,7 @@ Requires-Python: >=3.9
|
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: setuptools
|
|
17
|
-
Requires-Dist: cython
|
|
17
|
+
Requires-Dist: cython<4,>=3
|
|
18
18
|
Requires-Dist: rich
|
|
19
19
|
Dynamic: license-file
|
|
20
20
|
|
|
@@ -37,6 +37,7 @@ AutoCython 是一个 Python 源码保护工具,通过 Cython 编译 + AST 混
|
|
|
37
37
|
- **并发编译** — 基于 `ThreadPoolExecutor` 的多线程并行编译,可配置并发数
|
|
38
38
|
- **实时进度面板** — 基于 Rich 的实时任务状态表格、进度条、耗时统计
|
|
39
39
|
- **跨平台** — 支持 Linux、macOS、Windows,自动适配 `.so`/`.pyd` 扩展名
|
|
40
|
+
- **包路径保真** — 编译包内模块时保留 qualified module name,兼容 Pydantic / FastAPI 等依赖运行时模块命名空间的框架
|
|
40
41
|
- **可复现构建** — 通过 `--seed` 参数固定混淆随机种子
|
|
41
42
|
- **智能排除** — 自动跳过 `__init__.py`、虚拟环境、构建目录;支持 `# AutoCython No Compile` 标记豁免
|
|
42
43
|
- **中英双语** — CLI 帮助信息和进度面板自动适配系统语言
|
|
@@ -47,11 +48,13 @@ AutoCython 是一个 Python 源码保护工具,通过 Cython 编译 + AST 混
|
|
|
47
48
|
pip install AutoCython-zhang
|
|
48
49
|
```
|
|
49
50
|
|
|
51
|
+
> 当前编译链要求 `Cython>=3,<4`。该约束用于避免旧版 Cython 在编译后破坏 `Pydantic` / `FastAPI` / `dataclass` 等依赖运行时注解的框架行为。
|
|
52
|
+
|
|
50
53
|
### 依赖
|
|
51
54
|
|
|
52
55
|
| 包 | 用途 |
|
|
53
56
|
|---|------|
|
|
54
|
-
| `cython` | Python → C
|
|
57
|
+
| `cython>=3,<4` | Python → C 编译核心;确保运行时注解不被旧版编译链破坏 |
|
|
55
58
|
| `setuptools` | 构建扩展模块 |
|
|
56
59
|
| `rich` | 终端实时进度面板 |
|
|
57
60
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoCython-zhang
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.5
|
|
4
4
|
Summary: 自动Cython,使用Cython批量编译.py文件为.pyd文件!
|
|
5
5
|
Author-email: zhang_gavin <qq814608@163.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -14,7 +14,7 @@ Requires-Python: >=3.9
|
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: setuptools
|
|
17
|
-
Requires-Dist: cython
|
|
17
|
+
Requires-Dist: cython<4,>=3
|
|
18
18
|
Requires-Dist: rich
|
|
19
19
|
Dynamic: license-file
|
|
20
20
|
|
|
@@ -37,6 +37,7 @@ AutoCython 是一个 Python 源码保护工具,通过 Cython 编译 + AST 混
|
|
|
37
37
|
- **并发编译** — 基于 `ThreadPoolExecutor` 的多线程并行编译,可配置并发数
|
|
38
38
|
- **实时进度面板** — 基于 Rich 的实时任务状态表格、进度条、耗时统计
|
|
39
39
|
- **跨平台** — 支持 Linux、macOS、Windows,自动适配 `.so`/`.pyd` 扩展名
|
|
40
|
+
- **包路径保真** — 编译包内模块时保留 qualified module name,兼容 Pydantic / FastAPI 等依赖运行时模块命名空间的框架
|
|
40
41
|
- **可复现构建** — 通过 `--seed` 参数固定混淆随机种子
|
|
41
42
|
- **智能排除** — 自动跳过 `__init__.py`、虚拟环境、构建目录;支持 `# AutoCython No Compile` 标记豁免
|
|
42
43
|
- **中英双语** — CLI 帮助信息和进度面板自动适配系统语言
|
|
@@ -47,11 +48,13 @@ AutoCython 是一个 Python 源码保护工具,通过 Cython 编译 + AST 混
|
|
|
47
48
|
pip install AutoCython-zhang
|
|
48
49
|
```
|
|
49
50
|
|
|
51
|
+
> 当前编译链要求 `Cython>=3,<4`。该约束用于避免旧版 Cython 在编译后破坏 `Pydantic` / `FastAPI` / `dataclass` 等依赖运行时注解的框架行为。
|
|
52
|
+
|
|
50
53
|
### 依赖
|
|
51
54
|
|
|
52
55
|
| 包 | 用途 |
|
|
53
56
|
|---|------|
|
|
54
|
-
| `cython` | Python → C
|
|
57
|
+
| `cython>=3,<4` | Python → C 编译核心;确保运行时注解不被旧版编译链破坏 |
|
|
55
58
|
| `setuptools` | 构建扩展模块 |
|
|
56
59
|
| `rich` | 终端实时进度面板 |
|
|
57
60
|
|
|
@@ -17,6 +17,7 @@ AutoCython 是一个 Python 源码保护工具,通过 Cython 编译 + AST 混
|
|
|
17
17
|
- **并发编译** — 基于 `ThreadPoolExecutor` 的多线程并行编译,可配置并发数
|
|
18
18
|
- **实时进度面板** — 基于 Rich 的实时任务状态表格、进度条、耗时统计
|
|
19
19
|
- **跨平台** — 支持 Linux、macOS、Windows,自动适配 `.so`/`.pyd` 扩展名
|
|
20
|
+
- **包路径保真** — 编译包内模块时保留 qualified module name,兼容 Pydantic / FastAPI 等依赖运行时模块命名空间的框架
|
|
20
21
|
- **可复现构建** — 通过 `--seed` 参数固定混淆随机种子
|
|
21
22
|
- **智能排除** — 自动跳过 `__init__.py`、虚拟环境、构建目录;支持 `# AutoCython No Compile` 标记豁免
|
|
22
23
|
- **中英双语** — CLI 帮助信息和进度面板自动适配系统语言
|
|
@@ -27,11 +28,13 @@ AutoCython 是一个 Python 源码保护工具,通过 Cython 编译 + AST 混
|
|
|
27
28
|
pip install AutoCython-zhang
|
|
28
29
|
```
|
|
29
30
|
|
|
31
|
+
> 当前编译链要求 `Cython>=3,<4`。该约束用于避免旧版 Cython 在编译后破坏 `Pydantic` / `FastAPI` / `dataclass` 等依赖运行时注解的框架行为。
|
|
32
|
+
|
|
30
33
|
### 依赖
|
|
31
34
|
|
|
32
35
|
| 包 | 用途 |
|
|
33
36
|
|---|------|
|
|
34
|
-
| `cython` | Python → C
|
|
37
|
+
| `cython>=3,<4` | Python → C 编译核心;确保运行时注解不被旧版编译链破坏 |
|
|
35
38
|
| `setuptools` | 构建扩展模块 |
|
|
36
39
|
| `rich` | 终端实时进度面板 |
|
|
37
40
|
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
import glob
|
|
4
|
-
import shutil
|
|
5
|
-
import platform
|
|
6
|
-
import tempfile
|
|
7
|
-
import subprocess
|
|
8
|
-
|
|
9
|
-
from .obfuscate import obfuscate_source
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _strip_binary(path):
|
|
13
|
-
"""strip 符号表,失败静默"""
|
|
14
|
-
try:
|
|
15
|
-
if sys.platform.startswith('win'):
|
|
16
|
-
return
|
|
17
|
-
cmd = ['strip', '-x' if sys.platform == 'darwin' else '--strip-all', path]
|
|
18
|
-
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
19
|
-
except Exception:
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
def get_platform_extension() -> str:
|
|
23
|
-
"""返回当前平台的扩展名"""
|
|
24
|
-
if sys.platform.startswith('win'):
|
|
25
|
-
return '.pyd'
|
|
26
|
-
return '.so'
|
|
27
|
-
|
|
28
|
-
def compile_to_binary(file_path: str, del_source=False, obfuscate=True, obfuscate_seed=None):
|
|
29
|
-
"""
|
|
30
|
-
将指定的 Python 文件(.py)通过 Cython 编译为二进制扩展文件
|
|
31
|
-
|
|
32
|
-
:param file_path: Python 文件路径(可以是相对路径或绝对路径)
|
|
33
|
-
:param del_source: 是否删除源代码
|
|
34
|
-
:param obfuscate: 是否在编译前混淆源码(默认True)
|
|
35
|
-
:param obfuscate_seed: 混淆随机种子(None 表示不固定)
|
|
36
|
-
:return: 生成的二进制文件路径(与输入保持相同的路径类型)
|
|
37
|
-
"""
|
|
38
|
-
# 保存原始路径类型(相对/绝对)
|
|
39
|
-
is_absolute = os.path.isabs(file_path)
|
|
40
|
-
|
|
41
|
-
# 获取绝对路径用于内部操作
|
|
42
|
-
abs_file_path = os.path.abspath(file_path)
|
|
43
|
-
|
|
44
|
-
if not os.path.isfile(abs_file_path):
|
|
45
|
-
raise FileNotFoundError(f"FileNotFoundError: {file_path}.")
|
|
46
|
-
|
|
47
|
-
# 获取文件名和目录
|
|
48
|
-
file_name = os.path.basename(abs_file_path)
|
|
49
|
-
module_name, ext = os.path.splitext(file_name)
|
|
50
|
-
source_dir = os.path.dirname(abs_file_path) # 源文件所在目录(绝对路径)
|
|
51
|
-
|
|
52
|
-
if ext != ".py":
|
|
53
|
-
raise ValueError(f"ValueError: The file {file_path} is not a valid Python file (.py)!")
|
|
54
|
-
|
|
55
|
-
# 连字符文件名转下划线(Cython 模块名不允许连字符)
|
|
56
|
-
safe_module_name = module_name.replace('-', '_')
|
|
57
|
-
safe_file_name = safe_module_name + ext
|
|
58
|
-
|
|
59
|
-
# 获取平台特定扩展名
|
|
60
|
-
target_ext = get_platform_extension()
|
|
61
|
-
|
|
62
|
-
# 创建临时工作目录
|
|
63
|
-
temp_dir = tempfile.mkdtemp()
|
|
64
|
-
|
|
65
|
-
try:
|
|
66
|
-
# 将目标文件复制到临时目录(使用安全文件名)
|
|
67
|
-
temp_file_path = os.path.join(temp_dir, safe_file_name)
|
|
68
|
-
shutil.copy2(abs_file_path, temp_file_path)
|
|
69
|
-
|
|
70
|
-
# 混淆源码(失败则显式抛错,避免静默降级)
|
|
71
|
-
if obfuscate:
|
|
72
|
-
try:
|
|
73
|
-
with open(temp_file_path, 'r', encoding='utf-8') as f:
|
|
74
|
-
original_source = f.read()
|
|
75
|
-
obfuscated = obfuscate_source(original_source, seed=obfuscate_seed)
|
|
76
|
-
with open(temp_file_path, 'w', encoding='utf-8') as f:
|
|
77
|
-
f.write(obfuscated)
|
|
78
|
-
except Exception as exc:
|
|
79
|
-
raise RuntimeError(f"Obfuscation failed for {file_path}: {exc}") from exc
|
|
80
|
-
|
|
81
|
-
# 创建临时的 setup.py 文件
|
|
82
|
-
setup_code = f"""
|
|
83
|
-
from setuptools import setup
|
|
84
|
-
from Cython.Build import cythonize
|
|
85
|
-
|
|
86
|
-
# 编译器指令
|
|
87
|
-
compiler_directives = {{
|
|
88
|
-
'language_level': '3',
|
|
89
|
-
'annotation_typing': False,
|
|
90
|
-
'always_allow_keywords': True,
|
|
91
|
-
'binding': True,
|
|
92
|
-
'embedsignature': False,
|
|
93
|
-
'wraparound': False,
|
|
94
|
-
}}
|
|
95
|
-
|
|
96
|
-
setup(
|
|
97
|
-
ext_modules=cythonize(
|
|
98
|
-
{repr(safe_file_name)},
|
|
99
|
-
compiler_directives=compiler_directives,
|
|
100
|
-
force=True
|
|
101
|
-
)
|
|
102
|
-
)
|
|
103
|
-
"""
|
|
104
|
-
setup_path = os.path.join(temp_dir, "setup.py")
|
|
105
|
-
with open(setup_path, "w", encoding='utf-8') as f:
|
|
106
|
-
f.write(setup_code)
|
|
107
|
-
|
|
108
|
-
# 执行编译命令
|
|
109
|
-
command = [sys.executable, "setup.py", "build_ext", "--inplace"]
|
|
110
|
-
try:
|
|
111
|
-
result = subprocess.run(
|
|
112
|
-
command,
|
|
113
|
-
cwd=temp_dir,
|
|
114
|
-
stdout=subprocess.PIPE,
|
|
115
|
-
stderr=subprocess.PIPE,
|
|
116
|
-
timeout=300
|
|
117
|
-
)
|
|
118
|
-
except subprocess.TimeoutExpired:
|
|
119
|
-
raise RuntimeError(f"Compilation timed out after 300s: {file_path}")
|
|
120
|
-
|
|
121
|
-
if result.returncode != 0:
|
|
122
|
-
error_msg = result.stderr.decode('utf-8', errors='replace')
|
|
123
|
-
raise RuntimeError(f"Compilation failed: {error_msg}")
|
|
124
|
-
|
|
125
|
-
# 查找生成的二进制文件
|
|
126
|
-
pattern = os.path.join(temp_dir, f"{safe_module_name}*{target_ext}")
|
|
127
|
-
matches = glob.glob(pattern)
|
|
128
|
-
|
|
129
|
-
if not matches:
|
|
130
|
-
pattern = os.path.join(temp_dir, f"*{safe_module_name}*{target_ext}")
|
|
131
|
-
matches = glob.glob(pattern)
|
|
132
|
-
|
|
133
|
-
if not matches:
|
|
134
|
-
raise FileNotFoundError(f"FileNotFoundError: The file {file_path} is not a valid Python file (.py)! Generated file {target_ext} not found, in {temp_dir} possible file: {os.listdir(temp_dir)}")
|
|
135
|
-
|
|
136
|
-
# 取最新生成的文件
|
|
137
|
-
generated_file = max(matches, key=os.path.getmtime)
|
|
138
|
-
|
|
139
|
-
# 获取源文件的目录(使用原始路径类型)
|
|
140
|
-
if is_absolute:
|
|
141
|
-
output_dir = source_dir
|
|
142
|
-
else:
|
|
143
|
-
# 如果输入是相对路径,保持相对路径
|
|
144
|
-
output_dir = os.path.dirname(file_path) or '.'
|
|
145
|
-
|
|
146
|
-
# 创建目标目录(如果不存在)
|
|
147
|
-
if output_dir and not os.path.exists(output_dir):
|
|
148
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
149
|
-
|
|
150
|
-
# 目标文件路径(保持原始路径类型)
|
|
151
|
-
output_file_name = os.path.basename(generated_file)
|
|
152
|
-
output_path = os.path.join(output_dir, output_file_name)
|
|
153
|
-
|
|
154
|
-
# 移动文件(先删除已存在的目标)
|
|
155
|
-
if os.path.exists(output_path):
|
|
156
|
-
os.remove(output_path)
|
|
157
|
-
shutil.move(generated_file, output_path)
|
|
158
|
-
|
|
159
|
-
# strip 符号表
|
|
160
|
-
_strip_binary(output_path)
|
|
161
|
-
|
|
162
|
-
if del_source:
|
|
163
|
-
os.remove(file_path)
|
|
164
|
-
|
|
165
|
-
# 返回与输入相同类型的路径
|
|
166
|
-
return output_path
|
|
167
|
-
finally:
|
|
168
|
-
# 清理临时目录
|
|
169
|
-
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
170
|
-
|
|
171
|
-
# 测试函数
|
|
172
|
-
if __name__ == "__main__": # pragma: no cover
|
|
173
|
-
# 替换为你的 Python 文件路径
|
|
174
|
-
target_file = "test/example.py" # 请确保文件路径正确
|
|
175
|
-
|
|
176
|
-
try:
|
|
177
|
-
output_file = compile_to_binary(target_file)
|
|
178
|
-
print(output_file)
|
|
179
|
-
except Exception as e:
|
|
180
|
-
print(e)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython_zhang.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{autocython_zhang-2.3.3 → autocython_zhang-2.3.5}/AutoCython_zhang.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|