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