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,430 @@
|
|
|
1
|
+
"""Probe for GPU, CUDA, and cuDNN information."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import Any, Dict, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from .base import ProbeBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GpuProbe(ProbeBase):
|
|
15
|
+
"""Layered GPU detection probe.
|
|
16
|
+
|
|
17
|
+
Detection layers (tried in order, each one additive):
|
|
18
|
+
|
|
19
|
+
1. Device nodes: ``/dev/nvidia0``, ``/dev/nvidiactl``
|
|
20
|
+
2. Driver version: ``/proc/driver/nvidia/version``
|
|
21
|
+
3. NVML via ctypes: ``libnvidia-ml.so.1`` -- GPU name, memory,
|
|
22
|
+
compute capability, driver version
|
|
23
|
+
4. CUDA runtime via ctypes: ``libcudart.so`` -- runtime version
|
|
24
|
+
5. CUDA environment: ``CUDA_HOME`` / ``CUDA_PATH``, ``nvcc``
|
|
25
|
+
6. Jetson-specific: ``/etc/nv_tegra_release`` for L4T / JetPack
|
|
26
|
+
7. cuDNN via ctypes: ``libcudnn.so`` -- version
|
|
27
|
+
|
|
28
|
+
All layers are optional. If no GPU libraries are available the
|
|
29
|
+
probe returns ``gpu_available=False`` and nothing else.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
name = "gpu"
|
|
33
|
+
|
|
34
|
+
def collect(self) -> Dict[str, Any]:
|
|
35
|
+
"""Return GPU, CUDA, and cuDNN information."""
|
|
36
|
+
data: Dict[str, Any] = {}
|
|
37
|
+
|
|
38
|
+
# Layer 1: device nodes
|
|
39
|
+
has_device = self._check_device_nodes()
|
|
40
|
+
|
|
41
|
+
# Layer 2: driver version from /proc
|
|
42
|
+
driver_version = self._parse_proc_driver_version()
|
|
43
|
+
if driver_version:
|
|
44
|
+
data["gpu_driver_version"] = driver_version
|
|
45
|
+
|
|
46
|
+
# Layer 3: NVML
|
|
47
|
+
nvml_info = self._query_nvml()
|
|
48
|
+
if nvml_info:
|
|
49
|
+
data.update(nvml_info)
|
|
50
|
+
|
|
51
|
+
# Layer 4: CUDA runtime
|
|
52
|
+
cuda_runtime = self._query_cuda_runtime()
|
|
53
|
+
if cuda_runtime:
|
|
54
|
+
data["cuda_runtime_version"] = cuda_runtime
|
|
55
|
+
|
|
56
|
+
# Layer 5: CUDA toolkit from environment
|
|
57
|
+
cuda_toolkit = self._detect_cuda_toolkit()
|
|
58
|
+
if cuda_toolkit:
|
|
59
|
+
data["cuda_toolkit_version"] = cuda_toolkit
|
|
60
|
+
|
|
61
|
+
# Layer 6: Jetson-specific L4T info
|
|
62
|
+
jetson_info = self._detect_jetson_gpu()
|
|
63
|
+
if jetson_info:
|
|
64
|
+
# Jetson has an integrated GPU -- mark available even if
|
|
65
|
+
# /dev/nvidia0 is absent (Jetson uses /dev/nvhost-*)
|
|
66
|
+
has_device = True
|
|
67
|
+
# Only set fields that weren't already populated
|
|
68
|
+
for key, value in jetson_info.items():
|
|
69
|
+
if key not in data:
|
|
70
|
+
data[key] = value
|
|
71
|
+
|
|
72
|
+
# Layer 7: cuDNN
|
|
73
|
+
cudnn_ver = self._detect_cudnn()
|
|
74
|
+
if cudnn_ver:
|
|
75
|
+
data["cudnn_version"] = cudnn_ver
|
|
76
|
+
|
|
77
|
+
# Final: determine gpu_available
|
|
78
|
+
data["gpu_available"] = has_device or bool(data.get("gpu_driver_version"))
|
|
79
|
+
|
|
80
|
+
return data
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
# Layer 1: Device nodes
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _check_device_nodes() -> bool:
|
|
88
|
+
"""Check for NVIDIA device nodes in ``/dev``."""
|
|
89
|
+
return os.path.exists("/dev/nvidia0") or os.path.exists("/dev/nvidiactl")
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
# Layer 2: /proc driver version
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _parse_proc_driver_version() -> Optional[str]:
|
|
97
|
+
"""Parse ``/proc/driver/nvidia/version`` for the driver version.
|
|
98
|
+
|
|
99
|
+
Example content::
|
|
100
|
+
|
|
101
|
+
NVRM version: NVIDIA UNIX x86_64 Kernel Module 535.129.03 ...
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
with open("/proc/driver/nvidia/version", "r") as fh:
|
|
105
|
+
content = fh.read()
|
|
106
|
+
except (FileNotFoundError, PermissionError, OSError):
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
match = re.search(r"Kernel Module\s+([\d.]+)", content)
|
|
110
|
+
if match:
|
|
111
|
+
return match.group(1)
|
|
112
|
+
|
|
113
|
+
# Fallback: look for any version-like pattern on the NVRM line
|
|
114
|
+
for line in content.splitlines():
|
|
115
|
+
if "NVRM" in line:
|
|
116
|
+
ver_match = re.search(r"(\d+\.\d+(?:\.\d+)?)", line)
|
|
117
|
+
if ver_match:
|
|
118
|
+
return ver_match.group(1)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
# Layer 3: NVML
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _query_nvml() -> Optional[Dict[str, Any]]:
|
|
127
|
+
"""Query NVIDIA Management Library (NVML) via ctypes.
|
|
128
|
+
|
|
129
|
+
Collects GPU name, memory, compute capability, and driver version
|
|
130
|
+
for the first GPU (device index 0).
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
nvml = ctypes.CDLL("libnvidia-ml.so.1")
|
|
134
|
+
except OSError:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
result: Dict[str, Any] = {}
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# nvmlInit_v2
|
|
141
|
+
ret = nvml.nvmlInit_v2()
|
|
142
|
+
if ret != 0:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
# Driver version
|
|
146
|
+
buf = ctypes.create_string_buffer(256)
|
|
147
|
+
ret = nvml.nvmlSystemGetDriverVersion(buf, ctypes.c_uint(256))
|
|
148
|
+
if ret == 0:
|
|
149
|
+
result["gpu_driver_version"] = buf.value.decode("utf-8", errors="replace")
|
|
150
|
+
|
|
151
|
+
# Get device handle for GPU 0
|
|
152
|
+
handle = ctypes.c_void_p()
|
|
153
|
+
ret = nvml.nvmlDeviceGetHandleByIndex_v2(ctypes.c_uint(0), ctypes.byref(handle))
|
|
154
|
+
if ret != 0:
|
|
155
|
+
# Try legacy API
|
|
156
|
+
ret = nvml.nvmlDeviceGetHandleByIndex(ctypes.c_uint(0), ctypes.byref(handle))
|
|
157
|
+
if ret != 0:
|
|
158
|
+
nvml.nvmlShutdown()
|
|
159
|
+
return result if result else None
|
|
160
|
+
|
|
161
|
+
# GPU name
|
|
162
|
+
name_buf = ctypes.create_string_buffer(256)
|
|
163
|
+
ret = nvml.nvmlDeviceGetName(handle, name_buf, ctypes.c_uint(256))
|
|
164
|
+
if ret == 0:
|
|
165
|
+
result["gpu_name"] = name_buf.value.decode("utf-8", errors="replace")
|
|
166
|
+
|
|
167
|
+
# Memory info
|
|
168
|
+
class NvmlMemory(ctypes.Structure):
|
|
169
|
+
_fields_ = [
|
|
170
|
+
("total", ctypes.c_ulonglong),
|
|
171
|
+
("free", ctypes.c_ulonglong),
|
|
172
|
+
("used", ctypes.c_ulonglong),
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
mem = NvmlMemory()
|
|
176
|
+
ret = nvml.nvmlDeviceGetMemoryInfo(handle, ctypes.byref(mem))
|
|
177
|
+
if ret == 0:
|
|
178
|
+
result["gpu_memory_mb"] = int(mem.total // (1024 * 1024))
|
|
179
|
+
|
|
180
|
+
# Compute capability
|
|
181
|
+
major = ctypes.c_int()
|
|
182
|
+
minor = ctypes.c_int()
|
|
183
|
+
ret = nvml.nvmlDeviceGetCudaComputeCapability(
|
|
184
|
+
handle, ctypes.byref(major), ctypes.byref(minor)
|
|
185
|
+
)
|
|
186
|
+
if ret == 0:
|
|
187
|
+
result["gpu_compute_capability"] = (major.value, minor.value)
|
|
188
|
+
|
|
189
|
+
nvml.nvmlShutdown()
|
|
190
|
+
except Exception:
|
|
191
|
+
# Catch any ctypes-level errors
|
|
192
|
+
try:
|
|
193
|
+
nvml.nvmlShutdown()
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
return result if result else None
|
|
197
|
+
|
|
198
|
+
return result if result else None
|
|
199
|
+
|
|
200
|
+
# ------------------------------------------------------------------
|
|
201
|
+
# Layer 4: CUDA runtime
|
|
202
|
+
# ------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _query_cuda_runtime() -> Optional[Tuple[int, int]]:
|
|
206
|
+
"""Query the CUDA runtime library for its version.
|
|
207
|
+
|
|
208
|
+
Tries common soname patterns for ``libcudart``.
|
|
209
|
+
"""
|
|
210
|
+
lib_names = [
|
|
211
|
+
"libcudart.so",
|
|
212
|
+
"libcudart.so.12",
|
|
213
|
+
"libcudart.so.11.0",
|
|
214
|
+
"libcudart.so.11",
|
|
215
|
+
"libcudart.so.10.2",
|
|
216
|
+
"libcudart.so.10.1",
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
for lib_name in lib_names:
|
|
220
|
+
try:
|
|
221
|
+
cudart = ctypes.CDLL(lib_name)
|
|
222
|
+
version = ctypes.c_int()
|
|
223
|
+
ret = cudart.cudaRuntimeGetVersion(ctypes.byref(version))
|
|
224
|
+
if ret == 0 and version.value > 0:
|
|
225
|
+
# CUDA encodes version as major*1000 + minor*10
|
|
226
|
+
major = version.value // 1000
|
|
227
|
+
minor = (version.value % 1000) // 10
|
|
228
|
+
return (major, minor)
|
|
229
|
+
except (OSError, AttributeError):
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
# ------------------------------------------------------------------
|
|
235
|
+
# Layer 5: CUDA toolkit from environment
|
|
236
|
+
# ------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _detect_cuda_toolkit() -> Optional[Tuple[int, int]]:
|
|
240
|
+
"""Detect the CUDA toolkit version from environment variables and nvcc.
|
|
241
|
+
|
|
242
|
+
Checks ``CUDA_HOME``, ``CUDA_PATH``, and common install
|
|
243
|
+
directories for ``nvcc --version`` output.
|
|
244
|
+
"""
|
|
245
|
+
cuda_dirs = []
|
|
246
|
+
|
|
247
|
+
# Environment variables
|
|
248
|
+
for env_var in ("CUDA_HOME", "CUDA_PATH"):
|
|
249
|
+
val = os.environ.get(env_var, "")
|
|
250
|
+
if val and os.path.isdir(val):
|
|
251
|
+
cuda_dirs.append(val)
|
|
252
|
+
|
|
253
|
+
# Common install paths
|
|
254
|
+
common_paths = [
|
|
255
|
+
"/usr/local/cuda",
|
|
256
|
+
"/usr/local/cuda-12",
|
|
257
|
+
"/usr/local/cuda-11",
|
|
258
|
+
"/opt/cuda",
|
|
259
|
+
]
|
|
260
|
+
for path in common_paths:
|
|
261
|
+
if os.path.isdir(path) and path not in cuda_dirs:
|
|
262
|
+
cuda_dirs.append(path)
|
|
263
|
+
|
|
264
|
+
# Try nvcc in each candidate directory
|
|
265
|
+
for cuda_dir in cuda_dirs:
|
|
266
|
+
nvcc = os.path.join(cuda_dir, "bin", "nvcc")
|
|
267
|
+
ver = GpuProbe._nvcc_version(nvcc)
|
|
268
|
+
if ver is not None:
|
|
269
|
+
return ver
|
|
270
|
+
|
|
271
|
+
# Try nvcc on PATH
|
|
272
|
+
ver = GpuProbe._nvcc_version("nvcc")
|
|
273
|
+
if ver is not None:
|
|
274
|
+
return ver
|
|
275
|
+
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def _nvcc_version(nvcc_path: str) -> Optional[Tuple[int, int]]:
|
|
280
|
+
"""Run ``nvcc --version`` and parse the CUDA version.
|
|
281
|
+
|
|
282
|
+
Example output line::
|
|
283
|
+
|
|
284
|
+
Cuda compilation tools, release 12.2, V12.2.140
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
result = subprocess.run(
|
|
288
|
+
[nvcc_path, "--version"],
|
|
289
|
+
capture_output=True,
|
|
290
|
+
text=True,
|
|
291
|
+
timeout=5,
|
|
292
|
+
)
|
|
293
|
+
output = result.stdout + result.stderr
|
|
294
|
+
match = re.search(r"release\s+(\d+)\.(\d+)", output)
|
|
295
|
+
if match:
|
|
296
|
+
return (int(match.group(1)), int(match.group(2)))
|
|
297
|
+
except (FileNotFoundError, PermissionError, OSError, subprocess.TimeoutExpired):
|
|
298
|
+
pass
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
# ------------------------------------------------------------------
|
|
302
|
+
# Layer 6: Jetson / L4T
|
|
303
|
+
# ------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def _detect_jetson_gpu() -> Optional[Dict[str, Any]]:
|
|
307
|
+
"""Detect Jetson integrated GPU via ``/etc/nv_tegra_release``.
|
|
308
|
+
|
|
309
|
+
Jetson devices have an integrated NVIDIA GPU that does not
|
|
310
|
+
expose ``/dev/nvidia0`` -- they use ``/dev/nvhost-*`` instead.
|
|
311
|
+
"""
|
|
312
|
+
try:
|
|
313
|
+
with open("/etc/nv_tegra_release", "r") as fh:
|
|
314
|
+
content = fh.read()
|
|
315
|
+
except (FileNotFoundError, PermissionError, OSError):
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
if not content.strip():
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
info: Dict[str, Any] = {}
|
|
322
|
+
info["gpu_name"] = "NVIDIA Tegra (Jetson)"
|
|
323
|
+
|
|
324
|
+
# Try to determine CUDA version from the toolkit symlink
|
|
325
|
+
cuda_ver = GpuProbe._detect_cuda_toolkit()
|
|
326
|
+
if cuda_ver:
|
|
327
|
+
info["cuda_toolkit_version"] = cuda_ver
|
|
328
|
+
|
|
329
|
+
return info
|
|
330
|
+
|
|
331
|
+
# ------------------------------------------------------------------
|
|
332
|
+
# Layer 7: cuDNN
|
|
333
|
+
# ------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
@staticmethod
|
|
336
|
+
def _detect_cudnn() -> Optional[Tuple[int, int, int]]:
|
|
337
|
+
"""Detect cuDNN version via ctypes or header file.
|
|
338
|
+
|
|
339
|
+
Tries:
|
|
340
|
+
1. Loading ``libcudnn.so`` and calling ``cudnnGetVersion()``.
|
|
341
|
+
2. Parsing ``cudnn_version.h`` from CUDA include directories.
|
|
342
|
+
"""
|
|
343
|
+
# Method 1: ctypes
|
|
344
|
+
ver = GpuProbe._cudnn_via_ctypes()
|
|
345
|
+
if ver is not None:
|
|
346
|
+
return ver
|
|
347
|
+
|
|
348
|
+
# Method 2: header file
|
|
349
|
+
ver = GpuProbe._cudnn_via_header()
|
|
350
|
+
if ver is not None:
|
|
351
|
+
return ver
|
|
352
|
+
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
@staticmethod
|
|
356
|
+
def _cudnn_via_ctypes() -> Optional[Tuple[int, int, int]]:
|
|
357
|
+
"""Load ``libcudnn.so`` and query the version."""
|
|
358
|
+
lib_names = [
|
|
359
|
+
"libcudnn.so",
|
|
360
|
+
"libcudnn.so.9",
|
|
361
|
+
"libcudnn.so.8",
|
|
362
|
+
"libcudnn.so.7",
|
|
363
|
+
]
|
|
364
|
+
for lib_name in lib_names:
|
|
365
|
+
try:
|
|
366
|
+
cudnn = ctypes.CDLL(lib_name)
|
|
367
|
+
cudnn.cudnnGetVersion.restype = ctypes.c_size_t
|
|
368
|
+
version = cudnn.cudnnGetVersion()
|
|
369
|
+
if version > 0:
|
|
370
|
+
# cuDNN encodes version as major*1000 + minor*100 + patch
|
|
371
|
+
major = version // 1000
|
|
372
|
+
minor = (version % 1000) // 100
|
|
373
|
+
patch = version % 100
|
|
374
|
+
return (major, minor, patch)
|
|
375
|
+
except (OSError, AttributeError):
|
|
376
|
+
continue
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def _cudnn_via_header() -> Optional[Tuple[int, int, int]]:
|
|
381
|
+
"""Parse cuDNN version from ``cudnn_version.h``.
|
|
382
|
+
|
|
383
|
+
The header defines::
|
|
384
|
+
|
|
385
|
+
#define CUDNN_MAJOR 8
|
|
386
|
+
#define CUDNN_MINOR 9
|
|
387
|
+
#define CUDNN_PATCHLEVEL 7
|
|
388
|
+
"""
|
|
389
|
+
search_dirs = []
|
|
390
|
+
|
|
391
|
+
# CUDA_HOME / CUDA_PATH
|
|
392
|
+
for env_var in ("CUDA_HOME", "CUDA_PATH"):
|
|
393
|
+
val = os.environ.get(env_var, "")
|
|
394
|
+
if val:
|
|
395
|
+
search_dirs.append(os.path.join(val, "include"))
|
|
396
|
+
|
|
397
|
+
# Common locations
|
|
398
|
+
search_dirs.extend([
|
|
399
|
+
"/usr/local/cuda/include",
|
|
400
|
+
"/usr/include",
|
|
401
|
+
"/usr/include/x86_64-linux-gnu",
|
|
402
|
+
"/usr/include/aarch64-linux-gnu",
|
|
403
|
+
])
|
|
404
|
+
|
|
405
|
+
for include_dir in search_dirs:
|
|
406
|
+
# Try cudnn_version.h first (cuDNN >= 8), then cudnn.h
|
|
407
|
+
for header_name in ("cudnn_version.h", "cudnn.h"):
|
|
408
|
+
header_path = os.path.join(include_dir, header_name)
|
|
409
|
+
ver = GpuProbe._parse_cudnn_header(header_path)
|
|
410
|
+
if ver is not None:
|
|
411
|
+
return ver
|
|
412
|
+
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
@staticmethod
|
|
416
|
+
def _parse_cudnn_header(path: str) -> Optional[Tuple[int, int, int]]:
|
|
417
|
+
"""Extract CUDNN_MAJOR, CUDNN_MINOR, CUDNN_PATCHLEVEL from a header."""
|
|
418
|
+
try:
|
|
419
|
+
with open(path, "r") as fh:
|
|
420
|
+
content = fh.read()
|
|
421
|
+
except (FileNotFoundError, PermissionError, OSError):
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
major_m = re.search(r"#define\s+CUDNN_MAJOR\s+(\d+)", content)
|
|
425
|
+
minor_m = re.search(r"#define\s+CUDNN_MINOR\s+(\d+)", content)
|
|
426
|
+
patch_m = re.search(r"#define\s+CUDNN_PATCHLEVEL\s+(\d+)", content)
|
|
427
|
+
|
|
428
|
+
if major_m and minor_m and patch_m:
|
|
429
|
+
return (int(major_m.group(1)), int(minor_m.group(1)), int(patch_m.group(1)))
|
|
430
|
+
return None
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Probe for shared library paths and linker cache."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import site
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any, Dict, List, Tuple
|
|
11
|
+
|
|
12
|
+
from .base import ProbeBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LibraryProbe(ProbeBase):
|
|
16
|
+
"""Collects shared library path and linker cache information.
|
|
17
|
+
|
|
18
|
+
Gathered fields:
|
|
19
|
+
- ``ld_library_path``: directories from ``$LD_LIBRARY_PATH``
|
|
20
|
+
- ``ldconfig_cache``: mapping of library names to resolved paths
|
|
21
|
+
as reported by ``ldconfig -p``
|
|
22
|
+
- ``site_packages_paths``: Python site-packages directories
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
name = "library"
|
|
26
|
+
|
|
27
|
+
def collect(self) -> Dict[str, Any]:
|
|
28
|
+
"""Return library path, ldconfig cache, and site-packages paths."""
|
|
29
|
+
data: Dict[str, Any] = {}
|
|
30
|
+
|
|
31
|
+
data["ld_library_path"] = self._get_ld_library_path()
|
|
32
|
+
data["ldconfig_cache"] = self._get_ldconfig_cache()
|
|
33
|
+
data["site_packages_paths"] = self._get_site_packages()
|
|
34
|
+
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
# LD_LIBRARY_PATH
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def _get_ld_library_path() -> Tuple[str, ...]:
|
|
43
|
+
"""Parse ``$LD_LIBRARY_PATH`` into a tuple of directory strings.
|
|
44
|
+
|
|
45
|
+
Empty and duplicate entries are preserved because the dynamic
|
|
46
|
+
linker processes them in order and their semantics may matter.
|
|
47
|
+
"""
|
|
48
|
+
raw = os.environ.get("LD_LIBRARY_PATH", "")
|
|
49
|
+
if not raw:
|
|
50
|
+
return ()
|
|
51
|
+
return tuple(raw.split(os.pathsep))
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------------------
|
|
54
|
+
# ldconfig cache
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _get_ldconfig_cache() -> Dict[str, str]:
|
|
59
|
+
"""Run ``ldconfig -p`` and parse the output into a dict.
|
|
60
|
+
|
|
61
|
+
Returns a mapping of library soname to its absolute file path,
|
|
62
|
+
e.g. ``{"libz.so.1": "/lib/x86_64-linux-gnu/libz.so.1"}``.
|
|
63
|
+
|
|
64
|
+
Only the *first* occurrence of each soname is kept (ldconfig
|
|
65
|
+
prints in search order).
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
["ldconfig", "-p"],
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
timeout=5,
|
|
73
|
+
)
|
|
74
|
+
if result.returncode != 0:
|
|
75
|
+
return {}
|
|
76
|
+
return LibraryProbe._parse_ldconfig_output(result.stdout)
|
|
77
|
+
except (FileNotFoundError, PermissionError, OSError, subprocess.TimeoutExpired):
|
|
78
|
+
return {}
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _parse_ldconfig_output(output: str) -> Dict[str, str]:
|
|
82
|
+
"""Parse the human-readable output of ``ldconfig -p``.
|
|
83
|
+
|
|
84
|
+
Each line after the header looks like::
|
|
85
|
+
|
|
86
|
+
\tlibz.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libz.so.1
|
|
87
|
+
"""
|
|
88
|
+
cache: Dict[str, str] = {}
|
|
89
|
+
# Pattern: leading whitespace, soname, parenthesised flags, =>, path
|
|
90
|
+
pattern = re.compile(r"^\s+(\S+)\s+\(.*?\)\s+=>\s+(\S+)")
|
|
91
|
+
for line in output.splitlines():
|
|
92
|
+
match = pattern.match(line)
|
|
93
|
+
if match:
|
|
94
|
+
soname = match.group(1)
|
|
95
|
+
path = match.group(2)
|
|
96
|
+
# Keep only the first occurrence
|
|
97
|
+
if soname not in cache:
|
|
98
|
+
cache[soname] = path
|
|
99
|
+
return cache
|
|
100
|
+
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
# site-packages
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _get_site_packages() -> Tuple[str, ...]:
|
|
107
|
+
"""Return all site-packages directories known to the interpreter.
|
|
108
|
+
|
|
109
|
+
Combines ``site.getsitepackages()`` (system-level) with
|
|
110
|
+
``site.getusersitepackages()`` (user-level).
|
|
111
|
+
"""
|
|
112
|
+
paths: List[str] = []
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
system_paths = site.getsitepackages()
|
|
116
|
+
paths.extend(system_paths)
|
|
117
|
+
except (AttributeError, Exception):
|
|
118
|
+
# site.getsitepackages may not exist in virtualenvs
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
user_path = site.getusersitepackages()
|
|
123
|
+
if isinstance(user_path, str):
|
|
124
|
+
paths.append(user_path)
|
|
125
|
+
except (AttributeError, Exception):
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
# Fallback: derive from sys.path if the above yielded nothing
|
|
129
|
+
if not paths:
|
|
130
|
+
paths = [p for p in sys.path if "site-packages" in p or "dist-packages" in p]
|
|
131
|
+
|
|
132
|
+
return tuple(paths)
|