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,426 @@
1
+ """TensorRT plugin for PyBinaryGuard.
2
+
3
+ Provides rules that check TensorRT version compatibility with installed
4
+ deep-learning frameworks and detect serialised engine files built for a
5
+ different GPU architecture.
6
+
7
+ Provides
8
+ --------
9
+ - **TensorRTVersionRule** -- Checks that the installed TensorRT version
10
+ is compatible with the installed versions of TensorFlow, PyTorch, and
11
+ ONNX Runtime.
12
+ - **TensorRTEngineMismatchRule** -- Scans for ``.engine`` / ``.trt``
13
+ files that were serialised for a different GPU compute capability than
14
+ the current device.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import os
21
+ import struct
22
+ from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
23
+
24
+ from pybinaryguard.models.enums import Severity
25
+ from pybinaryguard.models.finding import Finding
26
+ from pybinaryguard.models.package import PackageBinaryInfo
27
+ from pybinaryguard.models.system import SystemProfile
28
+ from pybinaryguard.rules.base import Rule
29
+
30
+ if TYPE_CHECKING:
31
+ from pybinaryguard.plugins.hooks import HookRegistry
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # -- Version compatibility tables -------------------------------------------
36
+
37
+ # Minimum TensorRT version required by framework major.minor releases.
38
+ # Format: {("framework", (major, minor)): (trt_min_major, trt_min_minor)}
39
+ _FRAMEWORK_TRT_COMPAT: Dict[Tuple[str, Tuple[int, int]], Tuple[int, int]] = {
40
+ # PyTorch (torch.tensorrt / torch2trt)
41
+ ("pytorch", (2, 0)): (8, 5),
42
+ ("pytorch", (2, 1)): (8, 6),
43
+ ("pytorch", (2, 2)): (8, 6),
44
+ ("pytorch", (2, 3)): (10, 0),
45
+ ("pytorch", (2, 4)): (10, 0),
46
+ ("pytorch", (2, 5)): (10, 3),
47
+ # TensorFlow
48
+ ("tensorflow", (2, 12)): (8, 5),
49
+ ("tensorflow", (2, 13)): (8, 6),
50
+ ("tensorflow", (2, 14)): (8, 6),
51
+ ("tensorflow", (2, 15)): (8, 6),
52
+ ("tensorflow", (2, 16)): (8, 6),
53
+ # ONNX Runtime
54
+ ("onnxruntime", (1, 15)): (8, 6),
55
+ ("onnxruntime", (1, 16)): (8, 6),
56
+ ("onnxruntime", (1, 17)): (8, 6),
57
+ ("onnxruntime", (1, 18)): (10, 0),
58
+ ("onnxruntime", (1, 19)): (10, 0),
59
+ }
60
+
61
+ # Package names that correspond to each framework key above.
62
+ _FRAMEWORK_PACKAGE_NAMES: Dict[str, List[str]] = {
63
+ "pytorch": ["torch"],
64
+ "tensorflow": ["tensorflow", "tensorflow-gpu", "tf-nightly"],
65
+ "onnxruntime": ["onnxruntime", "onnxruntime-gpu"],
66
+ }
67
+
68
+
69
+ # -- Helpers ----------------------------------------------------------------
70
+
71
+
72
+ def _get_tensorrt_version() -> Optional[Tuple[int, int, int]]:
73
+ """Attempt to determine the installed TensorRT version.
74
+
75
+ Tries the ``tensorrt`` Python package first, then falls back to
76
+ parsing ``libnvinfer.so`` via ``ldconfig -p``.
77
+
78
+ Returns:
79
+ A ``(major, minor, patch)`` tuple, or ``None`` if TensorRT is
80
+ not detected.
81
+ """
82
+ # Method 1: Python package
83
+ try:
84
+ import tensorrt # type: ignore[import-untyped]
85
+ ver = getattr(tensorrt, "__version__", None)
86
+ if ver:
87
+ parts = ver.split(".")
88
+ if len(parts) >= 3:
89
+ return (int(parts[0]), int(parts[1]), int(parts[2]))
90
+ if len(parts) == 2:
91
+ return (int(parts[0]), int(parts[1]), 0)
92
+ except ImportError:
93
+ pass
94
+ except Exception:
95
+ logger.debug("Failed to read tensorrt.__version__", exc_info=True)
96
+
97
+ # Method 2: Parse libnvinfer soname from ldconfig
98
+ try:
99
+ import subprocess
100
+ result = subprocess.run(
101
+ ["ldconfig", "-p"],
102
+ capture_output=True,
103
+ text=True,
104
+ timeout=5,
105
+ )
106
+ if result.returncode == 0:
107
+ import re
108
+ match = re.search(r"libnvinfer\.so\.(\d+)\.(\d+)\.(\d+)", result.stdout)
109
+ if match:
110
+ return (
111
+ int(match.group(1)),
112
+ int(match.group(2)),
113
+ int(match.group(3)),
114
+ )
115
+ # Fallback: just major version from soname
116
+ match = re.search(r"libnvinfer\.so\.(\d+)", result.stdout)
117
+ if match:
118
+ return (int(match.group(1)), 0, 0)
119
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
120
+ pass
121
+
122
+ return None
123
+
124
+
125
+ def _parse_package_version(version_str: str) -> Optional[Tuple[int, int]]:
126
+ """Parse a package version string into (major, minor).
127
+
128
+ Handles common formats like ``2.1.0``, ``2.1.0+cu118``,
129
+ ``2.1.0.post1``, etc.
130
+
131
+ Args:
132
+ version_str: The raw version string.
133
+
134
+ Returns:
135
+ A ``(major, minor)`` tuple, or ``None`` on parse failure.
136
+ """
137
+ import re
138
+ match = re.match(r"(\d+)\.(\d+)", version_str)
139
+ if match:
140
+ return (int(match.group(1)), int(match.group(2)))
141
+ return None
142
+
143
+
144
+ # -- Rules ------------------------------------------------------------------
145
+
146
+
147
+ class TensorRTVersionRule(Rule):
148
+ """Check TensorRT version compatibility with installed frameworks.
149
+
150
+ Verifies that the installed TensorRT version meets the minimum
151
+ requirements of installed deep-learning frameworks (PyTorch,
152
+ TensorFlow, ONNX Runtime).
153
+ """
154
+
155
+ rule_id = "TENSORRT_VERSION_COMPAT"
156
+ description = (
157
+ "Verify that the installed TensorRT version is compatible with "
158
+ "installed deep-learning frameworks."
159
+ )
160
+
161
+ def is_applicable(self, profile: SystemProfile) -> bool:
162
+ """Only applies when a GPU is available."""
163
+ return profile.gpu_available
164
+
165
+ def evaluate(
166
+ self,
167
+ profile: SystemProfile,
168
+ packages: List[PackageBinaryInfo],
169
+ ) -> List[Finding]:
170
+ """Check TensorRT version against framework requirements."""
171
+ findings: List[Finding] = []
172
+
173
+ trt_version = _get_tensorrt_version()
174
+ if trt_version is None:
175
+ # TensorRT not installed -- nothing to check.
176
+ return findings
177
+
178
+ trt_major, trt_minor = trt_version[0], trt_version[1]
179
+
180
+ # Build a lookup of installed packages for quick access.
181
+ installed: Dict[str, str] = {}
182
+ for pkg in packages:
183
+ installed[pkg.package_name.lower()] = pkg.package_version
184
+
185
+ for framework_key, package_names in _FRAMEWORK_PACKAGE_NAMES.items():
186
+ for pkg_name in package_names:
187
+ version_str = installed.get(pkg_name.lower())
188
+ if version_str is None:
189
+ continue
190
+
191
+ fw_version = _parse_package_version(version_str)
192
+ if fw_version is None:
193
+ continue
194
+
195
+ required = _FRAMEWORK_TRT_COMPAT.get((framework_key, fw_version))
196
+ if required is None:
197
+ # No compatibility data for this version -- skip.
198
+ continue
199
+
200
+ req_major, req_minor = required
201
+ if trt_major < req_major or (trt_major == req_major and trt_minor < req_minor):
202
+ findings.append(Finding(
203
+ rule_id=self.rule_id,
204
+ severity=Severity.WARNING,
205
+ title=(
206
+ f"TensorRT {trt_major}.{trt_minor} may be "
207
+ f"incompatible with {pkg_name} {version_str}"
208
+ ),
209
+ explanation=(
210
+ f"{pkg_name}=={version_str} requires TensorRT >= "
211
+ f"{req_major}.{req_minor}, but TensorRT "
212
+ f"{trt_major}.{trt_minor}.{trt_version[2]} is installed."
213
+ ),
214
+ technical_detail=(
215
+ f"Framework: {pkg_name}=={version_str}, "
216
+ f"TensorRT: {trt_major}.{trt_minor}.{trt_version[2]}, "
217
+ f"Required: >= {req_major}.{req_minor}"
218
+ ),
219
+ suggestion=(
220
+ f"Upgrade TensorRT to version {req_major}.{req_minor} "
221
+ f"or later, or downgrade {pkg_name} to a version "
222
+ f"compatible with TensorRT {trt_major}.{trt_minor}."
223
+ ),
224
+ package=pkg_name,
225
+ package_version=version_str,
226
+ ))
227
+ else:
228
+ findings.append(Finding(
229
+ rule_id=self.rule_id,
230
+ severity=Severity.PASSED,
231
+ title=(
232
+ f"TensorRT {trt_major}.{trt_minor} is compatible "
233
+ f"with {pkg_name} {version_str}"
234
+ ),
235
+ explanation=(
236
+ f"TensorRT {trt_major}.{trt_minor}.{trt_version[2]} "
237
+ f"meets the minimum requirement "
238
+ f"(>= {req_major}.{req_minor}) for "
239
+ f"{pkg_name}=={version_str}."
240
+ ),
241
+ package=pkg_name,
242
+ package_version=version_str,
243
+ ))
244
+ # Only check the first matching package name per framework.
245
+ break
246
+
247
+ return findings
248
+
249
+
250
+ class TensorRTEngineMismatchRule(Rule):
251
+ """Detect serialised TensorRT engine files built for a different GPU.
252
+
253
+ TensorRT engine files (``.engine``, ``.trt``, ``.plan``) are
254
+ serialised for a specific GPU architecture and are not portable.
255
+ Loading an engine built for a different compute capability will fail
256
+ at runtime.
257
+
258
+ This rule scans site-packages directories for engine files and
259
+ performs a best-effort check of their target compute capability by
260
+ reading the header bytes.
261
+ """
262
+
263
+ rule_id = "TENSORRT_ENGINE_MISMATCH"
264
+ description = (
265
+ "Detect TensorRT engine files serialised for a different GPU "
266
+ "compute capability."
267
+ )
268
+
269
+ # TensorRT serialised engines have a small header. The exact layout
270
+ # varies by TensorRT version, but a common pattern is that the magic
271
+ # bytes start with "ptrt" (0x70747274) followed by version info.
272
+ # This is a best-effort heuristic.
273
+ _ENGINE_EXTENSIONS = (".engine", ".trt", ".plan")
274
+ _MAX_SCAN_BYTES = 256
275
+
276
+ def is_applicable(self, profile: SystemProfile) -> bool:
277
+ """Only applies when GPU compute capability is known."""
278
+ return (
279
+ profile.gpu_available
280
+ and profile.gpu_compute_capability is not None
281
+ )
282
+
283
+ def evaluate(
284
+ self,
285
+ profile: SystemProfile,
286
+ packages: List[PackageBinaryInfo],
287
+ ) -> List[Finding]:
288
+ """Scan for engine files in package install paths."""
289
+ findings: List[Finding] = []
290
+
291
+ if profile.gpu_compute_capability is None:
292
+ return findings
293
+
294
+ current_cc = profile.gpu_compute_capability
295
+
296
+ for pkg in packages:
297
+ if not pkg.install_path or not os.path.isdir(pkg.install_path):
298
+ continue
299
+
300
+ engine_files = self._find_engine_files(pkg.install_path)
301
+ for engine_path in engine_files:
302
+ engine_cc = self._read_engine_compute_capability(engine_path)
303
+ if engine_cc is not None and engine_cc != current_cc:
304
+ findings.append(Finding(
305
+ rule_id=self.rule_id,
306
+ severity=Severity.WARNING,
307
+ title=(
308
+ f"TensorRT engine built for different GPU: "
309
+ f"{os.path.basename(engine_path)}"
310
+ ),
311
+ explanation=(
312
+ f"Engine file {engine_path} appears to be "
313
+ f"serialised for compute capability "
314
+ f"{engine_cc[0]}.{engine_cc[1]}, but the "
315
+ f"current GPU has compute capability "
316
+ f"{current_cc[0]}.{current_cc[1]}. "
317
+ f"Loading this engine will fail at runtime."
318
+ ),
319
+ technical_detail=(
320
+ f"Engine CC: {engine_cc[0]}.{engine_cc[1]}, "
321
+ f"GPU CC: {current_cc[0]}.{current_cc[1]}, "
322
+ f"File: {engine_path}"
323
+ ),
324
+ suggestion=(
325
+ "Re-serialise the TensorRT engine on this GPU, "
326
+ "or use the ONNX model to rebuild the engine: "
327
+ "trtexec --onnx=model.onnx --saveEngine=model.engine"
328
+ ),
329
+ package=pkg.package_name,
330
+ package_version=pkg.package_version,
331
+ ))
332
+
333
+ return findings
334
+
335
+ def _find_engine_files(self, directory: str) -> List[str]:
336
+ """Recursively find TensorRT engine files in a directory.
337
+
338
+ Limits the search to 3 directory levels deep to avoid traversing
339
+ overly large trees.
340
+ """
341
+ engine_files: List[str] = []
342
+ try:
343
+ for root, dirs, files in os.walk(directory):
344
+ # Limit depth to avoid excessive traversal.
345
+ depth = root[len(directory):].count(os.sep)
346
+ if depth >= 3:
347
+ dirs.clear()
348
+ continue
349
+ for fname in files:
350
+ if any(fname.endswith(ext) for ext in self._ENGINE_EXTENSIONS):
351
+ engine_files.append(os.path.join(root, fname))
352
+ except (PermissionError, OSError):
353
+ pass
354
+ return engine_files
355
+
356
+ @staticmethod
357
+ def _read_engine_compute_capability(
358
+ path: str,
359
+ ) -> Optional[Tuple[int, int]]:
360
+ """Attempt to read the compute capability from a TensorRT engine file.
361
+
362
+ This is a best-effort heuristic. TensorRT engine files do not
363
+ have a publicly documented header format, but engine files
364
+ serialised by common TensorRT versions embed the compute
365
+ capability as two bytes in the header region.
366
+
367
+ We look for the ``ptrt`` magic sequence and attempt to extract
368
+ CC from known offsets. Returns ``None`` when the format is
369
+ unrecognised.
370
+ """
371
+ try:
372
+ with open(path, "rb") as fh:
373
+ header = fh.read(256)
374
+ except (FileNotFoundError, PermissionError, OSError):
375
+ return None
376
+
377
+ if len(header) < 32:
378
+ return None
379
+
380
+ # Look for the "ptrt" magic (0x70747274)
381
+ magic_offset = header.find(b"ptrt")
382
+ if magic_offset < 0:
383
+ # Try little-endian uint32 magic
384
+ magic_offset = header.find(b"trtp")
385
+ if magic_offset < 0:
386
+ return None
387
+
388
+ # In several TensorRT versions, the compute capability is stored
389
+ # as two uint8 values at offset magic+20 and magic+21 (major, minor).
390
+ cc_offset = magic_offset + 20
391
+ if cc_offset + 2 > len(header):
392
+ return None
393
+
394
+ cc_major = header[cc_offset]
395
+ cc_minor = header[cc_offset + 1]
396
+
397
+ # Sanity check: compute capability major should be 2-12
398
+ if 2 <= cc_major <= 12 and 0 <= cc_minor <= 9:
399
+ return (cc_major, cc_minor)
400
+
401
+ return None
402
+
403
+
404
+ # -- Plugin entry point -----------------------------------------------------
405
+
406
+
407
+ def register(registry: HookRegistry) -> None:
408
+ """Register TensorRT rules if TensorRT is available.
409
+
410
+ This function is called by the plugin loader. It activates when
411
+ either the ``tensorrt`` Python package is importable or
412
+ ``libnvinfer`` is found in the system library cache.
413
+ """
414
+ trt_version = _get_tensorrt_version()
415
+ if trt_version is None:
416
+ logger.debug("TensorRT plugin: TensorRT not detected; not activating")
417
+ return
418
+
419
+ registry.add_rule(TensorRTVersionRule())
420
+ registry.add_rule(TensorRTEngineMismatchRule())
421
+ logger.info(
422
+ "TensorRT plugin activated (version %d.%d.%d)",
423
+ trt_version[0],
424
+ trt_version[1],
425
+ trt_version[2],
426
+ )
@@ -0,0 +1,273 @@
1
+ """Hook registry for the plugin subsystem.
2
+
3
+ Plugins register their extensions (probes, rules, detectors, reporters,
4
+ and lifecycle hooks) with a :class:`HookRegistry` instance that is passed
5
+ to their ``register()`` function during discovery.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from pybinaryguard.probes.base import ProbeBase
15
+ from pybinaryguard.rules.base import Rule
16
+ from pybinaryguard.models.system import SystemProfile
17
+ from pybinaryguard.models.package import PackageBinaryInfo
18
+ from pybinaryguard.models.finding import Finding
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Type aliases for hook signatures.
23
+ BoardDetector = Callable[[Dict[str, Any]], Optional[str]]
24
+ FrameworkChecker = Callable[["SystemProfile", List["PackageBinaryInfo"]], List["Finding"]]
25
+ Reporter = Callable[[List["Finding"], "SystemProfile"], str]
26
+ ScanHook = Callable[["SystemProfile"], None]
27
+
28
+
29
+ class HookRegistry:
30
+ """Central registry that collects all plugin-provided extensions.
31
+
32
+ An instance is created by the plugin loader and handed to each plugin's
33
+ ``register()`` entry point. Plugins call the ``add_*`` methods to
34
+ register their contributions, and the host application reads them back
35
+ via the corresponding read-only properties.
36
+
37
+ Example (inside a plugin's ``register`` function)::
38
+
39
+ def register(registry: HookRegistry) -> None:
40
+ registry.add_probe(MyProbe())
41
+ registry.add_rule(MyRule())
42
+ registry.pre_scan(lambda profile: print("scan starting"))
43
+ """
44
+
45
+ def __init__(self) -> None:
46
+ self._probes: List[ProbeBase] = []
47
+ self._rules: List[Rule] = []
48
+ self._board_detectors: List[BoardDetector] = []
49
+ self._framework_checkers: List[FrameworkChecker] = []
50
+ self._reporters: List[Reporter] = []
51
+ self._pre_scan_hooks: List[ScanHook] = []
52
+ self._post_scan_hooks: List[ScanHook] = []
53
+
54
+ # ------------------------------------------------------------------
55
+ # Registration methods
56
+ # ------------------------------------------------------------------
57
+
58
+ def add_probe(self, probe: ProbeBase) -> None:
59
+ """Register an additional system probe.
60
+
61
+ Args:
62
+ probe: A :class:`~pybinaryguard.probes.base.ProbeBase` subclass
63
+ instance. Must have a unique ``name`` attribute.
64
+
65
+ Raises:
66
+ TypeError: If *probe* is not a :class:`ProbeBase` instance.
67
+ ValueError: If a probe with the same ``name`` is already registered.
68
+ """
69
+ from pybinaryguard.probes.base import ProbeBase as _ProbeBase
70
+
71
+ if not isinstance(probe, _ProbeBase):
72
+ raise TypeError(
73
+ f"Expected a ProbeBase instance, got {type(probe).__name__}"
74
+ )
75
+ existing_names = {p.name for p in self._probes}
76
+ if probe.name in existing_names:
77
+ raise ValueError(
78
+ f"A probe named {probe.name!r} is already registered"
79
+ )
80
+ self._probes.append(probe)
81
+ logger.debug("Registered plugin probe: %s", probe.name)
82
+
83
+ def add_rule(self, rule: Rule) -> None:
84
+ """Register an additional compatibility rule.
85
+
86
+ Args:
87
+ rule: A :class:`~pybinaryguard.rules.base.Rule` subclass instance.
88
+ Must have a unique ``rule_id`` attribute.
89
+
90
+ Raises:
91
+ TypeError: If *rule* is not a :class:`Rule` instance.
92
+ ValueError: If a rule with the same ``rule_id`` is already registered.
93
+ """
94
+ from pybinaryguard.rules.base import Rule as _Rule
95
+
96
+ if not isinstance(rule, _Rule):
97
+ raise TypeError(
98
+ f"Expected a Rule instance, got {type(rule).__name__}"
99
+ )
100
+ existing_ids = {r.rule_id for r in self._rules}
101
+ if rule.rule_id in existing_ids:
102
+ raise ValueError(
103
+ f"A rule with id {rule.rule_id!r} is already registered"
104
+ )
105
+ self._rules.append(rule)
106
+ logger.debug("Registered plugin rule: %s", rule.rule_id)
107
+
108
+ def add_board_detector(self, detector: BoardDetector) -> None:
109
+ """Register a board-detection callback.
110
+
111
+ A board detector is a callable that receives a dict of raw probe
112
+ data and returns either a board name string (e.g. ``"Jetson Orin
113
+ Nano"``) or ``None`` if the board was not recognised.
114
+
115
+ Args:
116
+ detector: The detection callable.
117
+
118
+ Raises:
119
+ TypeError: If *detector* is not callable.
120
+ """
121
+ if not callable(detector):
122
+ raise TypeError(
123
+ f"Expected a callable, got {type(detector).__name__}"
124
+ )
125
+ self._board_detectors.append(detector)
126
+ logger.debug("Registered plugin board detector: %s", detector)
127
+
128
+ def add_framework_checker(self, checker: FrameworkChecker) -> None:
129
+ """Register a framework-specific compatibility checker.
130
+
131
+ A framework checker receives the system profile and packages list
132
+ and returns additional findings for framework-specific issues
133
+ (e.g. OpenCV build flags, TensorRT version compatibility).
134
+
135
+ Args:
136
+ checker: The checker callable.
137
+
138
+ Raises:
139
+ TypeError: If *checker* is not callable.
140
+ """
141
+ if not callable(checker):
142
+ raise TypeError(
143
+ f"Expected a callable, got {type(checker).__name__}"
144
+ )
145
+ self._framework_checkers.append(checker)
146
+ logger.debug("Registered plugin framework checker: %s", checker)
147
+
148
+ def add_reporter(self, reporter: Reporter) -> None:
149
+ """Register a custom report formatter.
150
+
151
+ A reporter is a callable that takes a list of findings and the
152
+ system profile and returns a formatted report string.
153
+
154
+ Args:
155
+ reporter: The reporter callable.
156
+
157
+ Raises:
158
+ TypeError: If *reporter* is not callable.
159
+ """
160
+ if not callable(reporter):
161
+ raise TypeError(
162
+ f"Expected a callable, got {type(reporter).__name__}"
163
+ )
164
+ self._reporters.append(reporter)
165
+ logger.debug("Registered plugin reporter: %s", reporter)
166
+
167
+ def pre_scan(self, hook: ScanHook) -> None:
168
+ """Register a hook that runs before scanning begins.
169
+
170
+ The hook receives the :class:`SystemProfile` and may perform
171
+ logging, telemetry, or side-channel checks. It must not modify
172
+ system state.
173
+
174
+ Args:
175
+ hook: The pre-scan callable.
176
+
177
+ Raises:
178
+ TypeError: If *hook* is not callable.
179
+ """
180
+ if not callable(hook):
181
+ raise TypeError(
182
+ f"Expected a callable, got {type(hook).__name__}"
183
+ )
184
+ self._pre_scan_hooks.append(hook)
185
+ logger.debug("Registered plugin pre-scan hook: %s", hook)
186
+
187
+ def post_scan(self, hook: ScanHook) -> None:
188
+ """Register a hook that runs after scanning completes.
189
+
190
+ The hook receives the :class:`SystemProfile`. It must not modify
191
+ system state.
192
+
193
+ Args:
194
+ hook: The post-scan callable.
195
+
196
+ Raises:
197
+ TypeError: If *hook* is not callable.
198
+ """
199
+ if not callable(hook):
200
+ raise TypeError(
201
+ f"Expected a callable, got {type(hook).__name__}"
202
+ )
203
+ self._post_scan_hooks.append(hook)
204
+ logger.debug("Registered plugin post-scan hook: %s", hook)
205
+
206
+ # ------------------------------------------------------------------
207
+ # Read-only accessors
208
+ # ------------------------------------------------------------------
209
+
210
+ @property
211
+ def probes(self) -> List[ProbeBase]:
212
+ """All registered plugin probes (defensive copy)."""
213
+ return list(self._probes)
214
+
215
+ @property
216
+ def rules(self) -> List[Rule]:
217
+ """All registered plugin rules (defensive copy)."""
218
+ return list(self._rules)
219
+
220
+ @property
221
+ def board_detectors(self) -> List[BoardDetector]:
222
+ """All registered board-detection callbacks (defensive copy)."""
223
+ return list(self._board_detectors)
224
+
225
+ @property
226
+ def framework_checkers(self) -> List[FrameworkChecker]:
227
+ """All registered framework checkers (defensive copy)."""
228
+ return list(self._framework_checkers)
229
+
230
+ @property
231
+ def reporters(self) -> List[Reporter]:
232
+ """All registered report formatters (defensive copy)."""
233
+ return list(self._reporters)
234
+
235
+ @property
236
+ def pre_scan_hooks(self) -> List[ScanHook]:
237
+ """All registered pre-scan hooks (defensive copy)."""
238
+ return list(self._pre_scan_hooks)
239
+
240
+ @property
241
+ def post_scan_hooks(self) -> List[ScanHook]:
242
+ """All registered post-scan hooks (defensive copy)."""
243
+ return list(self._post_scan_hooks)
244
+
245
+ # ------------------------------------------------------------------
246
+ # Introspection helpers
247
+ # ------------------------------------------------------------------
248
+
249
+ @property
250
+ def probe_count(self) -> int:
251
+ """Number of registered probes."""
252
+ return len(self._probes)
253
+
254
+ @property
255
+ def rule_count(self) -> int:
256
+ """Number of registered rules."""
257
+ return len(self._rules)
258
+
259
+ def summary(self) -> Dict[str, int]:
260
+ """Return a dict summarising the number of registered extensions."""
261
+ return {
262
+ "probes": len(self._probes),
263
+ "rules": len(self._rules),
264
+ "board_detectors": len(self._board_detectors),
265
+ "framework_checkers": len(self._framework_checkers),
266
+ "reporters": len(self._reporters),
267
+ "pre_scan_hooks": len(self._pre_scan_hooks),
268
+ "post_scan_hooks": len(self._post_scan_hooks),
269
+ }
270
+
271
+ def __repr__(self) -> str:
272
+ parts = ", ".join(f"{k}={v}" for k, v in self.summary().items() if v)
273
+ return f"<HookRegistry({parts})>"