pybinaryguard 1.0.0__py3-none-any.whl

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 (91) hide show
  1. pybinaryguard/__init__.py +78 -0
  2. pybinaryguard/__main__.py +7 -0
  3. pybinaryguard/_compat/__init__.py +0 -0
  4. pybinaryguard/agent/__init__.py +63 -0
  5. pybinaryguard/agent/guard.py +232 -0
  6. pybinaryguard/agent/recommender.py +283 -0
  7. pybinaryguard/agent/schema.py +200 -0
  8. pybinaryguard/agent/simulator.py +430 -0
  9. pybinaryguard/agent/tool_interface.py +474 -0
  10. pybinaryguard/analyzers/__init__.py +209 -0
  11. pybinaryguard/analyzers/base.py +40 -0
  12. pybinaryguard/analyzers/dependency_analyzer.py +336 -0
  13. pybinaryguard/analyzers/elf_analyzer.py +754 -0
  14. pybinaryguard/analyzers/symbol_analyzer.py +280 -0
  15. pybinaryguard/analyzers/wheel_analyzer.py +308 -0
  16. pybinaryguard/cli/__init__.py +5 -0
  17. pybinaryguard/cli/commands.py +414 -0
  18. pybinaryguard/cli/formatters.py +720 -0
  19. pybinaryguard/cli/main.py +250 -0
  20. pybinaryguard/diagnostics/__init__.py +13 -0
  21. pybinaryguard/diagnostics/explainer.py +356 -0
  22. pybinaryguard/diagnostics/findings.py +146 -0
  23. pybinaryguard/diagnostics/suggestions.py +508 -0
  24. pybinaryguard/frameworks/__init__.py +20 -0
  25. pybinaryguard/frameworks/onnxruntime.py +214 -0
  26. pybinaryguard/frameworks/pytorch.py +223 -0
  27. pybinaryguard/frameworks/tensorflow.py +266 -0
  28. pybinaryguard/frameworks/tensorrt.py +189 -0
  29. pybinaryguard/models/__init__.py +19 -0
  30. pybinaryguard/models/enums.py +102 -0
  31. pybinaryguard/models/finding.py +109 -0
  32. pybinaryguard/models/package.py +121 -0
  33. pybinaryguard/models/system.py +118 -0
  34. pybinaryguard/plugins/__init__.py +19 -0
  35. pybinaryguard/plugins/contrib/__init__.py +7 -0
  36. pybinaryguard/plugins/contrib/gstreamer.py +109 -0
  37. pybinaryguard/plugins/contrib/jetson.py +385 -0
  38. pybinaryguard/plugins/contrib/opencv.py +306 -0
  39. pybinaryguard/plugins/contrib/tensorrt.py +426 -0
  40. pybinaryguard/plugins/hooks.py +273 -0
  41. pybinaryguard/plugins/loader.py +190 -0
  42. pybinaryguard/predictor/__init__.py +23 -0
  43. pybinaryguard/predictor/dependency_graph.py +226 -0
  44. pybinaryguard/predictor/linker_simulator.py +161 -0
  45. pybinaryguard/predictor/predictor.py +144 -0
  46. pybinaryguard/predictor/resolver.py +282 -0
  47. pybinaryguard/probes/__init__.py +73 -0
  48. pybinaryguard/probes/base.py +45 -0
  49. pybinaryguard/probes/board_probe.py +248 -0
  50. pybinaryguard/probes/cpu_probe.py +176 -0
  51. pybinaryguard/probes/glibc_probe.py +215 -0
  52. pybinaryguard/probes/gpu_probe.py +430 -0
  53. pybinaryguard/probes/library_probe.py +132 -0
  54. pybinaryguard/probes/os_probe.py +227 -0
  55. pybinaryguard/probes/python_probe.py +135 -0
  56. pybinaryguard/probes/toolchain_probe.py +89 -0
  57. pybinaryguard/probes/venv_probe.py +111 -0
  58. pybinaryguard/profiles/__init__.py +12 -0
  59. pybinaryguard/profiles/engine.py +343 -0
  60. pybinaryguard/rules/__init__.py +24 -0
  61. pybinaryguard/rules/base.py +57 -0
  62. pybinaryguard/rules/builtin/__init__.py +136 -0
  63. pybinaryguard/rules/builtin/arch_rules.py +86 -0
  64. pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
  65. pybinaryguard/rules/builtin/container_rules.py +170 -0
  66. pybinaryguard/rules/builtin/cpu_rules.py +204 -0
  67. pybinaryguard/rules/builtin/cuda_rules.py +760 -0
  68. pybinaryguard/rules/builtin/dependency_rules.py +278 -0
  69. pybinaryguard/rules/builtin/framework_rules.py +252 -0
  70. pybinaryguard/rules/builtin/glibc_rules.py +320 -0
  71. pybinaryguard/rules/builtin/numpy_rules.py +110 -0
  72. pybinaryguard/rules/builtin/predictive_rules.py +165 -0
  73. pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
  74. pybinaryguard/rules/builtin/source_build_rules.py +150 -0
  75. pybinaryguard/rules/builtin/venv_rules.py +159 -0
  76. pybinaryguard/rules/engine.py +123 -0
  77. pybinaryguard/scanner.py +904 -0
  78. pybinaryguard/scoring/__init__.py +19 -0
  79. pybinaryguard/scoring/engine.py +416 -0
  80. pybinaryguard/snapshot/__init__.py +20 -0
  81. pybinaryguard/snapshot/generator.py +168 -0
  82. pybinaryguard/snapshot/lockfile.py +152 -0
  83. pybinaryguard/snapshot/verifier.py +315 -0
  84. pybinaryguard/validators/__init__.py +7 -0
  85. pybinaryguard/validators/import_validator.py +231 -0
  86. pybinaryguard-1.0.0.dist-info/METADATA +876 -0
  87. pybinaryguard-1.0.0.dist-info/RECORD +91 -0
  88. pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
  89. pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
  90. pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
  91. pybinaryguard-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,200 @@
