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.
Files changed (20) hide show
  1. autocython_zhang-2.3.0/AutoCython/AutoCython.py +44 -0
  2. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython/_version.py +1 -1
  3. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython/compile.py +57 -17
  4. autocython_zhang-2.3.0/AutoCython/obfuscate.py +469 -0
  5. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython/run_tasks.py +19 -21
  6. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython/tools.py +16 -9
  7. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/PKG-INFO +38 -26
  8. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/SOURCES.txt +2 -0
  9. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/top_level.txt +1 -0
  10. autocython_zhang-2.3.0/MANIFEST.in +2 -0
  11. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/PKG-INFO +38 -26
  12. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/README.md +36 -3
  13. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/pyproject.toml +17 -2
  14. autocython_zhang-2.2.1/AutoCython/AutoCython.py +0 -36
  15. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython/__init__.py +0 -0
  16. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/dependency_links.txt +0 -0
  17. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/entry_points.txt +0 -0
  18. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/AutoCython_zhang.egg-info/requires.txt +0 -0
  19. {autocython_zhang-2.2.1 → autocython_zhang-2.3.0}/LICENSE +0 -0
  20. {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,4 +2,4 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  """版本号模块 - 单一版本号来源"""
4
4
 
5
- __version__ = "2.2.1"
5
+ __version__ = "2.3.0"
@@ -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, file_name)
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': True,
92
+ 'embedsignature': False,
62
93
  'wraparound': False,
63
94
  }}
64
95
 
65
96
  setup(
66
97
  ext_modules=cythonize(
67
- {repr(file_name)},
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
- result = subprocess.run(
80
- command,
81
- cwd=temp_dir,
82
- stdout=subprocess.PIPE,
83
- stderr=subprocess.PIPE
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"{module_name}*{target_ext}")
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"*{module_name}*{target_ext}")
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.getctime)
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
- def get_system_language():
16
- """
17
- 获取系统语言,兼容 Python 3.11+
14
+ from .tools import get_system_language
18
15
 
19
- :return: 'zh' 或 'en'
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 t in task_status if t["status"] == "success")
271
- failure_count = sum(1 for t in task_status if t["status"] == "failed")
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
- required_args = [f'--{a.dest}' for a in self._actions if a.required]
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
- required_group = parser.add_argument_group('required arguments')
123
- required_group.add_argument('-f', '--file', type=str, help=msg['file_help'])
124
- required_group.add_argument('-p', '--path', type=str, help=msg['path_help'])
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
- sys.exit(0)
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.2.1
3
+ Version: 2.3.0
4
4
  Summary: 自动Cython,使用Cython批量编译.py文件为.pyd文件!
5
5
  Author-email: zhang_gavin <qq814608@163.com>
6
- License: MIT License
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 True -f test.py
80
- AutoCython -d True -p D:/python_code/ProjectPath
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
@@ -0,0 +1,2 @@
1
+ global-exclude tests/*
2
+ prune tests
@@ -1,37 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AutoCython-zhang
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: 自动Cython,使用Cython批量编译.py文件为.pyd文件!
5
5
  Author-email: zhang_gavin <qq814608@163.com>
6
- License: MIT License
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 True -f test.py
80
- AutoCython -d True -p D:/python_code/ProjectPath
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 True -f test.py
39
- AutoCython -d True -p D:/python_code/ProjectPath
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 = {file = "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)