AutoCython-zhang 2.2.1__tar.gz → 2.3.0__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.0/AutoCython/AutoCython.py +44 -0
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython/_version.py +1 -1
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython/compile.py +57 -17
- autocython_zhang-2.3.0/AutoCython/obfuscate.py +469 -0
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython/run_tasks.py +19 -21
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython/tools.py +16 -9
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/PKG-INFO +38 -26
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/SOURCES.txt +2 -0
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/top_level.txt +1 -0
- autocython_zhang-2.3.0/MANIFEST.in +2 -0
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/PKG-INFO +38 -26
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/README.md +36 -3
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/pyproject.toml +17 -2
- autocython_zhang-2.2.1/AutoCython/AutoCython.py +0 -36
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython/__init__.py +0 -0
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/dependency_links.txt +0 -0
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/entry_points.txt +0 -0
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/requires.txt +0 -0
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/LICENSE +0 -0
- {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/setup.cfg +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from .run_tasks import run_tasks
|
|
4
|
+
from .compile import compile_to_binary
|
|
5
|
+
from .tools import parse_arguments, find_python_files
|
|
6
|
+
from .tools import show_no_compilable_files, show_file_not_found, show_path_not_found
|
|
7
|
+
|
|
8
|
+
def compile():
|
|
9
|
+
try:
|
|
10
|
+
args = parse_arguments()
|
|
11
|
+
obfuscate_seed = args.seed
|
|
12
|
+
|
|
13
|
+
if args.file:
|
|
14
|
+
if os.path.isfile(args.file):
|
|
15
|
+
compile_file = args.file
|
|
16
|
+
del_source = args.del_source
|
|
17
|
+
tasks = [
|
|
18
|
+
# 函数, 位置参数, 关键字参数
|
|
19
|
+
(compile_to_binary, compile_file, (compile_file, del_source, True, obfuscate_seed), {}),
|
|
20
|
+
]
|
|
21
|
+
run_tasks(tasks, max_workers=1, raise_on_failure=True) # 执行编译
|
|
22
|
+
else:
|
|
23
|
+
show_file_not_found(args.file)
|
|
24
|
+
elif args.path:
|
|
25
|
+
if os.path.exists(args.path) and not os.path.isfile(args.path):
|
|
26
|
+
compile_file_list = find_python_files(args.path)
|
|
27
|
+
if compile_file_list:
|
|
28
|
+
del_source = args.del_source
|
|
29
|
+
tasks = []
|
|
30
|
+
for compile_file in compile_file_list:
|
|
31
|
+
tasks.append(
|
|
32
|
+
# 函数, 位置参数, 关键字参数
|
|
33
|
+
(compile_to_binary, compile_file, (compile_file, del_source, True, obfuscate_seed), {}),
|
|
34
|
+
)
|
|
35
|
+
run_tasks(tasks, max_workers=args.conc, raise_on_failure=True) # 执行编译
|
|
36
|
+
else:
|
|
37
|
+
show_no_compilable_files(args.path)
|
|
38
|
+
else:
|
|
39
|
+
show_path_not_found(args.path)
|
|
40
|
+
except KeyboardInterrupt:
|
|
41
|
+
sys.exit(130)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
44
|
+
sys.exit(1)
|
|
@@ -2,21 +2,37 @@ import os
|
|
|
2
2
|
import sys
|
|
3
3
|
import glob
|
|
4
4
|
import shutil
|
|
5
|
+
import platform
|
|
5
6
|
import tempfile
|
|
6
7
|
import subprocess
|
|
7
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
|
+
|
|
8
22
|
def get_platform_extension() -> str:
|
|
9
23
|
"""返回当前平台的扩展名"""
|
|
10
24
|
if sys.platform.startswith('win'):
|
|
11
25
|
return '.pyd'
|
|
12
26
|
return '.so'
|
|
13
27
|
|
|
14
|
-
def compile_to_binary(file_path: str, del_source=False):
|
|
28
|
+
def compile_to_binary(file_path: str, del_source=False, obfuscate=True, obfuscate_seed=None):
|
|
15
29
|
"""
|
|
16
30
|
将指定的 Python 文件(.py)通过 Cython 编译为二进制扩展文件
|
|
17
31
|
|
|
18
32
|
:param file_path: Python 文件路径(可以是相对路径或绝对路径)
|
|
19
33
|
:param del_source: 是否删除源代码
|
|
34
|
+
:param obfuscate: 是否在编译前混淆源码(默认True)
|
|
35
|
+
:param obfuscate_seed: 混淆随机种子(None 表示不固定)
|
|
20
36
|
:return: 生成的二进制文件路径(与输入保持相同的路径类型)
|
|
21
37
|
"""
|
|
22
38
|
# 保存原始路径类型(相对/绝对)
|
|
@@ -36,6 +52,10 @@ def compile_to_binary(file_path: str, del_source=False):
|
|
|
36
52
|
if ext != ".py":
|
|
37
53
|
raise ValueError(f"ValueError: The file {file_path} is not a valid Python file (.py)!")
|
|
38
54
|
|
|
55
|
+
# 连字符文件名转下划线(Cython 模块名不允许连字符)
|
|
56
|
+
safe_module_name = module_name.replace('-', '_')
|
|
57
|
+
safe_file_name = safe_module_name + ext
|
|
58
|
+
|
|
39
59
|
# 获取平台特定扩展名
|
|
40
60
|
target_ext = get_platform_extension()
|
|
41
61
|
|
|
@@ -43,10 +63,21 @@ def compile_to_binary(file_path: str, del_source=False):
|
|
|
43
63
|
temp_dir = tempfile.mkdtemp()
|
|
44
64
|
|
|
45
65
|
try:
|
|
46
|
-
#
|
|
47
|
-
temp_file_path = os.path.join(temp_dir,
|
|
66
|
+
# 将目标文件复制到临时目录(使用安全文件名)
|
|
67
|
+
temp_file_path = os.path.join(temp_dir, safe_file_name)
|
|
48
68
|
shutil.copy2(abs_file_path, temp_file_path)
|
|
49
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:
|
|
79
|
+
pass
|
|
80
|
+
|
|
50
81
|
# 创建临时的 setup.py 文件
|
|
51
82
|
setup_code = f"""
|
|
52
83
|
from setuptools import setup
|
|
@@ -58,13 +89,13 @@ compiler_directives = {{
|
|
|
58
89
|
'annotation_typing': False,
|
|
59
90
|
'always_allow_keywords': True,
|
|
60
91
|
'binding': True,
|
|
61
|
-
'embedsignature':
|
|
92
|
+
'embedsignature': False,
|
|
62
93
|
'wraparound': False,
|
|
63
94
|
}}
|
|
64
95
|
|
|
65
96
|
setup(
|
|
66
97
|
ext_modules=cythonize(
|
|
67
|
-
{repr(
|
|
98
|
+
{repr(safe_file_name)},
|
|
68
99
|
compiler_directives=compiler_directives,
|
|
69
100
|
force=True
|
|
70
101
|
)
|
|
@@ -76,30 +107,34 @@ setup(
|
|
|
76
107
|
|
|
77
108
|
# 执行编译命令
|
|
78
109
|
command = [sys.executable, "setup.py", "build_ext", "--inplace"]
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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}")
|
|
85
120
|
|
|
86
121
|
if result.returncode != 0:
|
|
87
122
|
error_msg = result.stderr.decode('utf-8', errors='replace')
|
|
88
123
|
raise RuntimeError(f"Compilation failed: {error_msg}")
|
|
89
124
|
|
|
90
125
|
# 查找生成的二进制文件
|
|
91
|
-
pattern = os.path.join(temp_dir, f"{
|
|
126
|
+
pattern = os.path.join(temp_dir, f"{safe_module_name}*{target_ext}")
|
|
92
127
|
matches = glob.glob(pattern)
|
|
93
128
|
|
|
94
129
|
if not matches:
|
|
95
|
-
pattern = os.path.join(temp_dir, f"*{
|
|
130
|
+
pattern = os.path.join(temp_dir, f"*{safe_module_name}*{target_ext}")
|
|
96
131
|
matches = glob.glob(pattern)
|
|
97
132
|
|
|
98
133
|
if not matches:
|
|
99
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)}")
|
|
100
135
|
|
|
101
136
|
# 取最新生成的文件
|
|
102
|
-
generated_file = max(matches, key=os.path.
|
|
137
|
+
generated_file = max(matches, key=os.path.getmtime)
|
|
103
138
|
|
|
104
139
|
# 获取源文件的目录(使用原始路径类型)
|
|
105
140
|
if is_absolute:
|
|
@@ -116,9 +151,14 @@ setup(
|
|
|
116
151
|
output_file_name = os.path.basename(generated_file)
|
|
117
152
|
output_path = os.path.join(output_dir, output_file_name)
|
|
118
153
|
|
|
119
|
-
#
|
|
154
|
+
# 移动文件(先删除已存在的目标)
|
|
155
|
+
if os.path.exists(output_path):
|
|
156
|
+
os.remove(output_path)
|
|
120
157
|
shutil.move(generated_file, output_path)
|
|
121
158
|
|
|
159
|
+
# strip 符号表
|
|
160
|
+
_strip_binary(output_path)
|
|
161
|
+
|
|
122
162
|
if del_source:
|
|
123
163
|
os.remove(file_path)
|
|
124
164
|
|
|
@@ -129,7 +169,7 @@ setup(
|
|
|
129
169
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
130
170
|
|
|
131
171
|
# 测试函数
|
|
132
|
-
if __name__ == "__main__":
|
|
172
|
+
if __name__ == "__main__": # pragma: no cover
|
|
133
173
|
# 替换为你的 Python 文件路径
|
|
134
174
|
target_file = "test/example.py" # 请确保文件路径正确
|
|
135
175
|
|
|
@@ -137,4 +177,4 @@ if __name__ == "__main__":
|
|
|
137
177
|
output_file = compile_to_binary(target_file)
|
|
138
178
|
print(output_file)
|
|
139
179
|
except Exception as e:
|
|
140
|
-
print(e)
|
|
180
|
+
print(e)
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""AST 混淆模块:去 docstring、去 annotation、局部变量重命名、字符串加密、常量折叠、控制流平坦化、虚假分支"""
|
|
2
|
+
import ast
|
|
3
|
+
import copy
|
|
4
|
+
import hashlib
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
_UNSAFE_CALLS = frozenset({'globals', 'locals', 'eval', 'exec', 'vars'})
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _has_unsafe_call(node):
|
|
11
|
+
"""检查函数体是否包含 globals()/locals()/eval()/exec()/vars() 调用"""
|
|
12
|
+
for child in ast.walk(node):
|
|
13
|
+
if isinstance(child, ast.Call) and isinstance(child.func, ast.Name) and child.func.id in _UNSAFE_CALLS:
|
|
14
|
+
return True
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _walk_no_nested_scope(body):
|
|
19
|
+
"""遍历 body 中的节点,不穿透嵌套函数/类定义"""
|
|
20
|
+
for node in body:
|
|
21
|
+
yield node
|
|
22
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
23
|
+
continue
|
|
24
|
+
for child in ast.iter_child_nodes(node):
|
|
25
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
26
|
+
continue
|
|
27
|
+
yield from _walk_no_nested_scope([child])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _DocstringRemover(ast.NodeTransformer):
|
|
31
|
+
"""移除 module/class/function 首部的 docstring"""
|
|
32
|
+
|
|
33
|
+
def _strip_docstring(self, node):
|
|
34
|
+
if (node.body and isinstance(node.body[0], ast.Expr)
|
|
35
|
+
and isinstance(node.body[0].value, (ast.Constant, ast.Str))):
|
|
36
|
+
node.body.pop(0)
|
|
37
|
+
if not node.body:
|
|
38
|
+
node.body.append(ast.Pass())
|
|
39
|
+
return node
|
|
40
|
+
|
|
41
|
+
def visit_Module(self, node):
|
|
42
|
+
self.generic_visit(node)
|
|
43
|
+
return self._strip_docstring(node)
|
|
44
|
+
|
|
45
|
+
def visit_FunctionDef(self, node):
|
|
46
|
+
self.generic_visit(node)
|
|
47
|
+
return self._strip_docstring(node)
|
|
48
|
+
|
|
49
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
50
|
+
|
|
51
|
+
def visit_ClassDef(self, node):
|
|
52
|
+
self.generic_visit(node)
|
|
53
|
+
return self._strip_docstring(node)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _AnnotationRemover(ast.NodeTransformer):
|
|
57
|
+
"""清除函数参数和返回值的类型注解,以及变量注解"""
|
|
58
|
+
|
|
59
|
+
def visit_FunctionDef(self, node):
|
|
60
|
+
self.generic_visit(node)
|
|
61
|
+
node.returns = None
|
|
62
|
+
for arg in node.args.args + node.args.posonlyargs + node.args.kwonlyargs:
|
|
63
|
+
arg.annotation = None
|
|
64
|
+
if node.args.vararg:
|
|
65
|
+
node.args.vararg.annotation = None
|
|
66
|
+
if node.args.kwarg:
|
|
67
|
+
node.args.kwarg.annotation = None
|
|
68
|
+
return node
|
|
69
|
+
|
|
70
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
71
|
+
|
|
72
|
+
def visit_ClassDef(self, node):
|
|
73
|
+
self.generic_visit(node)
|
|
74
|
+
if not node.body:
|
|
75
|
+
node.body.append(ast.Pass())
|
|
76
|
+
return node
|
|
77
|
+
|
|
78
|
+
def visit_AnnAssign(self, node):
|
|
79
|
+
self.generic_visit(node)
|
|
80
|
+
if node.value is not None:
|
|
81
|
+
return ast.Assign(targets=[node.target], value=node.value,
|
|
82
|
+
lineno=node.lineno, col_offset=node.col_offset)
|
|
83
|
+
return None # 纯注解无赋值,直接删除
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _collect_nested_free_refs(body, local_names):
|
|
87
|
+
"""收集嵌套函数/类中引用的外层变量名(闭包变量)"""
|
|
88
|
+
free = set()
|
|
89
|
+
for node in body:
|
|
90
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
91
|
+
# 嵌套函数自己的局部名
|
|
92
|
+
nested_locals = set()
|
|
93
|
+
# nonlocal/global 声明的变量不是局部变量
|
|
94
|
+
nonlocal_names = set()
|
|
95
|
+
for child in _walk_no_nested_scope(node.body):
|
|
96
|
+
if isinstance(child, (ast.Nonlocal, ast.Global)):
|
|
97
|
+
nonlocal_names.update(child.names)
|
|
98
|
+
for arg in node.args.args + node.args.posonlyargs + node.args.kwonlyargs:
|
|
99
|
+
nested_locals.add(arg.arg)
|
|
100
|
+
if node.args.vararg:
|
|
101
|
+
nested_locals.add(node.args.vararg.arg)
|
|
102
|
+
if node.args.kwarg:
|
|
103
|
+
nested_locals.add(node.args.kwarg.arg)
|
|
104
|
+
for child in _walk_no_nested_scope(node.body):
|
|
105
|
+
if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store):
|
|
106
|
+
if child.id not in nonlocal_names:
|
|
107
|
+
nested_locals.add(child.id)
|
|
108
|
+
# 嵌套函数中读取的、属于外层 local_names 且不被自身遮蔽的名字
|
|
109
|
+
for child in ast.walk(node):
|
|
110
|
+
if isinstance(child, ast.Name) and child.id in local_names and child.id not in nested_locals:
|
|
111
|
+
free.add(child.id)
|
|
112
|
+
elif isinstance(node, ast.ClassDef):
|
|
113
|
+
# 类体中直接引用的外层变量
|
|
114
|
+
for child in _walk_no_nested_scope(node.body):
|
|
115
|
+
if isinstance(child, ast.Name) and child.id in local_names and isinstance(child.ctx, ast.Load):
|
|
116
|
+
free.add(child.id)
|
|
117
|
+
free |= _collect_nested_free_refs(node.body, local_names)
|
|
118
|
+
else:
|
|
119
|
+
# 递归搜索所有子节点中的嵌套函数/类(穿透 Try/With/For/If 等)
|
|
120
|
+
for child in ast.walk(node):
|
|
121
|
+
if child is node:
|
|
122
|
+
continue
|
|
123
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
124
|
+
free |= _collect_nested_free_refs([child], local_names)
|
|
125
|
+
return free
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _hash_name(name, salt):
|
|
129
|
+
"""基于原名+salt生成短 hash 标识符,避免明显的序号模式"""
|
|
130
|
+
h = hashlib.md5(f'{salt}:{name}'.encode()).hexdigest()[:8]
|
|
131
|
+
return f'_{h}'
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class _LocalVarRenamer(ast.NodeTransformer):
|
|
135
|
+
"""函数作用域内的局部变量重命名为 hash-based 短标识符"""
|
|
136
|
+
|
|
137
|
+
def _rename_function(self, node):
|
|
138
|
+
if _has_unsafe_call(node):
|
|
139
|
+
self.generic_visit(node)
|
|
140
|
+
return node
|
|
141
|
+
|
|
142
|
+
# 收集参数名(不重命名)
|
|
143
|
+
param_names = set()
|
|
144
|
+
for arg in node.args.args + node.args.posonlyargs + node.args.kwonlyargs:
|
|
145
|
+
param_names.add(arg.arg)
|
|
146
|
+
if node.args.vararg:
|
|
147
|
+
param_names.add(node.args.vararg.arg)
|
|
148
|
+
if node.args.kwarg:
|
|
149
|
+
param_names.add(node.args.kwarg.arg)
|
|
150
|
+
|
|
151
|
+
# 收集局部赋值目标(不穿透嵌套作用域)
|
|
152
|
+
local_names = set()
|
|
153
|
+
# 排除 nonlocal/global 声明的变量
|
|
154
|
+
nonlocal_names = set()
|
|
155
|
+
for child in _walk_no_nested_scope(node.body):
|
|
156
|
+
if isinstance(child, (ast.Nonlocal, ast.Global)):
|
|
157
|
+
nonlocal_names.update(child.names)
|
|
158
|
+
if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store):
|
|
159
|
+
name = child.id
|
|
160
|
+
if (name not in param_names and name != 'self' and name != 'cls'
|
|
161
|
+
and not name.startswith('__')):
|
|
162
|
+
local_names.add(name)
|
|
163
|
+
local_names -= nonlocal_names
|
|
164
|
+
|
|
165
|
+
if not local_names:
|
|
166
|
+
self.generic_visit(node)
|
|
167
|
+
return node
|
|
168
|
+
|
|
169
|
+
# 排除被嵌套函数引用的闭包变量(重命名会破坏语义)
|
|
170
|
+
closure_refs = _collect_nested_free_refs(node.body, local_names)
|
|
171
|
+
local_names -= closure_refs
|
|
172
|
+
|
|
173
|
+
if not local_names:
|
|
174
|
+
self.generic_visit(node)
|
|
175
|
+
return node
|
|
176
|
+
|
|
177
|
+
# 构建重命名映射(hash-based,用函数名做 salt 防跨函数碰撞)
|
|
178
|
+
salt = getattr(node, 'name', '')
|
|
179
|
+
rename_map = {name: _hash_name(name, salt) for name in sorted(local_names)}
|
|
180
|
+
# 碰撞检测:若有重复值,回退加序号
|
|
181
|
+
seen = {}
|
|
182
|
+
for name in sorted(local_names):
|
|
183
|
+
h = rename_map[name]
|
|
184
|
+
if h in seen:
|
|
185
|
+
rename_map[name] = f'{h}{len(seen)}'
|
|
186
|
+
seen[h] = name
|
|
187
|
+
|
|
188
|
+
# 应用重命名(不穿透嵌套作用域)
|
|
189
|
+
for child in _walk_no_nested_scope(node.body):
|
|
190
|
+
if isinstance(child, ast.Name) and child.id in rename_map:
|
|
191
|
+
child.id = rename_map[child.id]
|
|
192
|
+
|
|
193
|
+
# 递归处理嵌套函数/类(它们有自己的作用域)
|
|
194
|
+
self.generic_visit(node)
|
|
195
|
+
return node
|
|
196
|
+
|
|
197
|
+
def visit_FunctionDef(self, node):
|
|
198
|
+
return self._rename_function(node)
|
|
199
|
+
|
|
200
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class _StringEncryptor(ast.NodeTransformer):
|
|
204
|
+
"""将字符串常量替换为 XOR 解密表达式"""
|
|
205
|
+
|
|
206
|
+
def __init__(self, rng=None):
|
|
207
|
+
self._rng = rng or random
|
|
208
|
+
self._key = self._rng.randint(1, 255)
|
|
209
|
+
|
|
210
|
+
def visit_JoinedStr(self, node):
|
|
211
|
+
return node # 不处理 f-string
|
|
212
|
+
|
|
213
|
+
def visit_Constant(self, node):
|
|
214
|
+
v = node.value
|
|
215
|
+
if not isinstance(v, str) or len(v) <= 1:
|
|
216
|
+
return node
|
|
217
|
+
key = self._key
|
|
218
|
+
enc = bytes(b ^ key for b in v.encode('utf-8'))
|
|
219
|
+
# bytes(enc_literal).translate(bytes.maketrans(range(256), bytes(b^KEY for b in range(256)))).decode()
|
|
220
|
+
# 简化为:直接存加密后的 bytes 常量,用 translate 解密
|
|
221
|
+
# 构建 XOR 转换表作为常量
|
|
222
|
+
table_bytes = bytes(b ^ key for b in range(256))
|
|
223
|
+
return ast.Call(
|
|
224
|
+
func=ast.Attribute(
|
|
225
|
+
value=ast.Call(
|
|
226
|
+
func=ast.Attribute(
|
|
227
|
+
value=ast.Constant(value=enc),
|
|
228
|
+
attr='translate',
|
|
229
|
+
ctx=ast.Load(),
|
|
230
|
+
),
|
|
231
|
+
args=[ast.Call(
|
|
232
|
+
func=ast.Attribute(
|
|
233
|
+
value=ast.Name(id='bytes', ctx=ast.Load()),
|
|
234
|
+
attr='maketrans',
|
|
235
|
+
ctx=ast.Load(),
|
|
236
|
+
),
|
|
237
|
+
args=[
|
|
238
|
+
ast.Constant(value=bytes(range(256))),
|
|
239
|
+
ast.Constant(value=table_bytes),
|
|
240
|
+
],
|
|
241
|
+
keywords=[],
|
|
242
|
+
)],
|
|
243
|
+
keywords=[],
|
|
244
|
+
),
|
|
245
|
+
attr='decode',
|
|
246
|
+
ctx=ast.Load(),
|
|
247
|
+
),
|
|
248
|
+
args=[],
|
|
249
|
+
keywords=[],
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class _ConstantFoldingObfuscator(ast.NodeTransformer):
|
|
254
|
+
"""将整数常量拆分为算术表达式"""
|
|
255
|
+
|
|
256
|
+
def __init__(self, rng=None):
|
|
257
|
+
self._rng = rng or random
|
|
258
|
+
|
|
259
|
+
def visit_Constant(self, node):
|
|
260
|
+
v = node.value
|
|
261
|
+
if isinstance(v, bool) or not isinstance(v, int):
|
|
262
|
+
return node
|
|
263
|
+
if v in (0, 1) or v < 0 or v > 10000:
|
|
264
|
+
return node
|
|
265
|
+
return self._decompose_2layer(v)
|
|
266
|
+
|
|
267
|
+
def _decompose_1layer(self, n):
|
|
268
|
+
if n <= 1:
|
|
269
|
+
return ast.Constant(value=n)
|
|
270
|
+
# 尝试找因子
|
|
271
|
+
for d in range(2, min(int(n**0.5) + 1, 100)):
|
|
272
|
+
if n % d == 0:
|
|
273
|
+
return ast.BinOp(
|
|
274
|
+
left=ast.Constant(value=d),
|
|
275
|
+
op=ast.Mult(),
|
|
276
|
+
right=ast.Constant(value=n // d),
|
|
277
|
+
)
|
|
278
|
+
# 加法拆分
|
|
279
|
+
a = self._rng.randint(1, n - 1)
|
|
280
|
+
return ast.BinOp(
|
|
281
|
+
left=ast.Constant(value=a),
|
|
282
|
+
op=ast.Add(),
|
|
283
|
+
right=ast.Constant(value=n - a),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def _decompose_2layer(self, n):
|
|
287
|
+
a = self._rng.randint(1, n - 1)
|
|
288
|
+
b = n - a
|
|
289
|
+
return ast.BinOp(
|
|
290
|
+
left=self._decompose_1layer(a),
|
|
291
|
+
op=ast.Add(),
|
|
292
|
+
right=self._decompose_1layer(b),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class _ControlFlowFlattener(ast.NodeTransformer):
|
|
297
|
+
"""将函数体转换为 while True + 状态机调度"""
|
|
298
|
+
|
|
299
|
+
def __init__(self, rng=None):
|
|
300
|
+
self._rng = rng or random
|
|
301
|
+
|
|
302
|
+
def visit_FunctionDef(self, node):
|
|
303
|
+
self.generic_visit(node) # 先递归处理嵌套
|
|
304
|
+
if self._should_skip(node):
|
|
305
|
+
return node
|
|
306
|
+
node.body = self._flatten_body(node.body)
|
|
307
|
+
return node
|
|
308
|
+
|
|
309
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
310
|
+
|
|
311
|
+
def _should_skip(self, node):
|
|
312
|
+
if isinstance(node, ast.AsyncFunctionDef):
|
|
313
|
+
return True
|
|
314
|
+
if _has_unsafe_call(node):
|
|
315
|
+
return True
|
|
316
|
+
if len(node.body) < 3:
|
|
317
|
+
return True
|
|
318
|
+
for child in ast.walk(node):
|
|
319
|
+
if isinstance(child, (ast.Yield, ast.YieldFrom)):
|
|
320
|
+
return True
|
|
321
|
+
# nonlocal/global 变量在状态机分支中会被 Cython 误判为未赋值
|
|
322
|
+
if isinstance(child, (ast.Nonlocal, ast.Global)):
|
|
323
|
+
return True
|
|
324
|
+
# comprehension 在 while True 状态机内会触发 Cython ControlFlowAnalysis crash
|
|
325
|
+
for child in ast.walk(node):
|
|
326
|
+
if child is node:
|
|
327
|
+
continue
|
|
328
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
329
|
+
continue
|
|
330
|
+
if isinstance(child, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
|
|
331
|
+
return True
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
def _flatten_body(self, stmts):
|
|
335
|
+
states = self._rng.sample(range(100, 999), len(stmts) + 1)
|
|
336
|
+
# states[0] = 初始, states[1..n] = 各语句, states[-1] = 终止(break)
|
|
337
|
+
state_var = '_s'
|
|
338
|
+
cases = []
|
|
339
|
+
for i, stmt in enumerate(stmts):
|
|
340
|
+
test = ast.Compare(
|
|
341
|
+
left=ast.Name(id=state_var, ctx=ast.Load()),
|
|
342
|
+
ops=[ast.Eq()],
|
|
343
|
+
comparators=[ast.Constant(value=states[i])],
|
|
344
|
+
)
|
|
345
|
+
body = [stmt]
|
|
346
|
+
# return 语句不追加状态转移
|
|
347
|
+
if not isinstance(stmt, ast.Return):
|
|
348
|
+
if i < len(stmts) - 1:
|
|
349
|
+
body.append(ast.Assign(
|
|
350
|
+
targets=[ast.Name(id=state_var, ctx=ast.Store())],
|
|
351
|
+
value=ast.Constant(value=states[i + 1]),
|
|
352
|
+
))
|
|
353
|
+
else:
|
|
354
|
+
body.append(ast.Break())
|
|
355
|
+
cases.append((test, body))
|
|
356
|
+
|
|
357
|
+
# 构建 if/elif 链
|
|
358
|
+
if_node = None
|
|
359
|
+
for test, body in reversed(cases):
|
|
360
|
+
if if_node is None:
|
|
361
|
+
if_node = ast.If(test=test, body=body, orelse=[])
|
|
362
|
+
else:
|
|
363
|
+
if_node = ast.If(test=test, body=body, orelse=[if_node])
|
|
364
|
+
|
|
365
|
+
return [
|
|
366
|
+
ast.Assign(
|
|
367
|
+
targets=[ast.Name(id=state_var, ctx=ast.Store())],
|
|
368
|
+
value=ast.Constant(value=states[0]),
|
|
369
|
+
),
|
|
370
|
+
ast.While(
|
|
371
|
+
test=ast.Constant(value=True),
|
|
372
|
+
body=[if_node],
|
|
373
|
+
orelse=[],
|
|
374
|
+
),
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class _OpaquePredicateInserter(ast.NodeTransformer):
|
|
379
|
+
"""在函数体中插入永真/永假虚假分支"""
|
|
380
|
+
|
|
381
|
+
_PREDICATES_TRUE = [
|
|
382
|
+
# n * n >= 0
|
|
383
|
+
lambda: ast.Compare(
|
|
384
|
+
left=ast.BinOp(
|
|
385
|
+
left=ast.Constant(value=7),
|
|
386
|
+
op=ast.Mult(),
|
|
387
|
+
right=ast.Constant(value=7),
|
|
388
|
+
),
|
|
389
|
+
ops=[ast.GtE()],
|
|
390
|
+
comparators=[ast.Constant(value=0)],
|
|
391
|
+
),
|
|
392
|
+
# isinstance(1, int)
|
|
393
|
+
lambda: ast.Call(
|
|
394
|
+
func=ast.Name(id='isinstance', ctx=ast.Load()),
|
|
395
|
+
args=[ast.Constant(value=1), ast.Name(id='int', ctx=ast.Load())],
|
|
396
|
+
keywords=[],
|
|
397
|
+
),
|
|
398
|
+
# len([0]) > 0
|
|
399
|
+
lambda: ast.Compare(
|
|
400
|
+
left=ast.Call(
|
|
401
|
+
func=ast.Name(id='len', ctx=ast.Load()),
|
|
402
|
+
args=[ast.List(elts=[ast.Constant(value=0)], ctx=ast.Load())],
|
|
403
|
+
keywords=[],
|
|
404
|
+
),
|
|
405
|
+
ops=[ast.Gt()],
|
|
406
|
+
comparators=[ast.Constant(value=0)],
|
|
407
|
+
),
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
_DEAD_CODE = [
|
|
411
|
+
lambda: ast.Assign(
|
|
412
|
+
targets=[ast.Name(id='_', ctx=ast.Store())],
|
|
413
|
+
value=ast.Constant(value=0),
|
|
414
|
+
),
|
|
415
|
+
lambda: ast.Pass(),
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
def __init__(self, rng=None):
|
|
419
|
+
self._rng = rng or random
|
|
420
|
+
|
|
421
|
+
def visit_FunctionDef(self, node):
|
|
422
|
+
self.generic_visit(node)
|
|
423
|
+
if _has_unsafe_call(node):
|
|
424
|
+
return node
|
|
425
|
+
node.body = self._insert_predicates(node.body)
|
|
426
|
+
return node
|
|
427
|
+
|
|
428
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
429
|
+
|
|
430
|
+
def _insert_predicates(self, stmts):
|
|
431
|
+
result = []
|
|
432
|
+
for stmt in stmts:
|
|
433
|
+
result.append(stmt)
|
|
434
|
+
if self._rng.random() < 0.3:
|
|
435
|
+
pred = self._rng.choice(self._PREDICATES_TRUE)()
|
|
436
|
+
dead = self._rng.choice(self._DEAD_CODE)()
|
|
437
|
+
if self._rng.random() < 0.5:
|
|
438
|
+
# 永真分支:真分支有原始无害代码,假分支有死代码
|
|
439
|
+
result.append(ast.If(test=pred, body=[ast.Pass()], orelse=[dead]))
|
|
440
|
+
else:
|
|
441
|
+
# 永假分支(取反)
|
|
442
|
+
result.append(ast.If(
|
|
443
|
+
test=ast.UnaryOp(op=ast.Not(), operand=pred),
|
|
444
|
+
body=[dead],
|
|
445
|
+
orelse=[],
|
|
446
|
+
))
|
|
447
|
+
return result
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def obfuscate_source(source_code: str, seed=None) -> str:
|
|
451
|
+
"""对源码执行七重 AST 变换:去 docstring、去 annotation、局部变量重命名、
|
|
452
|
+
字符串加密、常量折叠、控制流平坦化、虚假分支。
|
|
453
|
+
ast.unparse() 天然丢弃所有注释。
|
|
454
|
+
|
|
455
|
+
:param seed: 随机种子;传入后可复现混淆结果
|
|
456
|
+
"""
|
|
457
|
+
if not source_code.strip():
|
|
458
|
+
return source_code
|
|
459
|
+
rng = random.Random(seed) if seed is not None else random
|
|
460
|
+
tree = ast.parse(source_code)
|
|
461
|
+
tree = _DocstringRemover().visit(tree)
|
|
462
|
+
tree = _AnnotationRemover().visit(tree)
|
|
463
|
+
tree = _LocalVarRenamer().visit(tree)
|
|
464
|
+
tree = _ControlFlowFlattener(rng=rng).visit(tree)
|
|
465
|
+
tree = _OpaquePredicateInserter(rng=rng).visit(tree)
|
|
466
|
+
tree = _StringEncryptor(rng=rng).visit(tree)
|
|
467
|
+
tree = _ConstantFoldingObfuscator(rng=rng).visit(tree)
|
|
468
|
+
ast.fix_missing_locations(tree)
|
|
469
|
+
return ast.unparse(tree)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import time
|
|
3
|
-
import locale
|
|
4
3
|
import platform
|
|
5
4
|
import threading
|
|
6
5
|
import concurrent.futures
|
|
@@ -12,29 +11,17 @@ from rich.spinner import Spinner
|
|
|
12
11
|
from rich.columns import Columns
|
|
13
12
|
from rich.progress import Progress, BarColumn, TimeRemainingColumn, TimeElapsedColumn
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
"""
|
|
17
|
-
获取系统语言,兼容 Python 3.11+
|
|
14
|
+
from .tools import get_system_language
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
try:
|
|
22
|
-
# Python 3.11+ 推荐方式
|
|
23
|
-
lang = locale.getlocale()[0]
|
|
24
|
-
if lang is None:
|
|
25
|
-
# 回退到环境变量
|
|
26
|
-
lang = os.environ.get('LANG', os.environ.get('LANGUAGE', ''))
|
|
27
|
-
return 'zh' if lang and lang.startswith('zh') else 'en'
|
|
28
|
-
except Exception:
|
|
29
|
-
return 'en'
|
|
30
|
-
|
|
31
|
-
def run_tasks(task_list, max_workers=2, language=None):
|
|
16
|
+
|
|
17
|
+
def run_tasks(task_list, max_workers=2, language=None, raise_on_failure=False):
|
|
32
18
|
"""
|
|
33
19
|
并发执行任务列表并实时显示状态
|
|
34
20
|
|
|
35
21
|
:param task_list: 任务列表,每个元素是 (函数, 位置参数元组, 关键字参数字典)
|
|
36
22
|
:param max_workers: 最大并发线程数
|
|
37
23
|
:param language: 显示语言 ('en' 或 'zh', 默认根据系统语言自动判断)
|
|
24
|
+
:param raise_on_failure: 若存在失败任务,是否抛出 RuntimeError
|
|
38
25
|
"""
|
|
39
26
|
print("""\n █████╗ ██╗ ██╗████████╗ ██████╗ ██████╗██╗ ██╗████████╗██╗ ██╗ ██████╗ ███╗ ██╗
|
|
40
27
|
██╔══██╗██║ ██║╚══██╔══╝██╔═══██╗██╔════╝╚██╗ ██╔╝╚══██╔══╝██║ ██║██╔═══██╗████╗ ██║
|
|
@@ -47,7 +34,7 @@ def run_tasks(task_list, max_workers=2, language=None):
|
|
|
47
34
|
print(f"{platform.system()} {platform.version()} {platform.machine()} | {platform.python_implementation()} {platform.python_version()} {platform.python_compiler()} | {bit_architecture}")
|
|
48
35
|
|
|
49
36
|
# 获取系统默认区域设置
|
|
50
|
-
language = get_system_language()
|
|
37
|
+
language = language or get_system_language()
|
|
51
38
|
|
|
52
39
|
# 中英文文本映射
|
|
53
40
|
TEXT_MAP = {
|
|
@@ -267,13 +254,24 @@ def run_tasks(task_list, max_workers=2, language=None):
|
|
|
267
254
|
|
|
268
255
|
# 最终汇总信息
|
|
269
256
|
total_elapsed = time.time() - start_time
|
|
270
|
-
success_count = sum(1 for
|
|
271
|
-
failure_count = sum(1 for
|
|
257
|
+
success_count = sum(1 for ts in task_status if ts["status"] == "success")
|
|
258
|
+
failure_count = sum(1 for ts in task_status if ts["status"] == "failed")
|
|
272
259
|
|
|
273
260
|
console.print(t['succeeded'].format(success_count) + t['failed'].format(failure_count) + t['total_time'].format(total_elapsed) + t['all_completed'])
|
|
274
261
|
|
|
262
|
+
summary = {
|
|
263
|
+
"total": total_tasks,
|
|
264
|
+
"succeeded": success_count,
|
|
265
|
+
"failed": failure_count,
|
|
266
|
+
"elapsed": total_elapsed,
|
|
267
|
+
"tasks": task_status,
|
|
268
|
+
}
|
|
269
|
+
if raise_on_failure and failure_count > 0:
|
|
270
|
+
raise RuntimeError(f"{failure_count} task(s) failed")
|
|
271
|
+
return summary
|
|
272
|
+
|
|
275
273
|
# 示例使用方式
|
|
276
|
-
if __name__ == "__main__":
|
|
274
|
+
if __name__ == "__main__": # pragma: no cover
|
|
277
275
|
# 示例任务函数
|
|
278
276
|
def task_success(seconds):
|
|
279
277
|
time.sleep(seconds)
|
|
@@ -35,8 +35,13 @@ def find_python_files(path):
|
|
|
35
35
|
return any(marker in line for marker in exclude_markers)
|
|
36
36
|
|
|
37
37
|
valid_py_files = []
|
|
38
|
+
skip_dirs = {
|
|
39
|
+
"__pycache__", "venv", ".venv", "build", "dist",
|
|
40
|
+
"node_modules", ".git", ".eggs",
|
|
41
|
+
}
|
|
38
42
|
|
|
39
43
|
for root, dirs, files in os.walk(path):
|
|
44
|
+
dirs[:] = [d for d in dirs if d not in skip_dirs and not d.endswith(".egg-info")]
|
|
40
45
|
for file in files:
|
|
41
46
|
if file == "__init__.py":
|
|
42
47
|
continue
|
|
@@ -80,6 +85,7 @@ def parse_arguments():
|
|
|
80
85
|
'path_help': 'Compile directory path',
|
|
81
86
|
'conc_help': 'Compile concurrency count (default: 2)',
|
|
82
87
|
'del_help': 'Remove source code after compilation (default: False)',
|
|
88
|
+
'seed_help': 'Set obfuscation random seed for reproducible output',
|
|
83
89
|
'help_help': 'Show help message',
|
|
84
90
|
'version_help': 'Show program version',
|
|
85
91
|
'version_text': f'v{__version__}',
|
|
@@ -92,6 +98,7 @@ def parse_arguments():
|
|
|
92
98
|
'path_help': '编译目录路径',
|
|
93
99
|
'conc_help': '编译并发数(默认:2)',
|
|
94
100
|
'del_help': '编译后删除源代码(默认:False)',
|
|
101
|
+
'seed_help': '设置混淆随机种子(用于可复现构建)',
|
|
95
102
|
'help_help': '显示帮助信息',
|
|
96
103
|
'version_help': '显示程序版本',
|
|
97
104
|
'version_text': f'v{__version__}',
|
|
@@ -105,9 +112,7 @@ def parse_arguments():
|
|
|
105
112
|
class CustomParser(argparse.ArgumentParser):
|
|
106
113
|
def error(self, message):
|
|
107
114
|
self.print_usage(sys.stderr)
|
|
108
|
-
|
|
109
|
-
err_msg = msg['required_error'].format(', '.join(required_args))
|
|
110
|
-
sys.stderr.write(f'error: {err_msg}\n')
|
|
115
|
+
sys.stderr.write(f'error: {message}\n')
|
|
111
116
|
sys.exit(2)
|
|
112
117
|
|
|
113
118
|
# 配置参数解析器
|
|
@@ -118,14 +123,15 @@ def parse_arguments():
|
|
|
118
123
|
formatter_class=argparse.RawTextHelpFormatter
|
|
119
124
|
)
|
|
120
125
|
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
# 添加参数定义(文件与目录互斥)
|
|
127
|
+
input_group = parser.add_mutually_exclusive_group(required=False)
|
|
128
|
+
input_group.add_argument('-f', '--file', type=str, help=msg['file_help'])
|
|
129
|
+
input_group.add_argument('-p', '--path', type=str, help=msg['path_help'])
|
|
125
130
|
|
|
126
131
|
optional_group = parser.add_argument_group('optional arguments')
|
|
127
132
|
optional_group.add_argument('-c', '--conc', type=int, default=2, help=msg['conc_help'])
|
|
128
133
|
optional_group.add_argument('-d', '--del', dest='del_source', action='store_true', help=msg['del_help'])
|
|
134
|
+
optional_group.add_argument('--seed', type=int, default=None, help=msg['seed_help'])
|
|
129
135
|
optional_group.add_argument('-h', '--help', action='store_true', help=msg['help_help'])
|
|
130
136
|
optional_group.add_argument('-v', '--version', action='store_true', help=msg['version_help'])
|
|
131
137
|
|
|
@@ -143,7 +149,8 @@ def parse_arguments():
|
|
|
143
149
|
|
|
144
150
|
if not args.file and not args.path:
|
|
145
151
|
parser.print_help()
|
|
146
|
-
|
|
152
|
+
print(f"\nerror: {msg['required_error'].format('-f/--file, -p/--path')}", file=sys.stderr)
|
|
153
|
+
sys.exit(2)
|
|
147
154
|
|
|
148
155
|
return args
|
|
149
156
|
|
|
@@ -171,7 +178,7 @@ def show_path_not_found(path):
|
|
|
171
178
|
else:
|
|
172
179
|
print(f"Path {path} does not exist!")
|
|
173
180
|
|
|
174
|
-
if __name__ == "__main__":
|
|
181
|
+
if __name__ == "__main__": # pragma: no cover
|
|
175
182
|
args = parse_arguments()
|
|
176
183
|
print(f"文件: {args.file}")
|
|
177
184
|
print(f"路径: {args.path}")
|
|
@@ -1,37 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoCython-zhang
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: 自动Cython,使用Cython批量编译.py文件为.pyd文件!
|
|
5
5
|
Author-email: zhang_gavin <qq814608@163.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
|
|
8
|
-
Copyright (c) [2024] [JianJun]
|
|
9
|
-
|
|
10
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
-
in the Software without restriction, including without limitation the rights
|
|
13
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
-
furnished to do so, subject to the following conditions:
|
|
16
|
-
|
|
17
|
-
The above copyright notice and this permission notice shall be included in all
|
|
18
|
-
copies or substantial portions of the Software.
|
|
19
|
-
|
|
20
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
-
SOFTWARE.
|
|
27
|
-
|
|
6
|
+
License-Expression: MIT
|
|
28
7
|
Project-URL: homepage, https://github.com/zhang0281/AutoCython
|
|
29
8
|
Project-URL: repository, https://github.com/zhang0281/AutoCython
|
|
30
9
|
Project-URL: documentation, https://github.com/zhang0281/AutoCython#readme
|
|
31
10
|
Keywords: cython,compile,pyd,pyc,python,autopyd
|
|
32
11
|
Classifier: Programming Language :: Python :: 3
|
|
33
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
34
12
|
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
35
14
|
Description-Content-Type: text/markdown
|
|
36
15
|
License-File: LICENSE
|
|
37
16
|
Requires-Dist: setuptools
|
|
@@ -76,8 +55,11 @@ AutoCython -f test.py
|
|
|
76
55
|
AutoCython -p D:/python_code/ProjectPath
|
|
77
56
|
|
|
78
57
|
# 编译后删除源代码 (默认不删除)
|
|
79
|
-
AutoCython -d
|
|
80
|
-
AutoCython -d
|
|
58
|
+
AutoCython -d -f test.py
|
|
59
|
+
AutoCython -d -p D:/python_code/ProjectPath
|
|
60
|
+
|
|
61
|
+
# 指定混淆随机种子(可复现构建)
|
|
62
|
+
AutoCython --seed 2025 -f test.py
|
|
81
63
|
```
|
|
82
64
|
|
|
83
65
|
### 编译界面
|
|
@@ -90,6 +72,36 @@ AutoCython -d True -p D:/python_code/ProjectPath
|
|
|
90
72
|
# 此文件将跳过编译处理
|
|
91
73
|
```
|
|
92
74
|
|
|
75
|
+
## 🧪 测试
|
|
76
|
+
```bash
|
|
77
|
+
# 运行全部测试
|
|
78
|
+
pytest
|
|
79
|
+
|
|
80
|
+
# 只运行单元测试
|
|
81
|
+
pytest -m unit
|
|
82
|
+
|
|
83
|
+
# 只运行集成测试 (kd_dist)
|
|
84
|
+
pytest -m integ
|
|
85
|
+
|
|
86
|
+
# 排除集成测试(快速验证)
|
|
87
|
+
pytest -m "not integ"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### kd-dist 集成测试配置
|
|
91
|
+
```bash
|
|
92
|
+
# 指定真实项目根目录(未设置则自动探测)
|
|
93
|
+
export KD_DIST_ROOT=/path/to/kd-dist
|
|
94
|
+
|
|
95
|
+
# 开启严格文件计数阈值校验(默认关闭)
|
|
96
|
+
export KD_DIST_STRICT_COUNTS=1
|
|
97
|
+
|
|
98
|
+
# 输出 kd-dist 分层测试 JSON 报告
|
|
99
|
+
export KD_DIST_REPORT_PATH=.pytest_cache/kd_dist_report.json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`tests/kd_dist/manifest.json` 用于定义 import policy 与行为等价用例,
|
|
103
|
+
`tests/kd_dist/known_failures.json` 用于记录已知 Cython 不兼容文件(xfail 治理)。
|
|
104
|
+
|
|
93
105
|
## ⚠️ 常见问题解决
|
|
94
106
|
|
|
95
107
|
一般是源代码中有 Cython 不支持的语句, 或者文件名不支持等.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
2
3
|
README.md
|
|
3
4
|
pyproject.toml
|
|
4
5
|
AutoCython/AutoCython.py
|
|
5
6
|
AutoCython/__init__.py
|
|
6
7
|
AutoCython/_version.py
|
|
7
8
|
AutoCython/compile.py
|
|
9
|
+
AutoCython/obfuscate.py
|
|
8
10
|
AutoCython/run_tasks.py
|
|
9
11
|
AutoCython/tools.py
|
|
10
12
|
AutoCython_zhang.egg-info/PKG-INFO
|
|
@@ -1,37 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoCython-zhang
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: 自动Cython,使用Cython批量编译.py文件为.pyd文件!
|
|
5
5
|
Author-email: zhang_gavin <qq814608@163.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
|
|
8
|
-
Copyright (c) [2024] [JianJun]
|
|
9
|
-
|
|
10
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
-
in the Software without restriction, including without limitation the rights
|
|
13
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
-
furnished to do so, subject to the following conditions:
|
|
16
|
-
|
|
17
|
-
The above copyright notice and this permission notice shall be included in all
|
|
18
|
-
copies or substantial portions of the Software.
|
|
19
|
-
|
|
20
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
-
SOFTWARE.
|
|
27
|
-
|
|
6
|
+
License-Expression: MIT
|
|
28
7
|
Project-URL: homepage, https://github.com/zhang0281/AutoCython
|
|
29
8
|
Project-URL: repository, https://github.com/zhang0281/AutoCython
|
|
30
9
|
Project-URL: documentation, https://github.com/zhang0281/AutoCython#readme
|
|
31
10
|
Keywords: cython,compile,pyd,pyc,python,autopyd
|
|
32
11
|
Classifier: Programming Language :: Python :: 3
|
|
33
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
34
12
|
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
35
14
|
Description-Content-Type: text/markdown
|
|
36
15
|
License-File: LICENSE
|
|
37
16
|
Requires-Dist: setuptools
|
|
@@ -76,8 +55,11 @@ AutoCython -f test.py
|
|
|
76
55
|
AutoCython -p D:/python_code/ProjectPath
|
|
77
56
|
|
|
78
57
|
# 编译后删除源代码 (默认不删除)
|
|
79
|
-
AutoCython -d
|
|
80
|
-
AutoCython -d
|
|
58
|
+
AutoCython -d -f test.py
|
|
59
|
+
AutoCython -d -p D:/python_code/ProjectPath
|
|
60
|
+
|
|
61
|
+
# 指定混淆随机种子(可复现构建)
|
|
62
|
+
AutoCython --seed 2025 -f test.py
|
|
81
63
|
```
|
|
82
64
|
|
|
83
65
|
### 编译界面
|
|
@@ -90,6 +72,36 @@ AutoCython -d True -p D:/python_code/ProjectPath
|
|
|
90
72
|
# 此文件将跳过编译处理
|
|
91
73
|
```
|
|
92
74
|
|
|
75
|
+
## 🧪 测试
|
|
76
|
+
```bash
|
|
77
|
+
# 运行全部测试
|
|
78
|
+
pytest
|
|
79
|
+
|
|
80
|
+
# 只运行单元测试
|
|
81
|
+
pytest -m unit
|
|
82
|
+
|
|
83
|
+
# 只运行集成测试 (kd_dist)
|
|
84
|
+
pytest -m integ
|
|
85
|
+
|
|
86
|
+
# 排除集成测试(快速验证)
|
|
87
|
+
pytest -m "not integ"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### kd-dist 集成测试配置
|
|
91
|
+
```bash
|
|
92
|
+
# 指定真实项目根目录(未设置则自动探测)
|
|
93
|
+
export KD_DIST_ROOT=/path/to/kd-dist
|
|
94
|
+
|
|
95
|
+
# 开启严格文件计数阈值校验(默认关闭)
|
|
96
|
+
export KD_DIST_STRICT_COUNTS=1
|
|
97
|
+
|
|
98
|
+
# 输出 kd-dist 分层测试 JSON 报告
|
|
99
|
+
export KD_DIST_REPORT_PATH=.pytest_cache/kd_dist_report.json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`tests/kd_dist/manifest.json` 用于定义 import policy 与行为等价用例,
|
|
103
|
+
`tests/kd_dist/known_failures.json` 用于记录已知 Cython 不兼容文件(xfail 治理)。
|
|
104
|
+
|
|
93
105
|
## ⚠️ 常见问题解决
|
|
94
106
|
|
|
95
107
|
一般是源代码中有 Cython 不支持的语句, 或者文件名不支持等.
|
|
@@ -35,8 +35,11 @@ AutoCython -f test.py
|
|
|
35
35
|
AutoCython -p D:/python_code/ProjectPath
|
|
36
36
|
|
|
37
37
|
# 编译后删除源代码 (默认不删除)
|
|
38
|
-
AutoCython -d
|
|
39
|
-
AutoCython -d
|
|
38
|
+
AutoCython -d -f test.py
|
|
39
|
+
AutoCython -d -p D:/python_code/ProjectPath
|
|
40
|
+
|
|
41
|
+
# 指定混淆随机种子(可复现构建)
|
|
42
|
+
AutoCython --seed 2025 -f test.py
|
|
40
43
|
```
|
|
41
44
|
|
|
42
45
|
### 编译界面
|
|
@@ -49,6 +52,36 @@ AutoCython -d True -p D:/python_code/ProjectPath
|
|
|
49
52
|
# 此文件将跳过编译处理
|
|
50
53
|
```
|
|
51
54
|
|
|
55
|
+
## 🧪 测试
|
|
56
|
+
```bash
|
|
57
|
+
# 运行全部测试
|
|
58
|
+
pytest
|
|
59
|
+
|
|
60
|
+
# 只运行单元测试
|
|
61
|
+
pytest -m unit
|
|
62
|
+
|
|
63
|
+
# 只运行集成测试 (kd_dist)
|
|
64
|
+
pytest -m integ
|
|
65
|
+
|
|
66
|
+
# 排除集成测试(快速验证)
|
|
67
|
+
pytest -m "not integ"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### kd-dist 集成测试配置
|
|
71
|
+
```bash
|
|
72
|
+
# 指定真实项目根目录(未设置则自动探测)
|
|
73
|
+
export KD_DIST_ROOT=/path/to/kd-dist
|
|
74
|
+
|
|
75
|
+
# 开启严格文件计数阈值校验(默认关闭)
|
|
76
|
+
export KD_DIST_STRICT_COUNTS=1
|
|
77
|
+
|
|
78
|
+
# 输出 kd-dist 分层测试 JSON 报告
|
|
79
|
+
export KD_DIST_REPORT_PATH=.pytest_cache/kd_dist_report.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`tests/kd_dist/manifest.json` 用于定义 import policy 与行为等价用例,
|
|
83
|
+
`tests/kd_dist/known_failures.json` 用于记录已知 Cython 不兼容文件(xfail 治理)。
|
|
84
|
+
|
|
52
85
|
## ⚠️ 常见问题解决
|
|
53
86
|
|
|
54
87
|
一般是源代码中有 Cython 不支持的语句, 或者文件名不支持等.
|
|
@@ -64,4 +97,4 @@ AutoCython -d True -p D:/python_code/ProjectPath
|
|
|
64
97
|
2. 20221123 可以通过文件头手动指定不编译的文件
|
|
65
98
|
3. 20230306 更新可以指定命令行头如 `Python310` 以此支持非Widnows系统下编译
|
|
66
99
|
4. 20230324 更新文档
|
|
67
|
-
5. 20240506 修复编译失败时遗漏复原 \_\_init\_\_.py 的问题
|
|
100
|
+
5. 20240506 修复编译失败时遗漏复原 \_\_init\_\_.py 的问题
|
|
@@ -5,14 +5,15 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "AutoCython-zhang"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
+
requires-python = ">=3.9"
|
|
8
9
|
authors = [{name = "zhang_gavin", email = "qq814608@163.com"}]
|
|
9
10
|
description = "自动Cython,使用Cython批量编译.py文件为.pyd文件!"
|
|
10
11
|
readme = "README.md"
|
|
11
|
-
license =
|
|
12
|
+
license = "MIT"
|
|
13
|
+
license-files = ["LICENSE"]
|
|
12
14
|
keywords = ["cython", "compile", "pyd", "pyc", "python", "autopyd"]
|
|
13
15
|
classifiers = [
|
|
14
16
|
"Programming Language :: Python :: 3",
|
|
15
|
-
"License :: OSI Approved :: MIT License",
|
|
16
17
|
"Operating System :: OS Independent",
|
|
17
18
|
]
|
|
18
19
|
dependencies = [
|
|
@@ -29,5 +30,19 @@ documentation = "https://github.com/zhang0281/AutoCython#readme"
|
|
|
29
30
|
[project.scripts]
|
|
30
31
|
AutoCython = "AutoCython:main"
|
|
31
32
|
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
addopts = "-n auto --dist=load"
|
|
35
|
+
markers = [
|
|
36
|
+
"unit: 单元测试",
|
|
37
|
+
"integ: 集成测试 (kd_dist)",
|
|
38
|
+
"kd_compile_plain: kd-dist 纯编译矩阵测试",
|
|
39
|
+
"kd_compile_obfuscate: kd-dist 混淆编译矩阵测试",
|
|
40
|
+
"kd_behavior: kd-dist 行为等价测试",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["."]
|
|
45
|
+
exclude = ["tests*"]
|
|
46
|
+
|
|
32
47
|
[tool.setuptools.dynamic]
|
|
33
48
|
version = {attr = "AutoCython._version.__version__"}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from .run_tasks import run_tasks
|
|
3
|
-
from .compile import compile_to_binary
|
|
4
|
-
from .tools import parse_arguments, find_python_files
|
|
5
|
-
from .tools import show_no_compilable_files, show_file_not_found, show_path_not_found
|
|
6
|
-
|
|
7
|
-
def compile():
|
|
8
|
-
args = parse_arguments()
|
|
9
|
-
|
|
10
|
-
if args.file:
|
|
11
|
-
if os.path.isfile(args.file):
|
|
12
|
-
compile_file = args.file
|
|
13
|
-
del_source = args.del_source
|
|
14
|
-
tasks = [
|
|
15
|
-
# 函数, 位置参数, 关键字参数
|
|
16
|
-
(compile_to_binary, compile_file, (compile_file, del_source), {}),
|
|
17
|
-
]
|
|
18
|
-
run_tasks(tasks, max_workers=1) # 执行编译
|
|
19
|
-
else:
|
|
20
|
-
show_file_not_found(args.file)
|
|
21
|
-
elif args.path:
|
|
22
|
-
if os.path.exists(args.path) and not os.path.isfile(args.path):
|
|
23
|
-
compile_file_list = find_python_files(args.path)
|
|
24
|
-
if compile_file_list:
|
|
25
|
-
del_source = args.del_source
|
|
26
|
-
tasks = []
|
|
27
|
-
for compile_file in compile_file_list:
|
|
28
|
-
tasks.append(
|
|
29
|
-
# 函数, 位置参数, 关键字参数
|
|
30
|
-
(compile_to_binary, compile_file, (compile_file, del_source), {}),
|
|
31
|
-
)
|
|
32
|
-
run_tasks(tasks, max_workers=args.conc) # 执行编译
|
|
33
|
-
else:
|
|
34
|
-
show_no_compilable_files(args.path)
|
|
35
|
-
else:
|
|
36
|
-
show_path_not_found(args.path)
|
|
File without changes
|
{autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|