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,109 @@
1
+ """GStreamer pipeline validation plugin.
2
+
3
+ Checks for GStreamer availability and hardware acceleration on embedded boards.
4
+ Activated only when GStreamer libraries are detected on the system.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from pybinaryguard.models.enums import Severity
13
+ from pybinaryguard.models.finding import Finding
14
+ from pybinaryguard.models.package import PackageBinaryInfo
15
+ from pybinaryguard.models.system import SystemProfile
16
+ from pybinaryguard.probes.base import ProbeBase
17
+ from pybinaryguard.rules.base import Rule
18
+
19
+
20
+ class GStreamerProbe(ProbeBase):
21
+ """Detect GStreamer installation and version."""
22
+
23
+ name = "gstreamer"
24
+
25
+ def collect(self) -> Dict[str, Any]:
26
+ """Check for GStreamer library presence."""
27
+ data: Dict[str, Any] = {}
28
+
29
+ # Check for GStreamer library files
30
+ gst_lib_names = [
31
+ "libgstreamer-1.0.so",
32
+ "libgstreamer-1.0.so.0",
33
+ ]
34
+
35
+ for lib_name in gst_lib_names:
36
+ for search_dir in ["/usr/lib", "/usr/lib/aarch64-linux-gnu",
37
+ "/usr/lib/x86_64-linux-gnu", "/usr/local/lib"]:
38
+ path = os.path.join(search_dir, lib_name)
39
+ if os.path.exists(path):
40
+ data["_gstreamer_available"] = True
41
+ return data
42
+
43
+ data["_gstreamer_available"] = False
44
+ return data
45
+
46
+ def is_applicable(self) -> bool:
47
+ """Only run on Linux."""
48
+ import platform
49
+ return platform.system() == "Linux"
50
+
51
+
52
+ class GStreamerMissingRule(Rule):
53
+ """Check for GStreamer availability on embedded boards with camera use-cases."""
54
+
55
+ rule_id = "GSTREAMER_MISSING"
56
+ description = "GStreamer not found on embedded board"
57
+
58
+ def is_applicable(self, profile: SystemProfile) -> bool:
59
+ return profile.is_embedded_board
60
+
61
+ def evaluate(
62
+ self, profile: SystemProfile, packages: List[PackageBinaryInfo]
63
+ ) -> List[Finding]:
64
+ findings: List[Finding] = []
65
+
66
+ # Only relevant if OpenCV or camera-related packages are installed
67
+ camera_packages = {"opencv-python", "opencv-contrib-python", "cv2"}
68
+ has_camera_pkg = any(
69
+ p.package_name.lower().replace("-", "_") in {
70
+ n.replace("-", "_") for n in camera_packages
71
+ }
72
+ for p in packages
73
+ )
74
+
75
+ if not has_camera_pkg:
76
+ return findings
77
+
78
+ # Check for GStreamer
79
+ gst_found = False
80
+ for search_dir in ["/usr/lib", "/usr/lib/aarch64-linux-gnu",
81
+ "/usr/lib/x86_64-linux-gnu"]:
82
+ if os.path.exists(os.path.join(search_dir, "libgstreamer-1.0.so.0")):
83
+ gst_found = True
84
+ break
85
+
86
+ if not gst_found:
87
+ findings.append(Finding(
88
+ rule_id=self.rule_id,
89
+ severity=Severity.WARNING,
90
+ title="GStreamer not found on embedded board",
91
+ explanation=(
92
+ "GStreamer is commonly needed for camera pipelines on "
93
+ "embedded boards but was not found on this system."
94
+ ),
95
+ suggestion="sudo apt-get install libgstreamer1.0-0 gstreamer1.0-plugins-base",
96
+ confidence=0.7,
97
+ ))
98
+
99
+ return findings
100
+
101
+
102
+ def register(registry: Any) -> None:
103
+ """Register GStreamer plugin components."""
104
+ # Only activate on Linux
105
+ import platform
106
+ if platform.system() != "Linux":
107
+ return
108
+
109
+ registry.add_rule(GStreamerMissingRule())
@@ -0,0 +1,385 @@
1
+ """Jetson platform plugin for PyBinaryGuard.
2
+
3
+ Activates when ``/etc/nv_tegra_release`` exists, indicating an NVIDIA
4
+ Jetson (Tegra) system running Linux for Tegra (L4T).
5
+
6
+ Provides
7
+ --------
8
+ - **JetsonProbe** -- Enhanced Jetson detection: L4T version, JetPack
9
+ mapping, and Tegra SoC model.
10
+ - **JetPackCUDARule** -- Verifies that the installed CUDA version matches
11
+ the expected version for the detected JetPack release.
12
+ - **JetsonX86WheelRule** -- Detects x86-only wheels installed on an ARM
13
+ Jetson board, which will fail at import time.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import os
20
+ import re
21
+ from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
22
+
23
+ from pybinaryguard.models.enums import Architecture, Severity
24
+ from pybinaryguard.models.finding import Finding
25
+ from pybinaryguard.models.package import PackageBinaryInfo
26
+ from pybinaryguard.models.system import SystemProfile
27
+ from pybinaryguard.probes.base import ProbeBase
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
+ # -- Mapping tables ---------------------------------------------------------
36
+
37
+ L4T_TO_JETPACK: Dict[str, str] = {
38
+ "32.7.1": "4.6.1",
39
+ "32.7.2": "4.6.2",
40
+ "32.7.3": "4.6.3",
41
+ "32.7.4": "4.6.4",
42
+ "35.1.0": "5.0.2",
43
+ "35.2.1": "5.1",
44
+ "35.3.1": "5.1.1",
45
+ "35.4.1": "5.1.2",
46
+ "35.5.0": "5.1.3",
47
+ "36.2.0": "6.0",
48
+ "36.3.0": "6.0.1",
49
+ "36.4.0": "6.1",
50
+ }
51
+
52
+ JETPACK_CUDA: Dict[str, Tuple[int, int]] = {
53
+ "4.6.1": (10, 2),
54
+ "4.6.2": (10, 2),
55
+ "4.6.3": (10, 2),
56
+ "4.6.4": (10, 2),
57
+ "5.0.2": (11, 4),
58
+ "5.1": (11, 4),
59
+ "5.1.1": (11, 4),
60
+ "5.1.2": (11, 4),
61
+ "5.1.3": (11, 4),
62
+ "6.0": (12, 2),
63
+ "6.0.1": (12, 2),
64
+ "6.1": (12, 6),
65
+ }
66
+
67
+ # Known Tegra SoC chip IDs from /sys/module/tegra_fuse/parameters/tegra_chip_id
68
+ _TEGRA_CHIP_NAMES: Dict[str, str] = {
69
+ "0x18": "TX2 (Parker)",
70
+ "0x19": "Xavier (Carmel)",
71
+ "0x21": "Nano/TX1 (Erista)",
72
+ "0x23": "Orin (Ampere)",
73
+ }
74
+
75
+ _TEGRA_RELEASE_PATH = "/etc/nv_tegra_release"
76
+ _TEGRA_CHIP_ID_PATH = "/sys/module/tegra_fuse/parameters/tegra_chip_id"
77
+
78
+
79
+ # -- Probe ------------------------------------------------------------------
80
+
81
+
82
+ class JetsonProbe(ProbeBase):
83
+ """Enhanced Jetson/Tegra probe.
84
+
85
+ Reads ``/etc/nv_tegra_release`` to determine the L4T version, maps it
86
+ to a JetPack version, and identifies the Tegra SoC model. All data is
87
+ returned as ``SystemProfile`` field values.
88
+
89
+ This probe is read-only and does not modify any system state.
90
+ """
91
+
92
+ name = "jetson"
93
+
94
+ def is_applicable(self) -> bool:
95
+ """Only run on systems where ``/etc/nv_tegra_release`` exists."""
96
+ return os.path.isfile(_TEGRA_RELEASE_PATH)
97
+
98
+ def collect(self) -> Dict[str, Any]:
99
+ """Collect Jetson platform information.
100
+
101
+ Returns
102
+ -------
103
+ Dict[str, Any]
104
+ Keys include ``is_embedded_board``, ``board_name``,
105
+ ``jetpack_version``, and -- when determinable --
106
+ ``cuda_toolkit_version``.
107
+ """
108
+ data: Dict[str, Any] = {
109
+ "is_embedded_board": True,
110
+ "architecture": Architecture.AARCH64,
111
+ }
112
+
113
+ l4t_version = self._parse_l4t_version()
114
+ tegra_model = self._read_tegra_model()
115
+
116
+ board_name = "NVIDIA Jetson"
117
+ if tegra_model:
118
+ board_name = f"NVIDIA Jetson ({tegra_model})"
119
+ data["board_name"] = board_name
120
+
121
+ if l4t_version:
122
+ jetpack = L4T_TO_JETPACK.get(l4t_version)
123
+ if jetpack:
124
+ data["jetpack_version"] = jetpack
125
+ expected_cuda = JETPACK_CUDA.get(jetpack)
126
+ if expected_cuda:
127
+ data["cuda_toolkit_version"] = expected_cuda
128
+
129
+ data["gpu_available"] = True
130
+
131
+ return data
132
+
133
+ # -- Internal helpers ---------------------------------------------------
134
+
135
+ @staticmethod
136
+ def _parse_l4t_version() -> Optional[str]:
137
+ """Parse the L4T version string from ``/etc/nv_tegra_release``.
138
+
139
+ The first line typically looks like::
140
+
141
+ # R35 (release), REVISION: 4.1, ...
142
+
143
+ This method extracts ``"35.4.1"`` from that line.
144
+ """
145
+ try:
146
+ with open(_TEGRA_RELEASE_PATH, "r") as fh:
147
+ content = fh.read()
148
+ except (FileNotFoundError, PermissionError, OSError):
149
+ return None
150
+
151
+ # Match "R<major> (release), REVISION: <minor>.<patch>"
152
+ match = re.search(
153
+ r"#\s*R(\d+)\s*\(release\),\s*REVISION:\s*(\d+\.\d+)",
154
+ content,
155
+ )
156
+ if match:
157
+ major = match.group(1)
158
+ minor_patch = match.group(2)
159
+ return f"{major}.{minor_patch}"
160
+ return None
161
+
162
+ @staticmethod
163
+ def _read_tegra_model() -> Optional[str]:
164
+ """Read the Tegra chip ID and map it to a human-readable name.
165
+
166
+ The chip ID is read from
167
+ ``/sys/module/tegra_fuse/parameters/tegra_chip_id``.
168
+ """
169
+ try:
170
+ with open(_TEGRA_CHIP_ID_PATH, "r") as fh:
171
+ raw = fh.read().strip()
172
+ except (FileNotFoundError, PermissionError, OSError):
173
+ return None
174
+
175
+ # Normalise to hex string
176
+ try:
177
+ chip_id = int(raw, 0)
178
+ hex_id = f"0x{chip_id:02x}"
179
+ except ValueError:
180
+ return None
181
+
182
+ return _TEGRA_CHIP_NAMES.get(hex_id)
183
+
184
+
185
+ # -- Rules ------------------------------------------------------------------
186
+
187
+
188
+ class JetPackCUDARule(Rule):
189
+ """Verify that installed CUDA matches the expected JetPack CUDA version.
190
+
191
+ On a Jetson board, the JetPack release pins a specific CUDA toolkit
192
+ version. If the detected CUDA version does not match, libraries
193
+ compiled against the expected CUDA may crash or produce incorrect
194
+ results.
195
+ """
196
+
197
+ rule_id = "JETPACK_CUDA_MISMATCH"
198
+ description = (
199
+ "Check that the installed CUDA version matches the expected "
200
+ "version for the detected JetPack release."
201
+ )
202
+
203
+ def is_applicable(self, profile: SystemProfile) -> bool:
204
+ """Only applies on Jetson boards with a known JetPack version."""
205
+ return (
206
+ profile.is_embedded_board
207
+ and profile.jetpack_version is not None
208
+ and profile.jetpack_version in JETPACK_CUDA
209
+ )
210
+
211
+ def evaluate(
212
+ self,
213
+ profile: SystemProfile,
214
+ packages: List[PackageBinaryInfo],
215
+ ) -> List[Finding]:
216
+ """Compare detected CUDA with JetPack-expected CUDA."""
217
+ findings: List[Finding] = []
218
+
219
+ if profile.jetpack_version is None:
220
+ return findings
221
+
222
+ expected_cuda = JETPACK_CUDA.get(profile.jetpack_version)
223
+ if expected_cuda is None:
224
+ return findings
225
+
226
+ actual_cuda = profile.cuda_runtime_version or profile.cuda_toolkit_version
227
+ if actual_cuda is None:
228
+ findings.append(Finding(
229
+ rule_id=self.rule_id,
230
+ severity=Severity.WARNING,
231
+ title="CUDA not detected on Jetson board",
232
+ explanation=(
233
+ f"JetPack {profile.jetpack_version} expects CUDA "
234
+ f"{expected_cuda[0]}.{expected_cuda[1]}, but no CUDA "
235
+ f"installation was detected on this system."
236
+ ),
237
+ suggestion=(
238
+ "Install the CUDA toolkit that matches your JetPack "
239
+ "version, or ensure that nvidia-smi / nvcc is accessible."
240
+ ),
241
+ ))
242
+ return findings
243
+
244
+ if actual_cuda[0] != expected_cuda[0] or actual_cuda[1] != expected_cuda[1]:
245
+ severity = Severity.CRITICAL if actual_cuda[0] != expected_cuda[0] else Severity.WARNING
246
+ findings.append(Finding(
247
+ rule_id=self.rule_id,
248
+ severity=severity,
249
+ title="CUDA version does not match JetPack expectation",
250
+ explanation=(
251
+ f"JetPack {profile.jetpack_version} expects CUDA "
252
+ f"{expected_cuda[0]}.{expected_cuda[1]}, but CUDA "
253
+ f"{actual_cuda[0]}.{actual_cuda[1]} was detected."
254
+ ),
255
+ technical_detail=(
256
+ f"Expected CUDA {expected_cuda[0]}.{expected_cuda[1]} "
257
+ f"(JetPack {profile.jetpack_version}), "
258
+ f"found {actual_cuda[0]}.{actual_cuda[1]}."
259
+ ),
260
+ suggestion=(
261
+ "Re-flash the Jetson with the correct JetPack SDK, or "
262
+ "install the matching CUDA toolkit version. Mixing CUDA "
263
+ "versions on Jetson is not recommended."
264
+ ),
265
+ ))
266
+ else:
267
+ findings.append(Finding(
268
+ rule_id=self.rule_id,
269
+ severity=Severity.PASSED,
270
+ title="CUDA version matches JetPack expectation",
271
+ explanation=(
272
+ f"CUDA {actual_cuda[0]}.{actual_cuda[1]} matches "
273
+ f"JetPack {profile.jetpack_version} requirements."
274
+ ),
275
+ ))
276
+
277
+ return findings
278
+
279
+
280
+ class JetsonX86WheelRule(Rule):
281
+ """Detect x86-only wheels installed on an ARM Jetson board.
282
+
283
+ Wheels built for ``manylinux_*_x86_64`` or ``linux_x86_64`` will
284
+ contain shared objects compiled for the wrong architecture and will
285
+ fail with ``ImportError`` or ``OSError`` when loaded on the Jetson's
286
+ AArch64 CPU.
287
+ """
288
+
289
+ rule_id = "JETSON_X86_WHEEL"
290
+ description = (
291
+ "Detect x86-compiled wheels installed on an ARM Jetson board."
292
+ )
293
+
294
+ _X86_PLATFORM_FRAGMENTS = ("x86_64", "i686", "i386", "amd64")
295
+
296
+ def is_applicable(self, profile: SystemProfile) -> bool:
297
+ """Only applies on Jetson boards (AArch64 embedded)."""
298
+ return (
299
+ profile.is_embedded_board
300
+ and profile.architecture in (Architecture.AARCH64, Architecture.ARMV7L)
301
+ )
302
+
303
+ def evaluate(
304
+ self,
305
+ profile: SystemProfile,
306
+ packages: List[PackageBinaryInfo],
307
+ ) -> List[Finding]:
308
+ """Check each package's wheel tags and shared objects for x86 targets."""
309
+ findings: List[Finding] = []
310
+
311
+ for pkg in packages:
312
+ if pkg.is_pure_python:
313
+ continue
314
+
315
+ # Check wheel tags
316
+ for tag in pkg.wheel_tags:
317
+ platform_lower = tag.platform.lower()
318
+ if any(frag in platform_lower for frag in self._X86_PLATFORM_FRAGMENTS):
319
+ findings.append(Finding(
320
+ rule_id=self.rule_id,
321
+ severity=Severity.CRITICAL,
322
+ title=f"x86 wheel installed on ARM Jetson: {pkg.package_name}",
323
+ explanation=(
324
+ f"Package {pkg.package_name}=={pkg.package_version} "
325
+ f"was installed from an x86 wheel (platform tag: "
326
+ f"{tag.platform!r}), but this system is {profile.architecture.value}."
327
+ ),
328
+ technical_detail=(
329
+ f"Wheel tag: {tag.interpreter}-{tag.abi}-{tag.platform}"
330
+ ),
331
+ suggestion=(
332
+ f"Reinstall {pkg.package_name} with a wheel built "
333
+ f"for aarch64, or build from source: "
334
+ f"pip install --no-binary {pkg.package_name} {pkg.package_name}"
335
+ ),
336
+ package=pkg.package_name,
337
+ package_version=pkg.package_version,
338
+ ))
339
+ break # One finding per package is sufficient
340
+
341
+ # Also check shared objects for architecture mismatch
342
+ if not findings or findings[-1].package != pkg.package_name:
343
+ for so in pkg.shared_objects:
344
+ if so.architecture in (Architecture.X86_64, Architecture.I686):
345
+ findings.append(Finding(
346
+ rule_id=self.rule_id,
347
+ severity=Severity.CRITICAL,
348
+ title=f"x86 binary in package on ARM Jetson: {pkg.package_name}",
349
+ explanation=(
350
+ f"Shared object {so.filename} in "
351
+ f"{pkg.package_name}=={pkg.package_version} is "
352
+ f"compiled for {so.architecture.value}, but this "
353
+ f"Jetson runs {profile.architecture.value}."
354
+ ),
355
+ technical_detail=f"Binary: {so.path}",
356
+ suggestion=(
357
+ f"Reinstall {pkg.package_name} from an ARM-compatible "
358
+ f"wheel or build from source."
359
+ ),
360
+ package=pkg.package_name,
361
+ package_version=pkg.package_version,
362
+ ))
363
+ break # One finding per package is sufficient
364
+
365
+ return findings
366
+
367
+
368
+ # -- Plugin entry point -----------------------------------------------------
369
+
370
+
371
+ def register(registry: HookRegistry) -> None:
372
+ """Register Jetson extensions if running on a Tegra platform.
373
+
374
+ This function is called by the plugin loader. It only activates
375
+ when ``/etc/nv_tegra_release`` exists, indicating an L4T-based
376
+ Jetson system.
377
+ """
378
+ if not os.path.exists(_TEGRA_RELEASE_PATH):
379
+ logger.debug("Jetson plugin: /etc/nv_tegra_release not found; not activating")
380
+ return
381
+
382
+ registry.add_probe(JetsonProbe())
383
+ registry.add_rule(JetPackCUDARule())
384
+ registry.add_rule(JetsonX86WheelRule())
385
+ logger.info("Jetson plugin activated")