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.
- pybinaryguard/__init__.py +78 -0
- pybinaryguard/__main__.py +7 -0
- pybinaryguard/_compat/__init__.py +0 -0
- pybinaryguard/agent/__init__.py +63 -0
- pybinaryguard/agent/guard.py +232 -0
- pybinaryguard/agent/recommender.py +283 -0
- pybinaryguard/agent/schema.py +200 -0
- pybinaryguard/agent/simulator.py +430 -0
- pybinaryguard/agent/tool_interface.py +474 -0
- pybinaryguard/analyzers/__init__.py +209 -0
- pybinaryguard/analyzers/base.py +40 -0
- pybinaryguard/analyzers/dependency_analyzer.py +336 -0
- pybinaryguard/analyzers/elf_analyzer.py +754 -0
- pybinaryguard/analyzers/symbol_analyzer.py +280 -0
- pybinaryguard/analyzers/wheel_analyzer.py +308 -0
- pybinaryguard/cli/__init__.py +5 -0
- pybinaryguard/cli/commands.py +414 -0
- pybinaryguard/cli/formatters.py +720 -0
- pybinaryguard/cli/main.py +250 -0
- pybinaryguard/diagnostics/__init__.py +13 -0
- pybinaryguard/diagnostics/explainer.py +356 -0
- pybinaryguard/diagnostics/findings.py +146 -0
- pybinaryguard/diagnostics/suggestions.py +508 -0
- pybinaryguard/frameworks/__init__.py +20 -0
- pybinaryguard/frameworks/onnxruntime.py +214 -0
- pybinaryguard/frameworks/pytorch.py +223 -0
- pybinaryguard/frameworks/tensorflow.py +266 -0
- pybinaryguard/frameworks/tensorrt.py +189 -0
- pybinaryguard/models/__init__.py +19 -0
- pybinaryguard/models/enums.py +102 -0
- pybinaryguard/models/finding.py +109 -0
- pybinaryguard/models/package.py +121 -0
- pybinaryguard/models/system.py +118 -0
- pybinaryguard/plugins/__init__.py +19 -0
- pybinaryguard/plugins/contrib/__init__.py +7 -0
- pybinaryguard/plugins/contrib/gstreamer.py +109 -0
- pybinaryguard/plugins/contrib/jetson.py +385 -0
- pybinaryguard/plugins/contrib/opencv.py +306 -0
- pybinaryguard/plugins/contrib/tensorrt.py +426 -0
- pybinaryguard/plugins/hooks.py +273 -0
- pybinaryguard/plugins/loader.py +190 -0
- pybinaryguard/predictor/__init__.py +23 -0
- pybinaryguard/predictor/dependency_graph.py +226 -0
- pybinaryguard/predictor/linker_simulator.py +161 -0
- pybinaryguard/predictor/predictor.py +144 -0
- pybinaryguard/predictor/resolver.py +282 -0
- pybinaryguard/probes/__init__.py +73 -0
- pybinaryguard/probes/base.py +45 -0
- pybinaryguard/probes/board_probe.py +248 -0
- pybinaryguard/probes/cpu_probe.py +176 -0
- pybinaryguard/probes/glibc_probe.py +215 -0
- pybinaryguard/probes/gpu_probe.py +430 -0
- pybinaryguard/probes/library_probe.py +132 -0
- pybinaryguard/probes/os_probe.py +227 -0
- pybinaryguard/probes/python_probe.py +135 -0
- pybinaryguard/probes/toolchain_probe.py +89 -0
- pybinaryguard/probes/venv_probe.py +111 -0
- pybinaryguard/profiles/__init__.py +12 -0
- pybinaryguard/profiles/engine.py +343 -0
- pybinaryguard/rules/__init__.py +24 -0
- pybinaryguard/rules/base.py +57 -0
- pybinaryguard/rules/builtin/__init__.py +136 -0
- pybinaryguard/rules/builtin/arch_rules.py +86 -0
- pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
- pybinaryguard/rules/builtin/container_rules.py +170 -0
- pybinaryguard/rules/builtin/cpu_rules.py +204 -0
- pybinaryguard/rules/builtin/cuda_rules.py +760 -0
- pybinaryguard/rules/builtin/dependency_rules.py +278 -0
- pybinaryguard/rules/builtin/framework_rules.py +252 -0
- pybinaryguard/rules/builtin/glibc_rules.py +320 -0
- pybinaryguard/rules/builtin/numpy_rules.py +110 -0
- pybinaryguard/rules/builtin/predictive_rules.py +165 -0
- pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
- pybinaryguard/rules/builtin/source_build_rules.py +150 -0
- pybinaryguard/rules/builtin/venv_rules.py +159 -0
- pybinaryguard/rules/engine.py +123 -0
- pybinaryguard/scanner.py +904 -0
- pybinaryguard/scoring/__init__.py +19 -0
- pybinaryguard/scoring/engine.py +416 -0
- pybinaryguard/snapshot/__init__.py +20 -0
- pybinaryguard/snapshot/generator.py +168 -0
- pybinaryguard/snapshot/lockfile.py +152 -0
- pybinaryguard/snapshot/verifier.py +315 -0
- pybinaryguard/validators/__init__.py +7 -0
- pybinaryguard/validators/import_validator.py +231 -0
- pybinaryguard-1.0.0.dist-info/METADATA +876 -0
- pybinaryguard-1.0.0.dist-info/RECORD +91 -0
- pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
- pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
- pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|