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,248 @@
1
+ """Probe for embedded / single-board computer detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ from typing import Any, Dict, Optional
8
+
9
+ from .base import ProbeBase
10
+
11
+
12
+ class BoardProbe(ProbeBase):
13
+ """Detects common embedded boards and single-board computers.
14
+
15
+ Supported boards:
16
+ - NVIDIA Jetson (Nano, TX2, Xavier, Orin)
17
+ - Raspberry Pi (all models)
18
+ - BeagleBone (Black, AI, etc.)
19
+ - Google Coral (Dev Board)
20
+ - Generic ARM SBCs (aarch64/armv7l without a desktop GPU)
21
+
22
+ Detection relies on ``/proc/device-tree/model``,
23
+ ``/etc/nv_tegra_release``, and device-node presence.
24
+ """
25
+
26
+ name = "board"
27
+
28
+ def collect(self) -> Dict[str, Any]:
29
+ """Return board detection results."""
30
+ data: Dict[str, Any] = {}
31
+
32
+ dt_model = self._read_device_tree_model()
33
+
34
+ # Try each board family in order of specificity
35
+ board = self._detect_jetson(dt_model)
36
+ if board is not None:
37
+ data["is_embedded_board"] = True
38
+ data["board_name"] = board["name"]
39
+ if board.get("jetpack_version"):
40
+ data["jetpack_version"] = board["jetpack_version"]
41
+ return data
42
+
43
+ board_name = self._detect_raspberry_pi(dt_model)
44
+ if board_name is not None:
45
+ data["is_embedded_board"] = True
46
+ data["board_name"] = board_name
47
+ return data
48
+
49
+ board_name = self._detect_beaglebone(dt_model)
50
+ if board_name is not None:
51
+ data["is_embedded_board"] = True
52
+ data["board_name"] = board_name
53
+ return data
54
+
55
+ board_name = self._detect_coral(dt_model)
56
+ if board_name is not None:
57
+ data["is_embedded_board"] = True
58
+ data["board_name"] = board_name
59
+ return data
60
+
61
+ board_name = self._detect_generic_arm_sbc(dt_model)
62
+ if board_name is not None:
63
+ data["is_embedded_board"] = True
64
+ data["board_name"] = board_name
65
+ return data
66
+
67
+ data["is_embedded_board"] = False
68
+ return data
69
+
70
+ # ------------------------------------------------------------------
71
+ # Common helpers
72
+ # ------------------------------------------------------------------
73
+
74
+ @staticmethod
75
+ def _read_device_tree_model() -> str:
76
+ """Read ``/proc/device-tree/model``.
77
+
78
+ The file is NUL-terminated on many kernels, so we strip trailing
79
+ NUL bytes and whitespace.
80
+ """
81
+ try:
82
+ with open("/proc/device-tree/model", "rb") as fh:
83
+ raw = fh.read()
84
+ return raw.decode("utf-8", errors="replace").strip("\x00").strip()
85
+ except (FileNotFoundError, PermissionError, OSError):
86
+ return ""
87
+
88
+ @staticmethod
89
+ def _read_text_file(path: str) -> str:
90
+ """Read a text file, returning empty string on failure."""
91
+ try:
92
+ with open(path, "r") as fh:
93
+ return fh.read().strip()
94
+ except (FileNotFoundError, PermissionError, OSError):
95
+ return ""
96
+
97
+ # ------------------------------------------------------------------
98
+ # Jetson
99
+ # ------------------------------------------------------------------
100
+
101
+ @staticmethod
102
+ def _detect_jetson(dt_model: str) -> Optional[Dict[str, str]]:
103
+ """Detect NVIDIA Jetson boards.
104
+
105
+ Primary indicator: ``/etc/nv_tegra_release`` exists.
106
+ Secondary: device-tree model contains "Jetson" or "NVIDIA".
107
+ """
108
+ tegra_release = BoardProbe._read_text_file("/etc/nv_tegra_release")
109
+ is_jetson = bool(tegra_release)
110
+
111
+ if not is_jetson:
112
+ # Fallback: check device-tree model
113
+ model_lower = dt_model.lower()
114
+ if "jetson" in model_lower or ("nvidia" in model_lower and "tegra" in model_lower):
115
+ is_jetson = True
116
+
117
+ if not is_jetson:
118
+ return None
119
+
120
+ name = dt_model if dt_model else "NVIDIA Jetson (unknown model)"
121
+
122
+ # Try to determine JetPack version from L4T version
123
+ jetpack_version = BoardProbe._jetpack_from_l4t(tegra_release)
124
+
125
+ result: Dict[str, str] = {"name": name}
126
+ if jetpack_version:
127
+ result["jetpack_version"] = jetpack_version
128
+
129
+ return result
130
+
131
+ @staticmethod
132
+ def _jetpack_from_l4t(tegra_release: str) -> str:
133
+ """Map L4T release string to JetPack version.
134
+
135
+ ``/etc/nv_tegra_release`` contains a line like::
136
+
137
+ # R35 (release), REVISION: 4.1, ...
138
+
139
+ Known L4T-to-JetPack mappings (approximate):
140
+ - R36.x -> JetPack 6.x
141
+ - R35.x -> JetPack 5.x
142
+ - R32.x -> JetPack 4.x
143
+ """
144
+ if not tegra_release:
145
+ return ""
146
+
147
+ import re
148
+
149
+ match = re.search(r"R(\d+)\s.*?REVISION:\s*(\d+(?:\.\d+)?)", tegra_release)
150
+ if not match:
151
+ return ""
152
+
153
+ major = int(match.group(1))
154
+ revision = match.group(2)
155
+ l4t_tag = f"L4T R{major} rev {revision}"
156
+
157
+ # Coarse mapping
158
+ if major >= 36:
159
+ return f"6.x ({l4t_tag})"
160
+ elif major >= 35:
161
+ return f"5.x ({l4t_tag})"
162
+ elif major >= 32:
163
+ return f"4.x ({l4t_tag})"
164
+ else:
165
+ return l4t_tag
166
+
167
+ # ------------------------------------------------------------------
168
+ # Raspberry Pi
169
+ # ------------------------------------------------------------------
170
+
171
+ @staticmethod
172
+ def _detect_raspberry_pi(dt_model: str) -> Optional[str]:
173
+ """Detect Raspberry Pi boards via the device-tree model string."""
174
+ if "raspberry pi" in dt_model.lower():
175
+ return dt_model
176
+ return None
177
+
178
+ # ------------------------------------------------------------------
179
+ # BeagleBone
180
+ # ------------------------------------------------------------------
181
+
182
+ @staticmethod
183
+ def _detect_beaglebone(dt_model: str) -> Optional[str]:
184
+ """Detect BeagleBone boards via the device-tree model string."""
185
+ if "beaglebone" in dt_model.lower() or "beagle bone" in dt_model.lower():
186
+ return dt_model
187
+ return None
188
+
189
+ # ------------------------------------------------------------------
190
+ # Google Coral
191
+ # ------------------------------------------------------------------
192
+
193
+ @staticmethod
194
+ def _detect_coral(dt_model: str) -> Optional[str]:
195
+ """Detect Google Coral dev board.
196
+
197
+ Checks the device-tree model and also looks for the Edge TPU
198
+ USB device node at ``/dev/apex_0``.
199
+ """
200
+ model_lower = dt_model.lower()
201
+ if "coral" in model_lower:
202
+ return dt_model
203
+
204
+ # Some Coral boards identify as "Freescale" in the DT model;
205
+ # check for the TPU device node as a secondary signal
206
+ if os.path.exists("/dev/apex_0"):
207
+ if dt_model:
208
+ return f"{dt_model} (Coral TPU detected)"
209
+ return "Google Coral (TPU detected)"
210
+
211
+ return None
212
+
213
+ # ------------------------------------------------------------------
214
+ # Generic ARM SBC
215
+ # ------------------------------------------------------------------
216
+
217
+ @staticmethod
218
+ def _detect_generic_arm_sbc(dt_model: str) -> Optional[str]:
219
+ """Detect a generic ARM single-board computer.
220
+
221
+ Criteria:
222
+ - Architecture is aarch64 or armv7l.
223
+ - No desktop GPU is detected (no ``/dev/nvidia*`` or ``/dev/dri/card*``).
224
+ - A device-tree model string is available (bare-metal servers
225
+ typically do not populate ``/proc/device-tree/model``).
226
+ """
227
+ machine = ""
228
+ try:
229
+ machine = platform.machine()
230
+ except Exception:
231
+ pass
232
+
233
+ if machine not in ("aarch64", "arm64", "armv7l", "armv6l"):
234
+ return None
235
+
236
+ # Must have a device-tree model to differentiate from cloud ARM servers
237
+ if not dt_model:
238
+ return None
239
+
240
+ # Exclude systems with a desktop GPU
241
+ has_desktop_gpu = (
242
+ os.path.exists("/dev/nvidia0")
243
+ or os.path.exists("/dev/dri/card0")
244
+ )
245
+ if has_desktop_gpu:
246
+ return None
247
+
248
+ return dt_model
@@ -0,0 +1,176 @@
1
+ """Probe for CPU architecture and feature flags."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ from typing import Any, Dict, FrozenSet
8
+
9
+ from pybinaryguard.models.enums import Architecture
10
+
11
+ from .base import ProbeBase
12
+
13
+
14
+ class CpuProbe(ProbeBase):
15
+ """Collects CPU architecture, model, core count, and feature flags.
16
+
17
+ On x86 the flags are read from the ``flags`` line in
18
+ ``/proc/cpuinfo``. On ARM/AArch64 the equivalent is the
19
+ ``Features`` line. This probe works without external dependencies.
20
+ """
21
+
22
+ name = "cpu"
23
+
24
+ def collect(self) -> Dict[str, Any]:
25
+ """Return architecture, CPU model, core count, and feature flags."""
26
+ data: Dict[str, Any] = {}
27
+
28
+ machine = self._get_machine()
29
+ data["architecture"] = Architecture.from_machine(machine)
30
+
31
+ cpuinfo = self._read_cpuinfo()
32
+
33
+ data["cpu_model"] = self._extract_cpu_model(cpuinfo, machine)
34
+ data["cpu_cores"] = self._get_core_count()
35
+
36
+ flags = self._extract_flags(cpuinfo, machine)
37
+ data["cpu_flags"] = flags
38
+
39
+ # Feature detection based on flags
40
+ data["has_sse42"] = self._has_flag(flags, {"sse4_2", "sse4.2"})
41
+ data["has_avx"] = self._has_flag(flags, {"avx"})
42
+ data["has_avx2"] = self._has_flag(flags, {"avx2"})
43
+ data["has_avx512"] = self._has_any_avx512(flags)
44
+ data["has_neon"] = self._detect_neon(flags, machine)
45
+
46
+ return data
47
+
48
+ # ------------------------------------------------------------------
49
+ # Internal helpers
50
+ # ------------------------------------------------------------------
51
+
52
+ @staticmethod
53
+ def _get_machine() -> str:
54
+ """Return the raw machine string from the platform module."""
55
+ try:
56
+ return platform.machine()
57
+ except Exception:
58
+ return ""
59
+
60
+ @staticmethod
61
+ def _read_cpuinfo() -> str:
62
+ """Read ``/proc/cpuinfo`` and return its content as a string."""
63
+ try:
64
+ with open("/proc/cpuinfo", "r") as fh:
65
+ return fh.read()
66
+ except (FileNotFoundError, PermissionError, OSError):
67
+ return ""
68
+
69
+ @staticmethod
70
+ def _extract_cpu_model(cpuinfo: str, machine: str) -> str:
71
+ """Extract the CPU model name from cpuinfo text.
72
+
73
+ On x86: ``model name : Intel(R) Core(TM) ...``
74
+ On ARM: ``Hardware : BCM2835`` or ``model name`` if present.
75
+ """
76
+ # Try "model name" first (works on x86 and some ARM)
77
+ for line in cpuinfo.splitlines():
78
+ stripped = line.strip()
79
+ if stripped.lower().startswith("model name"):
80
+ parts = stripped.split(":", 1)
81
+ if len(parts) == 2:
82
+ return parts[1].strip()
83
+
84
+ # ARM fallback: "Hardware" line
85
+ if machine.startswith(("aarch64", "arm")):
86
+ for line in cpuinfo.splitlines():
87
+ stripped = line.strip()
88
+ if stripped.lower().startswith("hardware"):
89
+ parts = stripped.split(":", 1)
90
+ if len(parts) == 2:
91
+ return parts[1].strip()
92
+
93
+ # Last resort: "CPU implementer" + "CPU part" (common on AArch64)
94
+ if machine.startswith(("aarch64", "arm")):
95
+ implementer = ""
96
+ part = ""
97
+ for line in cpuinfo.splitlines():
98
+ stripped = line.strip().lower()
99
+ if stripped.startswith("cpu implementer"):
100
+ p = stripped.split(":", 1)
101
+ if len(p) == 2:
102
+ implementer = p[1].strip()
103
+ elif stripped.startswith("cpu part"):
104
+ p = stripped.split(":", 1)
105
+ if len(p) == 2:
106
+ part = p[1].strip()
107
+ if implementer or part:
108
+ return f"implementer={implementer} part={part}"
109
+
110
+ return ""
111
+
112
+ @staticmethod
113
+ def _get_core_count() -> int:
114
+ """Return the number of logical CPU cores."""
115
+ try:
116
+ count = os.cpu_count()
117
+ return count if count is not None else 0
118
+ except Exception:
119
+ return 0
120
+
121
+ @staticmethod
122
+ def _extract_flags(cpuinfo: str, machine: str) -> FrozenSet[str]:
123
+ """Extract CPU feature flags from ``/proc/cpuinfo``.
124
+
125
+ On x86, flags appear on a ``flags`` line.
126
+ On ARM/AArch64, they appear on a ``Features`` line.
127
+ """
128
+ # Determine which key to look for
129
+ if machine in ("x86_64", "AMD64", "i686", "i386"):
130
+ key_candidates = ["flags"]
131
+ elif machine.startswith(("aarch64", "arm")):
132
+ key_candidates = ["features", "flags"]
133
+ else:
134
+ key_candidates = ["flags", "features"]
135
+
136
+ for line in cpuinfo.splitlines():
137
+ stripped = line.strip().lower()
138
+ for key in key_candidates:
139
+ if stripped.startswith(key):
140
+ parts = line.split(":", 1)
141
+ if len(parts) == 2:
142
+ flag_list = parts[1].strip().split()
143
+ return frozenset(f.lower() for f in flag_list)
144
+
145
+ return frozenset()
146
+
147
+ @staticmethod
148
+ def _has_flag(flags: FrozenSet[str], names: set) -> bool: # type: ignore[type-arg]
149
+ """Return ``True`` if *any* of the given names are present in *flags*."""
150
+ return bool(flags & names)
151
+
152
+ @staticmethod
153
+ def _has_any_avx512(flags: FrozenSet[str]) -> bool:
154
+ """Return ``True`` if any AVX-512 feature flag is present.
155
+
156
+ AVX-512 is reported as multiple sub-features (``avx512f``,
157
+ ``avx512bw``, ``avx512vl``, etc.). The foundation flag
158
+ ``avx512f`` must be present for any AVX-512 to be usable, but
159
+ we also accept any flag starting with ``avx512``.
160
+ """
161
+ return any(f.startswith("avx512") for f in flags)
162
+
163
+ @staticmethod
164
+ def _detect_neon(flags: FrozenSet[str], machine: str) -> bool:
165
+ """Detect ARM NEON support.
166
+
167
+ On AArch64 NEON is mandatory, so we return ``True`` even when
168
+ the ``neon`` flag is not explicitly listed in ``/proc/cpuinfo``.
169
+ On 32-bit ARM we look for the ``neon`` flag.
170
+ """
171
+ if machine in ("aarch64", "arm64"):
172
+ # NEON is mandatory in the ARMv8-A architecture
173
+ return True
174
+ if "neon" in flags:
175
+ return True
176
+ return False
@@ -0,0 +1,215 @@
1
+ """Probe for GLIBC / musl libc 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 GlibcProbe(ProbeBase):
15
+ """Collects C library version information (GLIBC or musl).
16
+
17
+ Detection strategy for GLIBC:
18
+ 1. ``os.confstr("CS_GNU_LIBC_VERSION")`` -- fast and authoritative.
19
+ 2. ctypes fallback: load ``libc.so.6`` and call ``gnu_get_libc_version()``.
20
+
21
+ Detection strategy for musl:
22
+ 1. Scan ``/proc/self/maps`` for paths containing ``musl``.
23
+ 2. Fall back to running ``ldd --version`` and inspecting stderr for
24
+ the ``musl`` banner.
25
+ """
26
+
27
+ name = "glibc"
28
+
29
+ def collect(self) -> Dict[str, Any]:
30
+ """Return ``glibc_version`` and/or ``musl_version``."""
31
+ data: Dict[str, Any] = {}
32
+
33
+ # Try musl first -- if the system is musl-based there is no glibc
34
+ musl_ver = self._detect_musl()
35
+ if musl_ver is not None:
36
+ data["musl_version"] = musl_ver
37
+ return data
38
+
39
+ # Try glibc
40
+ glibc_ver = self._detect_glibc()
41
+ if glibc_ver is not None:
42
+ data["glibc_version"] = glibc_ver
43
+
44
+ return data
45
+
46
+ # ------------------------------------------------------------------
47
+ # GLIBC detection
48
+ # ------------------------------------------------------------------
49
+
50
+ @staticmethod
51
+ def _detect_glibc() -> Optional[Tuple[int, int]]:
52
+ """Detect the GLIBC version.
53
+
54
+ Returns a ``(major, minor)`` tuple or ``None`` if GLIBC cannot be
55
+ found.
56
+ """
57
+ # Method 1: os.confstr
58
+ ver = GlibcProbe._glibc_via_confstr()
59
+ if ver is not None:
60
+ return ver
61
+
62
+ # Method 2: ctypes
63
+ ver = GlibcProbe._glibc_via_ctypes()
64
+ if ver is not None:
65
+ return ver
66
+
67
+ return None
68
+
69
+ @staticmethod
70
+ def _glibc_via_confstr() -> Optional[Tuple[int, int]]:
71
+ """Parse ``os.confstr('CS_GNU_LIBC_VERSION')``."""
72
+ try:
73
+ libc_string = os.confstr("CS_GNU_LIBC_VERSION") # e.g. "glibc 2.35"
74
+ if not libc_string:
75
+ return None
76
+ return GlibcProbe._parse_glibc_version_string(libc_string)
77
+ except (ValueError, OSError, AttributeError):
78
+ return None
79
+
80
+ @staticmethod
81
+ def _glibc_via_ctypes() -> Optional[Tuple[int, int]]:
82
+ """Load ``libc.so.6`` via ctypes and call ``gnu_get_libc_version``."""
83
+ try:
84
+ libc = ctypes.CDLL("libc.so.6")
85
+ gnu_get_libc_version = libc.gnu_get_libc_version
86
+ gnu_get_libc_version.restype = ctypes.c_char_p
87
+ version_bytes: bytes = gnu_get_libc_version()
88
+ version_str = version_bytes.decode("ascii", errors="replace")
89
+ return GlibcProbe._parse_dotted_version(version_str)
90
+ except (OSError, AttributeError, TypeError):
91
+ return None
92
+
93
+ @staticmethod
94
+ def _parse_glibc_version_string(s: str) -> Optional[Tuple[int, int]]:
95
+ """Parse a string like ``'glibc 2.35'`` into ``(2, 35)``."""
96
+ match = re.search(r"(\d+)\.(\d+)", s)
97
+ if match:
98
+ return (int(match.group(1)), int(match.group(2)))
99
+ return None
100
+
101
+ @staticmethod
102
+ def _parse_dotted_version(s: str) -> Optional[Tuple[int, int]]:
103
+ """Parse ``'2.35'`` into ``(2, 35)``."""
104
+ match = re.match(r"(\d+)\.(\d+)", s.strip())
105
+ if match:
106
+ return (int(match.group(1)), int(match.group(2)))
107
+ return None
108
+
109
+ # ------------------------------------------------------------------
110
+ # musl detection
111
+ # ------------------------------------------------------------------
112
+
113
+ @staticmethod
114
+ def _detect_musl() -> Optional[Tuple[int, int]]:
115
+ """Detect musl libc and its version.
116
+
117
+ Returns a ``(major, minor)`` tuple or ``None``.
118
+ """
119
+ # Method 1: /proc/self/maps
120
+ ver = GlibcProbe._musl_via_proc_maps()
121
+ if ver is not None:
122
+ return ver
123
+
124
+ # Method 2: ldd --version (musl ldd writes to stderr)
125
+ ver = GlibcProbe._musl_via_ldd()
126
+ if ver is not None:
127
+ return ver
128
+
129
+ return None
130
+
131
+ @staticmethod
132
+ def _musl_via_proc_maps() -> Optional[Tuple[int, int]]:
133
+ """Scan ``/proc/self/maps`` for musl shared objects.
134
+
135
+ A musl-based system maps something like::
136
+
137
+ 7f... /lib/ld-musl-x86_64.so.1
138
+
139
+ We then try to extract the version by reading the library's
140
+ banner (the first few bytes often contain the version string)
141
+ or by invoking the linker with ``--version``.
142
+ """
143
+ try:
144
+ with open("/proc/self/maps", "r") as fh:
145
+ for line in fh:
146
+ if "musl" in line:
147
+ # Extract the library path
148
+ parts = line.strip().split()
149
+ if len(parts) >= 6:
150
+ lib_path = parts[-1]
151
+ ver = GlibcProbe._musl_version_from_binary(lib_path)
152
+ if ver is not None:
153
+ return ver
154
+ # Even if we cannot determine the version, signal
155
+ # that musl is present with a fallback.
156
+ return (1, 0)
157
+ except (FileNotFoundError, PermissionError, OSError):
158
+ pass
159
+ return None
160
+
161
+ @staticmethod
162
+ def _musl_version_from_binary(path: str) -> Optional[Tuple[int, int]]:
163
+ """Execute the musl linker binary to extract version information.
164
+
165
+ ``ld-musl-*.so.1 --version`` prints something like::
166
+
167
+ musl libc (x86_64)
168
+ Version 1.2.3
169
+ """
170
+ try:
171
+ result = subprocess.run(
172
+ [path],
173
+ capture_output=True,
174
+ text=True,
175
+ timeout=5,
176
+ )
177
+ # musl's ld.so exits non-zero when invoked directly but
178
+ # prints version info to stderr.
179
+ output = result.stdout + result.stderr
180
+ return GlibcProbe._parse_musl_version_output(output)
181
+ except (FileNotFoundError, PermissionError, OSError, subprocess.TimeoutExpired):
182
+ return None
183
+
184
+ @staticmethod
185
+ def _musl_via_ldd() -> Optional[Tuple[int, int]]:
186
+ """Run ``ldd --version`` and parse musl's banner."""
187
+ try:
188
+ result = subprocess.run(
189
+ ["ldd", "--version"],
190
+ capture_output=True,
191
+ text=True,
192
+ timeout=5,
193
+ )
194
+ output = result.stdout + result.stderr
195
+ if "musl" not in output.lower():
196
+ return None
197
+ return GlibcProbe._parse_musl_version_output(output)
198
+ except (FileNotFoundError, PermissionError, OSError, subprocess.TimeoutExpired):
199
+ return None
200
+
201
+ @staticmethod
202
+ def _parse_musl_version_output(output: str) -> Optional[Tuple[int, int]]:
203
+ """Extract musl version from its banner text.
204
+
205
+ Looks for patterns like ``Version 1.2.3`` or ``musl libc ... 1.2.3``.
206
+ """
207
+ # "Version 1.2.3"
208
+ match = re.search(r"[Vv]ersion\s+(\d+)\.(\d+)", output)
209
+ if match:
210
+ return (int(match.group(1)), int(match.group(2)))
211
+ # Fallback: any x.y after "musl"
212
+ match = re.search(r"musl.*?(\d+)\.(\d+)", output, re.IGNORECASE)
213
+ if match:
214
+ return (int(match.group(1)), int(match.group(2)))
215
+ return None