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,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)