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,250 @@
|
|
|
1
|
+
"""CLI entry point for PyBinaryGuard.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
pybinaryguard scan
|
|
6
|
+
pybinaryguard check torch
|
|
7
|
+
pybinaryguard profile
|
|
8
|
+
pybinaryguard doctor --error "GLIBC_2.34 not found"
|
|
9
|
+
pybinaryguard inspect wheel.whl
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from typing import List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
20
|
+
"""Build the argument parser with all commands and options."""
|
|
21
|
+
# Parent parser for shared global options
|
|
22
|
+
parent = argparse.ArgumentParser(add_help=False)
|
|
23
|
+
parent.add_argument(
|
|
24
|
+
"--format",
|
|
25
|
+
choices=["table", "json", "minimal"],
|
|
26
|
+
default="table",
|
|
27
|
+
help="Output format (default: table)",
|
|
28
|
+
)
|
|
29
|
+
parent.add_argument(
|
|
30
|
+
"--severity",
|
|
31
|
+
choices=["critical", "warning", "info", "all"],
|
|
32
|
+
default="all",
|
|
33
|
+
help="Minimum severity to show (default: all)",
|
|
34
|
+
)
|
|
35
|
+
parent.add_argument(
|
|
36
|
+
"--no-color",
|
|
37
|
+
action="store_true",
|
|
38
|
+
help="Disable coloured output",
|
|
39
|
+
)
|
|
40
|
+
parent.add_argument(
|
|
41
|
+
"--verbose", "-v",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="Show technical details and all suggestions",
|
|
44
|
+
)
|
|
45
|
+
parent.add_argument(
|
|
46
|
+
"--quiet", "-q",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="Only show critical findings",
|
|
49
|
+
)
|
|
50
|
+
parent.add_argument(
|
|
51
|
+
"--ci",
|
|
52
|
+
action="store_true",
|
|
53
|
+
help="CI mode: minimal output + strict exit codes",
|
|
54
|
+
)
|
|
55
|
+
parent.add_argument(
|
|
56
|
+
"--timeout",
|
|
57
|
+
type=float,
|
|
58
|
+
default=30.0,
|
|
59
|
+
help="Max scan time in seconds (default: 30)",
|
|
60
|
+
)
|
|
61
|
+
parent.add_argument(
|
|
62
|
+
"--ignore",
|
|
63
|
+
nargs="*",
|
|
64
|
+
default=[],
|
|
65
|
+
metavar="RULE_ID",
|
|
66
|
+
help="Rule IDs to ignore (e.g. CUDA_MINOR_MISMATCH)",
|
|
67
|
+
)
|
|
68
|
+
mode_group = parent.add_mutually_exclusive_group()
|
|
69
|
+
mode_group.add_argument(
|
|
70
|
+
"--fast",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="Fast scan: metadata only, skip binary analysis (<1s)",
|
|
73
|
+
)
|
|
74
|
+
mode_group.add_argument(
|
|
75
|
+
"--deep",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help="Deep scan: full symbol resolution + hash verification",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Main parser
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
prog="pybinaryguard",
|
|
83
|
+
description="PyBinaryGuard -- Binary Compatibility Scanner for Python",
|
|
84
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
85
|
+
parents=[parent],
|
|
86
|
+
epilog=(
|
|
87
|
+
"Examples:\n"
|
|
88
|
+
" pybinaryguard scan Full environment scan\n"
|
|
89
|
+
" pybinaryguard check torch Check a specific package\n"
|
|
90
|
+
" pybinaryguard profile Show system profile\n"
|
|
91
|
+
" pybinaryguard doctor --error 'GLIBC_2.34 not found'\n"
|
|
92
|
+
" pybinaryguard inspect wheel.whl Analyse a wheel file\n"
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--version",
|
|
97
|
+
action="version",
|
|
98
|
+
version="%(prog)s 1.0.0",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
102
|
+
|
|
103
|
+
# scan
|
|
104
|
+
subparsers.add_parser(
|
|
105
|
+
"scan", parents=[parent],
|
|
106
|
+
help="Full environment scan -- check everything",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# check
|
|
110
|
+
check_parser = subparsers.add_parser(
|
|
111
|
+
"check", parents=[parent],
|
|
112
|
+
help="Check a specific installed package",
|
|
113
|
+
)
|
|
114
|
+
check_parser.add_argument("package", help="Package name to check")
|
|
115
|
+
|
|
116
|
+
# profile
|
|
117
|
+
subparsers.add_parser(
|
|
118
|
+
"profile", parents=[parent],
|
|
119
|
+
help="Show system profile (no compatibility checks)",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# doctor
|
|
123
|
+
doctor_parser = subparsers.add_parser(
|
|
124
|
+
"doctor", parents=[parent],
|
|
125
|
+
help="Interactive troubleshooting",
|
|
126
|
+
)
|
|
127
|
+
doctor_parser.add_argument("--error", help="Error message to diagnose")
|
|
128
|
+
doctor_parser.add_argument("--package", help="Package that fails")
|
|
129
|
+
|
|
130
|
+
# inspect
|
|
131
|
+
inspect_parser = subparsers.add_parser(
|
|
132
|
+
"inspect", parents=[parent],
|
|
133
|
+
help="Analyse a .whl or .so file",
|
|
134
|
+
)
|
|
135
|
+
inspect_parser.add_argument("file", help="Path to .whl or .so file")
|
|
136
|
+
|
|
137
|
+
# snapshot
|
|
138
|
+
snapshot_parser = subparsers.add_parser(
|
|
139
|
+
"snapshot", parents=[parent],
|
|
140
|
+
help="Create environment snapshot (lockfile)",
|
|
141
|
+
)
|
|
142
|
+
snapshot_parser.add_argument(
|
|
143
|
+
"-o", "--output",
|
|
144
|
+
help="Output file (default: stdout)",
|
|
145
|
+
)
|
|
146
|
+
snapshot_parser.add_argument(
|
|
147
|
+
"--no-hashes",
|
|
148
|
+
action="store_true",
|
|
149
|
+
help="Skip computing binary hashes (faster)",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# verify
|
|
153
|
+
verify_parser = subparsers.add_parser(
|
|
154
|
+
"verify", parents=[parent],
|
|
155
|
+
help="Verify environment against snapshot",
|
|
156
|
+
)
|
|
157
|
+
verify_parser.add_argument(
|
|
158
|
+
"lockfile",
|
|
159
|
+
help="Path to lockfile to verify against",
|
|
160
|
+
)
|
|
161
|
+
verify_parser.add_argument(
|
|
162
|
+
"--no-hashes",
|
|
163
|
+
action="store_true",
|
|
164
|
+
help="Skip hash verification",
|
|
165
|
+
)
|
|
166
|
+
verify_parser.add_argument(
|
|
167
|
+
"--no-compat-checks",
|
|
168
|
+
action="store_true",
|
|
169
|
+
help="Skip compatibility checks",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# export-tool-schema (agent SDK)
|
|
173
|
+
schema_parser = subparsers.add_parser(
|
|
174
|
+
"export-tool-schema", parents=[parent],
|
|
175
|
+
help="Export tool schema for agent framework registration",
|
|
176
|
+
)
|
|
177
|
+
schema_parser.add_argument(
|
|
178
|
+
"--schema-format",
|
|
179
|
+
choices=["openai", "mcp", "json_schema"],
|
|
180
|
+
default="openai",
|
|
181
|
+
help="Schema format (default: openai)",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# simulate (agent SDK)
|
|
185
|
+
simulate_parser = subparsers.add_parser(
|
|
186
|
+
"simulate", parents=[parent],
|
|
187
|
+
help="Predict package compatibility before installing",
|
|
188
|
+
)
|
|
189
|
+
simulate_parser.add_argument(
|
|
190
|
+
"package_spec",
|
|
191
|
+
help="Package specifier or wheel filename to simulate",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# validate (import testing)
|
|
195
|
+
validate_parser = subparsers.add_parser(
|
|
196
|
+
"validate", parents=[parent],
|
|
197
|
+
help="Test actual imports in isolated subprocesses",
|
|
198
|
+
)
|
|
199
|
+
validate_parser.add_argument(
|
|
200
|
+
"--packages",
|
|
201
|
+
nargs="*",
|
|
202
|
+
metavar="PKG",
|
|
203
|
+
help="Specific packages to test (default: all with binaries)",
|
|
204
|
+
)
|
|
205
|
+
validate_parser.add_argument(
|
|
206
|
+
"--import-timeout",
|
|
207
|
+
type=float,
|
|
208
|
+
default=10.0,
|
|
209
|
+
help="Timeout per import test in seconds (default: 10)",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return parser
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
216
|
+
"""Main CLI entry point.
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
-------
|
|
220
|
+
int
|
|
221
|
+
Exit code: 0 = pass, 1 = warnings, 2 = critical, 3 = scanner error.
|
|
222
|
+
"""
|
|
223
|
+
parser = _build_parser()
|
|
224
|
+
args = parser.parse_args(argv)
|
|
225
|
+
|
|
226
|
+
# Default to scan if no command given
|
|
227
|
+
if not args.command:
|
|
228
|
+
args.command = "scan"
|
|
229
|
+
|
|
230
|
+
# Apply CI mode overrides
|
|
231
|
+
if args.ci:
|
|
232
|
+
args.format = "minimal"
|
|
233
|
+
if args.severity == "all":
|
|
234
|
+
args.severity = "critical"
|
|
235
|
+
args.no_color = True
|
|
236
|
+
|
|
237
|
+
# Apply quiet mode
|
|
238
|
+
if args.quiet:
|
|
239
|
+
args.severity = "critical"
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
from pybinaryguard.cli.commands import dispatch
|
|
243
|
+
|
|
244
|
+
return dispatch(args)
|
|
245
|
+
except KeyboardInterrupt:
|
|
246
|
+
print("\nInterrupted.", file=sys.stderr)
|
|
247
|
+
return 130
|
|
248
|
+
except Exception as exc:
|
|
249
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
250
|
+
return 3
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Diagnostics subsystem -- error diagnosis, explanation, and fix suggestions."""
|
|
2
|
+
|
|
3
|
+
from pybinaryguard.diagnostics.explainer import diagnose_error, explain_finding
|
|
4
|
+
from pybinaryguard.diagnostics.findings import filter_findings, sort_findings
|
|
5
|
+
from pybinaryguard.diagnostics.suggestions import suggest_fix
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"diagnose_error",
|
|
9
|
+
"explain_finding",
|
|
10
|
+
"filter_findings",
|
|
11
|
+
"sort_findings",
|
|
12
|
+
"suggest_fix",
|
|
13
|
+
]
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Maps error messages and rule IDs to plain-English explanations.
|
|
2
|
+
|
|
3
|
+
This module provides the ``ErrorPatternDB`` class which matches raw error
|
|
4
|
+
messages against a curated database of regex patterns and returns
|
|
5
|
+
beginner-friendly diagnoses including root cause, explanation, and fix hints.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from pybinaryguard.models.finding import Finding
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Error-pattern database
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
ERROR_PATTERNS: List[Dict[str, str]] = [
|
|
23
|
+
{
|
|
24
|
+
"pattern": r"version .GLIBC_(\d+\.\d+). not found",
|
|
25
|
+
"root_cause": "GLIBC version too old",
|
|
26
|
+
"rule_id": "GLIBC_VERSION_MISMATCH",
|
|
27
|
+
"explanation": (
|
|
28
|
+
"Your system's C library (GLIBC) is older than what this package "
|
|
29
|
+
"needs. The package was compiled on a newer Linux system."
|
|
30
|
+
),
|
|
31
|
+
"fix_hint": "Upgrade your OS or use an older package version.",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"pattern": r"Illegal instruction",
|
|
35
|
+
"root_cause": "CPU instruction set mismatch",
|
|
36
|
+
"rule_id": "ILLEGAL_INSTRUCTION_RISK",
|
|
37
|
+
"explanation": (
|
|
38
|
+
"The package uses CPU instructions (like AVX2) that your "
|
|
39
|
+
"processor doesn't support."
|
|
40
|
+
),
|
|
41
|
+
"fix_hint": "Install a version built for your CPU, or use a different machine.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"pattern": r"libcudart\.so\.(\d+): cannot open",
|
|
45
|
+
"root_cause": "CUDA runtime version mismatch",
|
|
46
|
+
"rule_id": "CUDA_RUNTIME_MISMATCH",
|
|
47
|
+
"explanation": (
|
|
48
|
+
"The package was built for a different CUDA version than "
|
|
49
|
+
"what's installed on your system."
|
|
50
|
+
),
|
|
51
|
+
"fix_hint": "Install the package variant matching your CUDA version.",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"pattern": r"libcublas\.so\.(\d+): cannot open",
|
|
55
|
+
"root_cause": "CUDA library missing",
|
|
56
|
+
"rule_id": "CUDA_LIB_MISSING",
|
|
57
|
+
"explanation": (
|
|
58
|
+
"A CUDA math library required by this package is not installed."
|
|
59
|
+
),
|
|
60
|
+
"fix_hint": (
|
|
61
|
+
"Install the matching CUDA toolkit or reinstall the package "
|
|
62
|
+
"for your CUDA version."
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"pattern": r"undefined symbol: _Py",
|
|
67
|
+
"root_cause": "Python ABI mismatch",
|
|
68
|
+
"rule_id": "PYTHON_ABI_MISMATCH",
|
|
69
|
+
"explanation": (
|
|
70
|
+
"The package was compiled for a different Python version's "
|
|
71
|
+
"internal API."
|
|
72
|
+
),
|
|
73
|
+
"fix_hint": "Reinstall the package for your current Python version.",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"pattern": (
|
|
77
|
+
r"module compiled against API version (0x[0-9a-f]+) "
|
|
78
|
+
r"but this version of numpy is (0x[0-9a-f]+)"
|
|
79
|
+
),
|
|
80
|
+
"root_cause": "NumPy C API version mismatch",
|
|
81
|
+
"rule_id": "NUMPY_ABI_MISMATCH",
|
|
82
|
+
"explanation": (
|
|
83
|
+
"The package was built against a different NumPy version's C API."
|
|
84
|
+
),
|
|
85
|
+
"fix_hint": "Reinstall the package or upgrade/downgrade NumPy.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"pattern": r"not a supported wheel on this platform",
|
|
89
|
+
"root_cause": "Platform/architecture mismatch",
|
|
90
|
+
"rule_id": "ARCH_MISMATCH",
|
|
91
|
+
"explanation": (
|
|
92
|
+
"This wheel was built for a different CPU architecture or OS."
|
|
93
|
+
),
|
|
94
|
+
"fix_hint": "Download the correct wheel for your platform.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"pattern": r"cudaErrorInsufficientDriver",
|
|
98
|
+
"root_cause": "GPU driver too old",
|
|
99
|
+
"rule_id": "CUDA_DRIVER_TOO_OLD",
|
|
100
|
+
"explanation": (
|
|
101
|
+
"Your GPU driver is too old to support the installed CUDA version."
|
|
102
|
+
),
|
|
103
|
+
"fix_hint": "Update your NVIDIA GPU driver.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"pattern": r"NVML: Driver/library version mismatch",
|
|
107
|
+
"root_cause": "Container driver mismatch",
|
|
108
|
+
"rule_id": "CONTAINER_DRIVER_MISMATCH",
|
|
109
|
+
"explanation": (
|
|
110
|
+
"The NVIDIA driver in your container doesn't match the host "
|
|
111
|
+
"driver. This often happens when the container image has a "
|
|
112
|
+
"newer CUDA than the host supports."
|
|
113
|
+
),
|
|
114
|
+
"fix_hint": (
|
|
115
|
+
"Use nvidia-container-toolkit and ensure host driver supports "
|
|
116
|
+
"your CUDA version."
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"pattern": r"libcudnn\.so\.\d+: cannot open",
|
|
121
|
+
"root_cause": "cuDNN missing",
|
|
122
|
+
"rule_id": "CUDNN_VERSION_MISMATCH",
|
|
123
|
+
"explanation": (
|
|
124
|
+
"The cuDNN library required for GPU-accelerated deep learning "
|
|
125
|
+
"is not installed."
|
|
126
|
+
),
|
|
127
|
+
"fix_hint": "Install the cuDNN version matching your CUDA installation.",
|
|
128
|
+
},
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Pre-compile patterns once at import time for performance.
|
|
133
|
+
_COMPILED_PATTERNS: List[Tuple[re.Pattern[str], Dict[str, str]]] = [
|
|
134
|
+
(re.compile(entry["pattern"]), entry) for entry in ERROR_PATTERNS
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# ErrorPatternDB
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ErrorPatternDB:
|
|
144
|
+
"""A database of error-message patterns with associated root causes.
|
|
145
|
+
|
|
146
|
+
The default set of patterns is embedded in the module (``ERROR_PATTERNS``)
|
|
147
|
+
*and* also available as ``error_patterns.json`` for external tooling. You
|
|
148
|
+
can extend the database at runtime by loading additional patterns from
|
|
149
|
+
JSON files or adding them programmatically.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self) -> None:
|
|
153
|
+
self._patterns: List[Dict[str, str]] = list(ERROR_PATTERNS)
|
|
154
|
+
self._compiled: List[Tuple[re.Pattern[str], Dict[str, str]]] = list(
|
|
155
|
+
_COMPILED_PATTERNS
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# -- mutation -----------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def add_pattern(self, entry: Dict[str, str]) -> None:
|
|
161
|
+
"""Add a single pattern entry to the database.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
entry:
|
|
166
|
+
A dict with keys ``pattern``, ``root_cause``, ``rule_id``,
|
|
167
|
+
``explanation``, and ``fix_hint``.
|
|
168
|
+
|
|
169
|
+
Raises
|
|
170
|
+
------
|
|
171
|
+
ValueError
|
|
172
|
+
If any required key is missing.
|
|
173
|
+
"""
|
|
174
|
+
required_keys = {"pattern", "root_cause", "rule_id", "explanation", "fix_hint"}
|
|
175
|
+
missing = required_keys - entry.keys()
|
|
176
|
+
if missing:
|
|
177
|
+
raise ValueError(f"Pattern entry is missing required keys: {missing}")
|
|
178
|
+
|
|
179
|
+
self._patterns.append(entry)
|
|
180
|
+
self._compiled.append((re.compile(entry["pattern"]), entry))
|
|
181
|
+
|
|
182
|
+
def load_json(self, path: str) -> int:
|
|
183
|
+
"""Load additional patterns from a JSON file.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
path:
|
|
188
|
+
Absolute or relative path to a JSON file containing a list of
|
|
189
|
+
pattern entries.
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
int
|
|
194
|
+
Number of patterns loaded.
|
|
195
|
+
|
|
196
|
+
Raises
|
|
197
|
+
------
|
|
198
|
+
FileNotFoundError
|
|
199
|
+
If the file does not exist.
|
|
200
|
+
json.JSONDecodeError
|
|
201
|
+
If the file is not valid JSON.
|
|
202
|
+
ValueError
|
|
203
|
+
If any entry is missing required keys.
|
|
204
|
+
"""
|
|
205
|
+
json_path = Path(path)
|
|
206
|
+
with json_path.open("r", encoding="utf-8") as fh:
|
|
207
|
+
entries: List[Dict[str, str]] = json.load(fh)
|
|
208
|
+
|
|
209
|
+
for entry in entries:
|
|
210
|
+
self.add_pattern(entry)
|
|
211
|
+
return len(entries)
|
|
212
|
+
|
|
213
|
+
# -- queries ------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def get_error_patterns(self) -> List[Dict[str, str]]:
|
|
216
|
+
"""Return all registered patterns as a list of dicts.
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
-------
|
|
220
|
+
List[Dict[str, str]]
|
|
221
|
+
A shallow copy of the internal pattern list.
|
|
222
|
+
"""
|
|
223
|
+
return list(self._patterns)
|
|
224
|
+
|
|
225
|
+
def diagnose_error(self, error_message: str) -> Optional[Dict[str, str]]:
|
|
226
|
+
"""Match *error_message* against all known patterns.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
error_message:
|
|
231
|
+
The raw error text (e.g. a traceback line or stderr fragment).
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
Optional[Dict[str, str]]
|
|
236
|
+
A dict with keys ``root_cause``, ``rule_id``, ``explanation``,
|
|
237
|
+
``fix_hint``, and ``matched_pattern`` if a match is found; otherwise
|
|
238
|
+
``None``.
|
|
239
|
+
"""
|
|
240
|
+
if not error_message:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
for compiled, entry in self._compiled:
|
|
244
|
+
match = compiled.search(error_message)
|
|
245
|
+
if match:
|
|
246
|
+
return {
|
|
247
|
+
"root_cause": entry["root_cause"],
|
|
248
|
+
"rule_id": entry["rule_id"],
|
|
249
|
+
"explanation": entry["explanation"],
|
|
250
|
+
"fix_hint": entry["fix_hint"],
|
|
251
|
+
"matched_pattern": entry["pattern"],
|
|
252
|
+
}
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
def explain_finding(self, finding: Finding) -> str:
|
|
256
|
+
"""Generate a full plain-English explanation paragraph for a Finding.
|
|
257
|
+
|
|
258
|
+
The explanation combines the finding's own explanation text with
|
|
259
|
+
any matching pattern data (if the finding carries a
|
|
260
|
+
``related_error``), and appends a fix suggestion when available.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
finding:
|
|
265
|
+
A :class:`Finding` instance to explain.
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
str
|
|
270
|
+
A multi-sentence explanation suitable for displaying to
|
|
271
|
+
end users.
|
|
272
|
+
"""
|
|
273
|
+
parts: List[str] = []
|
|
274
|
+
|
|
275
|
+
# Start with the finding's built-in explanation.
|
|
276
|
+
if finding.explanation:
|
|
277
|
+
parts.append(finding.explanation)
|
|
278
|
+
|
|
279
|
+
# If there is a related error string, try to diagnose it for
|
|
280
|
+
# additional context.
|
|
281
|
+
if finding.related_error:
|
|
282
|
+
diagnosis = self.diagnose_error(finding.related_error)
|
|
283
|
+
if diagnosis and diagnosis["explanation"] not in (finding.explanation or ""):
|
|
284
|
+
parts.append(
|
|
285
|
+
f"Root cause: {diagnosis['root_cause']}. "
|
|
286
|
+
f"{diagnosis['explanation']}"
|
|
287
|
+
)
|
|
288
|
+
if diagnosis["fix_hint"]:
|
|
289
|
+
parts.append(f"Suggested fix: {diagnosis['fix_hint']}")
|
|
290
|
+
|
|
291
|
+
# Fall back to the finding's own suggestion if nothing else matched.
|
|
292
|
+
if finding.suggestion and not any("Suggested fix" in p for p in parts):
|
|
293
|
+
parts.append(f"Suggested fix: {finding.suggestion}")
|
|
294
|
+
|
|
295
|
+
if not parts:
|
|
296
|
+
return f"No detailed explanation available for rule {finding.rule_id}."
|
|
297
|
+
|
|
298
|
+
return " ".join(parts)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# Module-level convenience functions (thin wrappers around a default DB)
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
_DEFAULT_DB = ErrorPatternDB()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def diagnose_error(error_message: str) -> Optional[Dict[str, str]]:
|
|
309
|
+
"""Match *error_message* against all known error patterns.
|
|
310
|
+
|
|
311
|
+
This is a convenience wrapper around
|
|
312
|
+
:meth:`ErrorPatternDB.diagnose_error` using the default built-in
|
|
313
|
+
pattern database.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
error_message:
|
|
318
|
+
The raw error text to diagnose.
|
|
319
|
+
|
|
320
|
+
Returns
|
|
321
|
+
-------
|
|
322
|
+
Optional[Dict[str, str]]
|
|
323
|
+
Diagnosis dict or ``None``.
|
|
324
|
+
"""
|
|
325
|
+
return _DEFAULT_DB.diagnose_error(error_message)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def explain_finding(finding: Finding) -> str:
|
|
329
|
+
"""Generate a plain-English explanation for a Finding.
|
|
330
|
+
|
|
331
|
+
This is a convenience wrapper around
|
|
332
|
+
:meth:`ErrorPatternDB.explain_finding` using the default built-in
|
|
333
|
+
pattern database.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
finding:
|
|
338
|
+
The finding to explain.
|
|
339
|
+
|
|
340
|
+
Returns
|
|
341
|
+
-------
|
|
342
|
+
str
|
|
343
|
+
A human-readable explanation paragraph.
|
|
344
|
+
"""
|
|
345
|
+
return _DEFAULT_DB.explain_finding(finding)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def get_error_patterns() -> List[Dict[str, str]]:
|
|
349
|
+
"""Return all built-in error patterns.
|
|
350
|
+
|
|
351
|
+
Returns
|
|
352
|
+
-------
|
|
353
|
+
List[Dict[str, str]]
|
|
354
|
+
A list of pattern dicts.
|
|
355
|
+
"""
|
|
356
|
+
return _DEFAULT_DB.get_error_patterns()
|