1
+ """Tool schema export for agent framework registration.
2
+
3
+ Generates tool descriptors in formats compatible with:
4
+ - OpenAI function calling (Chat Completions API)
5
+ - MCP (Model Context Protocol) tool format
6
+ - Generic JSON Schema
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from typing import Any, Dict, List
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Tool definitions
17
+ # ---------------------------------------------------------------------------
18
+
19
+ _TOOLS: List[Dict[str, Any]] = [
20
+ {
21
+ "name": "pybinaryguard_scan",
22
+ "description": (
23
+ "Scan the current Python environment for binary compatibility "
24
+ "issues. Returns a structured report with health score, findings, "
25
+ "and recommended fix actions. Use this before deploying or after "
26
+ "installing new packages."
27
+ ),
28
+ "parameters": {
29
+ "type": "object",
30
+ "properties": {
31
+ "scan_mode": {
32
+ "type": "string",
33
+ "enum": ["fast", "standard", "deep"],
34
+ "description": (
35
+ "Scan depth. 'fast' checks metadata only (<1s). "
36
+ "'standard' parses binaries. 'deep' adds hash "
37
+ "verification and full symbol resolution."
38
+ ),
39
+ "default": "standard",
40
+ },
41
+ "packages": {
42
+ "type": "array",
43
+ "items": {"type": "string"},
44
+ "description": "Restrict scan to these package names. Empty means scan all.",
45
+ },
46
+ "severity_threshold": {
47
+ "type": "string",
48
+ "enum": ["critical", "warning", "info", "all"],
49
+ "description": "Minimum severity to include in results.",
50
+ "default": "all",
51
+ },
52
+ },
53
+ "required": [],
54
+ },
55
+ },
56
+ {
57
+ "name": "pybinaryguard_check",
58
+ "description": (
59
+ "Check a specific installed Python package for binary "
60
+ "compatibility issues with the current system."
61
+ ),
62
+ "parameters": {
63
+ "type": "object",
64
+ "properties": {
65
+ "package": {
66
+ "type": "string",
67
+ "description": "Package name to check (e.g. 'torch', 'numpy').",
68
+ },
69
+ },
70
+ "required": ["package"],
71
+ },
72
+ },
73
+ {
74
+ "name": "pybinaryguard_simulate_install",
75
+ "description": (
76
+ "Predict whether a package will be compatible with the current "
77
+ "system BEFORE installing it. Checks architecture, GLIBC version, "
78
+ "CUDA compatibility, and Python ABI from the wheel filename."
79
+ ),
80
+ "parameters": {
81
+ "type": "object",
82
+ "properties": {
83
+ "package_spec": {
84
+ "type": "string",
85
+ "description": (
86
+ "Package specifier. Can be a package name ('torch'), "
87
+ "a version pin ('torch==2.4.0'), or a wheel filename "
88
+ "('torch-2.4.0+cu124-cp312-cp312-manylinux_2_17_x86_64.whl')."
89
+ ),
90
+ },
91
+ },
92
+ "required": ["package_spec"],
93
+ },
94
+ },
95
+ {
96
+ "name": "pybinaryguard_doctor",
97
+ "description": (
98
+ "Diagnose a specific error message. Paste a Python traceback or "
99
+ "error string and get a structured diagnosis with fix plan."
100
+ ),
101
+ "parameters": {
102
+ "type": "object",
103
+ "properties": {
104
+ "error_message": {
105
+ "type": "string",
106
+ "description": "The error message or traceback to diagnose.",
107
+ },
108
+ "package": {
109
+ "type": "string",
110
+ "description": "Optional package name related to the error.",
111
+ },
112
+ },
113
+ "required": ["error_message"],
114
+ },
115
+ },
116
+ {
117
+ "name": "pybinaryguard_profile",
118
+ "description": (
119
+ "Get the current system's binary compatibility profile: Python "
120
+ "version, architecture, GLIBC, CUDA stack, GPU info, and board "
121
+ "detection. Use this to understand the environment."
122
+ ),
123
+ "parameters": {
124
+ "type": "object",
125
+ "properties": {},
126
+ "required": [],
127
+ },
128
+ },
129
+ ]
130
+
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # Export functions
134
+ # ---------------------------------------------------------------------------
135
+
136
+ def get_tool_descriptors(format: str = "openai") -> List[Dict[str, Any]]:
137
+ """Get tool descriptors in the specified format.
138
+
139
+ Parameters
140
+ ----------
141
+ format:
142
+ Output format: "openai", "mcp", or "json_schema".
143
+
144
+ Returns
145
+ -------
146
+ List[Dict[str, Any]]
147
+ Tool descriptors ready for agent framework registration.
148
+ """
149
+ if format == "openai":
150
+ return _to_openai_format()
151
+ elif format == "mcp":
152
+ return _to_mcp_format()
153
+ elif format == "json_schema":
154
+ return _TOOLS
155
+ else:
156
+ raise ValueError(f"Unknown format: {format!r}. Use 'openai', 'mcp', or 'json_schema'.")
157
+
158
+
159
+ def export_tool_schema(format: str = "openai") -> str:
160
+ """Export tool schemas as a JSON string.
161
+
162
+ Parameters
163
+ ----------
164
+ format:
165
+ Output format: "openai", "mcp", or "json_schema".
166
+
167
+ Returns
168
+ -------
169
+ str
170
+ JSON string of tool descriptors.
171
+ """
172
+ descriptors = get_tool_descriptors(format)
173
+ return json.dumps(descriptors, indent=2)
174
+
175
+
176
+ def _to_openai_format() -> List[Dict[str, Any]]:
177
+ """Convert to OpenAI function calling format."""
178
+ tools = []
179
+ for tool in _TOOLS:
180
+ tools.append({
181
+ "type": "function",
182
+ "function": {
183
+ "name": tool["name"],
184
+ "description": tool["description"],
185
+ "parameters": tool["parameters"],
186
+ },
187
+ })
188
+ return tools
189
+
190
+
191
+ def _to_mcp_format() -> List[Dict[str, Any]]:
192
+ """Convert to MCP (Model Context Protocol) tool format."""
193
+ tools = []
194
+ for tool in _TOOLS:
195
+ tools.append({
196
+ "name": tool["name"],
197
+ "description": tool["description"],
198
+ "inputSchema": tool["parameters"],
199
+ })
200
+ return tools
@@ -0,0 +1,430 @@
1
+ """Pre-install compatibility simulator.
2
+
3
+ Predicts whether a package will work on the current system BEFORE
4
+ ``pip install``. Parses wheel filenames / version specifiers and
5
+ checks against the live SystemProfile.
6
+
7
+ No network calls — works fully offline.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from dataclasses import dataclass
14
+ from typing import Dict, List, Optional, Tuple
15
+
16
+ from pybinaryguard.agent.tool_interface import AgentSimulateResult
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Wheel filename parser
21
+ # ---------------------------------------------------------------------------
22
+
23
+ # Pattern: name-version(-build)?-python-abi-platform.whl
24
+ _WHEEL_RE = re.compile(
25
+ r"^(?P<name>[A-Za-z0-9]([A-Za-z0-9._]*[A-Za-z0-9])?)"
26
+ r"-(?P<version>[^-]+)"
27
+ r"(-(?P<build>\d[^-]*))?"
28
+ r"-(?P<python>[^-]+)"
29
+ r"-(?P<abi>[^-]+)"
30
+ r"-(?P<platform>[^-]+)"
31
+ r"\.whl$"
32
+ )
33
+
34
+ # CUDA variant in version: +cu118, +cu121, +cu124
35
+ _CUDA_VARIANT_RE = re.compile(r"\+cu(\d{2,3})")
36
+
37
+ # manylinux tag: manylinux_2_17_x86_64 or manylinux2014_x86_64
38
+ _MANYLINUX_RE = re.compile(r"manylinux_(\d+)_(\d+)_(\w+)")
39
+ _MANYLINUX_LEGACY = {
40
+ "manylinux1": ((2, 5), None),
41
+ "manylinux2010": ((2, 12), None),
42
+ "manylinux2014": ((2, 17), None),
43
+ }
44
+
45
+ # Architecture mapping from platform tag
46
+ _PLATFORM_ARCH = {
47
+ "x86_64": "x86_64",
48
+ "amd64": "x86_64",
49
+ "aarch64": "aarch64",
50
+ "arm64": "aarch64",
51
+ "armv7l": "armv7l",
52
+ "i686": "i686",
53
+ "ppc64le": "ppc64le",
54
+ "s390x": "s390x",
55
+ }
56
+
57
+
58
+ @dataclass
59
+ class ParsedWheel:
60
+ """Parsed wheel filename components."""
61
+
62
+ name: str
63
+ version: str
64
+ python_tag: str # e.g. "cp312"
65
+ abi_tag: str # e.g. "cp312"
66
+ platform_tag: str # e.g. "manylinux_2_17_x86_64"
67
+ cuda_variant: Optional[Tuple[int, int]] = None # (12, 4) from +cu124
68
+ required_glibc: Optional[Tuple[int, int]] = None
69
+ target_arch: Optional[str] = None
70
+
71
+
72
+ def _parse_wheel_filename(filename: str) -> Optional[ParsedWheel]:
73
+ """Parse a wheel filename into structured components."""
74
+ m = _WHEEL_RE.match(filename)
75
+ if not m:
76
+ return None
77
+
78
+ name = m.group("name")
79
+ version = m.group("version")
80
+ python_tag = m.group("python")
81
+ abi_tag = m.group("abi")
82
+ platform_tag = m.group("platform")
83
+
84
+ # Extract CUDA variant from version
85
+ cuda_variant = None
86
+ cuda_m = _CUDA_VARIANT_RE.search(version)
87
+ if cuda_m:
88
+ cu_str = cuda_m.group(1)
89
+ if len(cu_str) == 2:
90
+ cuda_variant = (int(cu_str[0]), int(cu_str[1]))
91
+ elif len(cu_str) == 3:
92
+ cuda_variant = (int(cu_str[:2]), int(cu_str[2]))
93
+
94
+ # Parse manylinux GLIBC requirement
95
+ required_glibc = None
96
+ target_arch = None
97
+ ml_m = _MANYLINUX_RE.search(platform_tag)
98
+ if ml_m:
99
+ required_glibc = (int(ml_m.group(1)), int(ml_m.group(2)))
100
+ target_arch = _PLATFORM_ARCH.get(ml_m.group(3))
101
+ else:
102
+ # Check legacy tags
103
+ for prefix, (glibc, _) in _MANYLINUX_LEGACY.items():
104
+ if platform_tag.startswith(prefix):
105
+ required_glibc = glibc
106
+ # Try to extract arch from rest of tag
107
+ rest = platform_tag[len(prefix):]
108
+ if rest.startswith("_"):
109
+ rest = rest[1:]
110
+ target_arch = _PLATFORM_ARCH.get(rest)
111
+ break
112
+
113
+ # If no manylinux, check for direct arch in platform
114
+ if target_arch is None:
115
+ for arch_str, arch_val in _PLATFORM_ARCH.items():
116
+ if arch_str in platform_tag.lower():
117
+ target_arch = arch_val
118
+ break
119
+
120
+ return ParsedWheel(
121
+ name=name,
122
+ version=version,
123
+ python_tag=python_tag,
124
+ abi_tag=abi_tag,
125
+ platform_tag=platform_tag,
126
+ cuda_variant=cuda_variant,
127
+ required_glibc=required_glibc,
128
+ target_arch=target_arch,
129
+ )
130
+
131
+
132
+ def _parse_version_spec(spec: str) -> Tuple[str, str, Optional[Tuple[int, int]]]:
133
+ """Parse 'torch==2.4.0+cu124' into (name, version, cuda_variant)."""
134
+ # Split on version operators
135
+ for op in ("==", ">=", "<=", "!=", "~=", ">", "<"):
136
+ if op in spec:
137
+ name, version = spec.split(op, 1)
138
+ cuda_variant = None
139
+ cuda_m = _CUDA_VARIANT_RE.search(version)
140
+ if cuda_m:
141
+ cu_str = cuda_m.group(1)
142
+ if len(cu_str) == 2:
143
+ cuda_variant = (int(cu_str[0]), int(cu_str[1]))
144
+ elif len(cu_str) == 3:
145
+ cuda_variant = (int(cu_str[:2]), int(cu_str[2]))
146
+ return name.strip(), version.strip(), cuda_variant
147
+
148
+ return spec.strip(), "", None
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # Compatibility checks
153
+ # ---------------------------------------------------------------------------
154
+
155
+ def _check_python_compat(
156
+ python_tag: str,
157
+ system_python: Tuple[int, int, int],
158
+ ) -> Optional[Dict[str, object]]:
159
+ """Check Python version compatibility from wheel tag."""
160
+ if python_tag in ("py3", "py2.py3", "none"):
161
+ return None # Universal wheel
162
+
163
+ # Parse cp312 -> (3, 12)
164
+ m = re.match(r"cp(\d)(\d+)", python_tag)
165
+ if not m:
166
+ return None
167
+
168
+ wheel_major = int(m.group(1))
169
+ wheel_minor = int(m.group(2))
170
+
171
+ if (wheel_major, wheel_minor) != (system_python[0], system_python[1]):
172
+ return {
173
+ "type": "python_version_mismatch",
174
+ "severity": "critical",
175
+ "message": (
176
+ f"Wheel requires Python {wheel_major}.{wheel_minor} "
177
+ f"but system has {system_python[0]}.{system_python[1]}"
178
+ ),
179
+ "wheel_python": f"{wheel_major}.{wheel_minor}",
180
+ "system_python": f"{system_python[0]}.{system_python[1]}",
181
+ }
182
+ return None
183
+
184
+
185
+ def _check_arch_compat(
186
+ target_arch: Optional[str],
187
+ system_arch: str,
188
+ ) -> Optional[Dict[str, object]]:
189
+ """Check architecture compatibility."""
190
+ if target_arch is None:
191
+ return None
192
+ if target_arch == system_arch:
193
+ return None
194
+
195
+ return {
196
+ "type": "architecture_mismatch",
197
+ "severity": "critical",
198
+ "message": (
199
+ f"Wheel built for {target_arch} but system is {system_arch}"
200
+ ),
201
+ "wheel_arch": target_arch,
202
+ "system_arch": system_arch,
203
+ }
204
+
205
+
206
+ def _check_glibc_compat(
207
+ required_glibc: Optional[Tuple[int, int]],
208
+ system_glibc: Optional[Tuple[int, int]],
209
+ ) -> Optional[Dict[str, object]]:
210
+ """Check GLIBC version compatibility."""
211
+ if required_glibc is None or system_glibc is None:
212
+ return None
213
+
214
+ if required_glibc > system_glibc:
215
+ return {
216
+ "type": "glibc_too_old",
217
+ "severity": "critical",
218
+ "message": (
219
+ f"Wheel requires GLIBC {required_glibc[0]}.{required_glibc[1]} "
220
+ f"but system has {system_glibc[0]}.{system_glibc[1]}"
221
+ ),
222
+ "required": f"{required_glibc[0]}.{required_glibc[1]}",
223
+ "system": f"{system_glibc[0]}.{system_glibc[1]}",
224
+ }
225
+ return None
226
+
227
+
228
+ def _check_cuda_compat(
229
+ cuda_variant: Optional[Tuple[int, int]],
230
+ system_cuda: Optional[Tuple[int, int]],
231
+ gpu_available: bool,
232
+ ) -> Optional[Dict[str, object]]:
233
+ """Check CUDA version compatibility."""
234
+ if cuda_variant is None:
235
+ return None # Not a CUDA wheel
236
+
237
+ if not gpu_available:
238
+ return {
239
+ "type": "no_gpu",
240
+ "severity": "warning",
241
+ "message": (
242
+ f"Wheel is CUDA-enabled (cu{cuda_variant[0]}{cuda_variant[1]}) "
243
+ f"but no GPU detected on this system"
244
+ ),
245
+ "wheel_cuda": f"{cuda_variant[0]}.{cuda_variant[1]}",
246
+ }
247
+
248
+ if system_cuda is None:
249
+ return {
250
+ "type": "no_cuda_runtime",
251
+ "severity": "critical",
252
+ "message": (
253
+ f"Wheel requires CUDA {cuda_variant[0]}.{cuda_variant[1]} "
254
+ f"but no CUDA runtime detected"
255
+ ),
256
+ "wheel_cuda": f"{cuda_variant[0]}.{cuda_variant[1]}",
257
+ }
258
+
259
+ # CUDA major version must match, minor can be >= wheel's
260
+ if cuda_variant[0] != system_cuda[0]:
261
+ return {
262
+ "type": "cuda_major_mismatch",
263
+ "severity": "critical",
264
+ "message": (
265
+ f"Wheel built for CUDA {cuda_variant[0]}.{cuda_variant[1]} "
266
+ f"but system has CUDA {system_cuda[0]}.{system_cuda[1]}"
267
+ ),
268
+ "wheel_cuda": f"{cuda_variant[0]}.{cuda_variant[1]}",
269
+ "system_cuda": f"{system_cuda[0]}.{system_cuda[1]}",
270
+ }
271
+
272
+ if cuda_variant[1] > system_cuda[1]:
273
+ return {
274
+ "type": "cuda_minor_mismatch",
275
+ "severity": "warning",
276
+ "message": (
277
+ f"Wheel built for CUDA {cuda_variant[0]}.{cuda_variant[1]} "
278
+ f"but system has {system_cuda[0]}.{system_cuda[1]} — "
279
+ f"minor version mismatch may cause issues"
280
+ ),
281
+ "wheel_cuda": f"{cuda_variant[0]}.{cuda_variant[1]}",
282
+ "system_cuda": f"{system_cuda[0]}.{system_cuda[1]}",
283
+ }
284
+
285
+ return None
286
+
287
+
288
+ def _check_musl_compat(
289
+ platform_tag: str,
290
+ system_glibc: Optional[Tuple[int, int]],
291
+ system_musl: Optional[Tuple[int, int]],
292
+ ) -> Optional[Dict[str, object]]:
293
+ """Check musl vs glibc compatibility."""
294
+ is_musllinux = "musllinux" in platform_tag
295
+ is_manylinux = "manylinux" in platform_tag
296
+
297
+ if is_manylinux and system_musl is not None and system_glibc is None:
298
+ return {
299
+ "type": "glibc_on_musl",
300
+ "severity": "critical",
301
+ "message": (
302
+ "Wheel is glibc-linked (manylinux) but system uses musl "
303
+ "(Alpine Linux). Build from source instead."
304
+ ),
305
+ "fix_hint": "pip install --no-binary :all: <package>",
306
+ }
307
+
308
+ if is_musllinux and system_musl is None:
309
+ return {
310
+ "type": "musl_on_glibc",
311
+ "severity": "critical",
312
+ "message": (
313
+ "Wheel is musl-linked (musllinux) but system uses glibc."
314
+ ),
315
+ }
316
+
317
+ return None
318
+
319
+
320
+ # ---------------------------------------------------------------------------
321
+ # Main simulate function
322
+ # ---------------------------------------------------------------------------
323
+
324
+ def simulate(package_spec: str) -> AgentSimulateResult:
325
+ """Run pre-install compatibility simulation.
326
+
327
+ Parameters
328
+ ----------
329
+ package_spec:
330
+ Package name, version pin, or wheel filename.
331
+
332
+ Returns
333
+ -------
334
+ AgentSimulateResult
335
+ Structured prediction result.
336
+ """
337
+ from pybinaryguard.scanner import Scanner
338
+
339
+ # Collect system profile
340
+ scanner = Scanner(timeout=10.0)
341
+ profile = scanner.get_profile()
342
+
343
+ blockers: List[Dict[str, object]] = []
344
+ warnings: List[Dict[str, object]] = []
345
+ parsed_tags: Optional[Dict[str, str]] = None
346
+ confidence = 0.5 # Base confidence for name-only
347
+
348
+ # Try parsing as wheel filename
349
+ wheel = _parse_wheel_filename(package_spec)
350
+
351
+ if wheel is not None:
352
+ confidence = 0.95 # Wheel filenames are highly informative
353
+ parsed_tags = {
354
+ "name": wheel.name,
355
+ "version": wheel.version,
356
+ "python": wheel.python_tag,
357
+ "abi": wheel.abi_tag,
358
+ "platform": wheel.platform_tag,
359
+ }
360
+ if wheel.cuda_variant:
361
+ parsed_tags["cuda"] = f"{wheel.cuda_variant[0]}.{wheel.cuda_variant[1]}"
362
+
363
+ # Run all checks
364
+ checks = [
365
+ _check_python_compat(wheel.python_tag, profile.python_version),
366
+ _check_arch_compat(wheel.target_arch, profile.architecture.value),
367
+ _check_glibc_compat(wheel.required_glibc, profile.glibc_version),
368
+ _check_cuda_compat(
369
+ wheel.cuda_variant,
370
+ profile.cuda_runtime_version,
371
+ profile.gpu_available,
372
+ ),
373
+ _check_musl_compat(
374
+ wheel.platform_tag,
375
+ profile.glibc_version,
376
+ profile.musl_version,
377
+ ),
378
+ ]
379
+
380
+ for check_result in checks:
381
+ if check_result is None:
382
+ continue
383
+ if check_result["severity"] == "critical":
384
+ blockers.append(check_result)
385
+ else:
386
+ warnings.append(check_result)
387
+
388
+ else:
389
+ # Parse as name or name==version
390
+ name, version, cuda_variant = _parse_version_spec(package_spec)
391
+ parsed_tags = {"name": name}
392
+ if version:
393
+ parsed_tags["version"] = version
394
+ confidence = 0.6
395
+
396
+ if cuda_variant:
397
+ parsed_tags["cuda"] = f"{cuda_variant[0]}.{cuda_variant[1]}"
398
+ confidence = 0.7
399
+
400
+ cuda_check = _check_cuda_compat(
401
+ cuda_variant,
402
+ profile.cuda_runtime_version,
403
+ profile.gpu_available,
404
+ )
405
+ if cuda_check:
406
+ if cuda_check["severity"] == "critical":
407
+ blockers.append(cuda_check)
408
+ else:
409
+ warnings.append(cuda_check)
410
+
411
+ # Determine overall prediction
412
+ predicted_compatible = len(blockers) == 0
413
+
414
+ # Risk level
415
+ if blockers:
416
+ risk_level = "critical" if len(blockers) >= 2 else "high"
417
+ elif warnings:
418
+ risk_level = "medium" if len(warnings) >= 2 else "low"
419
+ else:
420
+ risk_level = "none"
421
+
422
+ return AgentSimulateResult(
423
+ package_spec=package_spec,
424
+ predicted_compatible=predicted_compatible,
425
+ confidence=confidence,
426
+ risk_level=risk_level,
427
+ warnings=warnings,
428
+ blockers=blockers,
429
+ parsed_tags=parsed_tags,
430
+ )