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,227 @@
|
|
|
1
|
+
"""Probe for OS, kernel, and virtualisation environment information."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from pybinaryguard.models.enums import ContainerRuntime
|
|
10
|
+
|
|
11
|
+
from .base import ProbeBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OsProbe(ProbeBase):
|
|
15
|
+
"""Collects operating-system and runtime-environment metadata.
|
|
16
|
+
|
|
17
|
+
- Distro name and version from ``/etc/os-release``.
|
|
18
|
+
- Kernel version from ``platform.release()``.
|
|
19
|
+
- Container detection: ``/.dockerenv``, ``/proc/1/cgroup``, env vars.
|
|
20
|
+
- VM detection: ``/sys/class/dmi/id/`` sysfs entries.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name = "os"
|
|
24
|
+
|
|
25
|
+
def collect(self) -> Dict[str, Any]:
|
|
26
|
+
"""Return OS, kernel, container, and VM information."""
|
|
27
|
+
data: Dict[str, Any] = {}
|
|
28
|
+
|
|
29
|
+
os_info = self._parse_os_release()
|
|
30
|
+
data["os_name"] = os_info.get("name", "")
|
|
31
|
+
data["os_version"] = os_info.get("version_id", os_info.get("version", ""))
|
|
32
|
+
data["kernel_version"] = self._get_kernel_version()
|
|
33
|
+
|
|
34
|
+
container_info = self._detect_container()
|
|
35
|
+
data["is_container"] = container_info[0]
|
|
36
|
+
data["container_runtime"] = container_info[1]
|
|
37
|
+
|
|
38
|
+
data["is_virtual_machine"] = self._detect_vm()
|
|
39
|
+
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
# OS release
|
|
44
|
+
# ------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _parse_os_release() -> Dict[str, str]:
|
|
48
|
+
"""Parse ``/etc/os-release`` (or ``/usr/lib/os-release`` fallback).
|
|
49
|
+
|
|
50
|
+
Returns a dictionary with keys lower-cased. Values are unquoted.
|
|
51
|
+
"""
|
|
52
|
+
for path in ("/etc/os-release", "/usr/lib/os-release"):
|
|
53
|
+
try:
|
|
54
|
+
with open(path, "r") as fh:
|
|
55
|
+
return OsProbe._parse_release_file(fh.read())
|
|
56
|
+
except (FileNotFoundError, PermissionError, OSError):
|
|
57
|
+
continue
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _parse_release_file(text: str) -> Dict[str, str]:
|
|
62
|
+
"""Parse key=value pairs from an os-release style file."""
|
|
63
|
+
result: Dict[str, str] = {}
|
|
64
|
+
for line in text.splitlines():
|
|
65
|
+
line = line.strip()
|
|
66
|
+
if not line or line.startswith("#"):
|
|
67
|
+
continue
|
|
68
|
+
if "=" not in line:
|
|
69
|
+
continue
|
|
70
|
+
key, _, value = line.partition("=")
|
|
71
|
+
key = key.strip().lower()
|
|
72
|
+
value = value.strip()
|
|
73
|
+
# Remove surrounding quotes (single or double)
|
|
74
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
|
|
75
|
+
value = value[1:-1]
|
|
76
|
+
result[key] = value
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _get_kernel_version() -> str:
|
|
81
|
+
"""Return the kernel version string."""
|
|
82
|
+
try:
|
|
83
|
+
return platform.release()
|
|
84
|
+
except Exception:
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
# Container detection
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _detect_container() -> tuple: # type: ignore[type-arg]
|
|
93
|
+
"""Detect whether we are running inside a container.
|
|
94
|
+
|
|
95
|
+
Returns ``(is_container: bool, runtime: ContainerRuntime)``.
|
|
96
|
+
"""
|
|
97
|
+
# Check /.dockerenv -- Docker creates this file
|
|
98
|
+
if os.path.exists("/.dockerenv"):
|
|
99
|
+
return (True, ContainerRuntime.DOCKER)
|
|
100
|
+
|
|
101
|
+
# Check for Podman-specific environment variable
|
|
102
|
+
if os.environ.get("container") == "podman":
|
|
103
|
+
return (True, ContainerRuntime.PODMAN)
|
|
104
|
+
|
|
105
|
+
# Check environment hints
|
|
106
|
+
if os.environ.get("container") == "lxc":
|
|
107
|
+
return (True, ContainerRuntime.LXC)
|
|
108
|
+
|
|
109
|
+
# Check /run/.containerenv (Podman)
|
|
110
|
+
if os.path.exists("/run/.containerenv"):
|
|
111
|
+
return (True, ContainerRuntime.PODMAN)
|
|
112
|
+
|
|
113
|
+
# Check /proc/1/cgroup for container signals
|
|
114
|
+
runtime = OsProbe._detect_container_from_cgroup()
|
|
115
|
+
if runtime is not None:
|
|
116
|
+
return (True, runtime)
|
|
117
|
+
|
|
118
|
+
# Check /proc/1/environ for container_runtime (requires root)
|
|
119
|
+
runtime = OsProbe._detect_container_from_mountinfo()
|
|
120
|
+
if runtime is not None:
|
|
121
|
+
return (True, runtime)
|
|
122
|
+
|
|
123
|
+
return (False, ContainerRuntime.NONE)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _detect_container_from_cgroup() -> Optional[ContainerRuntime]:
|
|
127
|
+
"""Inspect ``/proc/1/cgroup`` for container runtime indicators.
|
|
128
|
+
|
|
129
|
+
Docker cgroups contain ``/docker/``, containerd uses
|
|
130
|
+
``/cri-containerd-``, and LXC uses ``/lxc/``.
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
with open("/proc/1/cgroup", "r") as fh:
|
|
134
|
+
content = fh.read()
|
|
135
|
+
except (FileNotFoundError, PermissionError, OSError):
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
if "/docker/" in content or "/docker-" in content:
|
|
139
|
+
return ContainerRuntime.DOCKER
|
|
140
|
+
if "/lxc/" in content:
|
|
141
|
+
return ContainerRuntime.LXC
|
|
142
|
+
if "containerd" in content or "/cri-containerd-" in content:
|
|
143
|
+
return ContainerRuntime.CONTAINERD
|
|
144
|
+
if "/kubepods" in content:
|
|
145
|
+
# Kubernetes pods -- usually containerd or Docker underneath
|
|
146
|
+
return ContainerRuntime.CONTAINERD
|
|
147
|
+
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def _detect_container_from_mountinfo() -> Optional[ContainerRuntime]:
|
|
152
|
+
"""Inspect ``/proc/1/mountinfo`` for overlay filesystems.
|
|
153
|
+
|
|
154
|
+
Container runtimes typically mount an overlay root filesystem.
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
with open("/proc/1/mountinfo", "r") as fh:
|
|
158
|
+
content = fh.read()
|
|
159
|
+
except (FileNotFoundError, PermissionError, OSError):
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
if "overlay" in content and ("docker" in content or "containerd" in content):
|
|
163
|
+
if "docker" in content:
|
|
164
|
+
return ContainerRuntime.DOCKER
|
|
165
|
+
return ContainerRuntime.CONTAINERD
|
|
166
|
+
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
# VM detection
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _detect_vm() -> bool:
|
|
175
|
+
"""Detect whether the system is running inside a virtual machine.
|
|
176
|
+
|
|
177
|
+
Reads DMI/SMBIOS information from ``/sys/class/dmi/id/`` and
|
|
178
|
+
checks for known hypervisor product names.
|
|
179
|
+
"""
|
|
180
|
+
vm_indicators = {
|
|
181
|
+
"vmware",
|
|
182
|
+
"virtualbox",
|
|
183
|
+
"vbox",
|
|
184
|
+
"kvm",
|
|
185
|
+
"qemu",
|
|
186
|
+
"xen",
|
|
187
|
+
"hyper-v",
|
|
188
|
+
"microsoft corporation",
|
|
189
|
+
"parallels",
|
|
190
|
+
"bochs",
|
|
191
|
+
"bhyve",
|
|
192
|
+
"amazon ec2",
|
|
193
|
+
"google compute engine",
|
|
194
|
+
"openstack",
|
|
195
|
+
"nutanix",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
dmi_files = [
|
|
199
|
+
"/sys/class/dmi/id/product_name",
|
|
200
|
+
"/sys/class/dmi/id/sys_vendor",
|
|
201
|
+
"/sys/class/dmi/id/board_vendor",
|
|
202
|
+
"/sys/class/dmi/id/bios_vendor",
|
|
203
|
+
"/sys/class/dmi/id/chassis_vendor",
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
for dmi_path in dmi_files:
|
|
207
|
+
try:
|
|
208
|
+
with open(dmi_path, "r") as fh:
|
|
209
|
+
value = fh.read().strip().lower()
|
|
210
|
+
for indicator in vm_indicators:
|
|
211
|
+
if indicator in value:
|
|
212
|
+
return True
|
|
213
|
+
except (FileNotFoundError, PermissionError, OSError):
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Fallback: check /proc/cpuinfo for hypervisor flag
|
|
217
|
+
try:
|
|
218
|
+
with open("/proc/cpuinfo", "r") as fh:
|
|
219
|
+
for line in fh:
|
|
220
|
+
if line.strip().lower().startswith("flags"):
|
|
221
|
+
if "hypervisor" in line.lower():
|
|
222
|
+
return True
|
|
223
|
+
break
|
|
224
|
+
except (FileNotFoundError, PermissionError, OSError):
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
return False
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Probe for Python interpreter information."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import sysconfig
|
|
7
|
+
from typing import Any, Dict, Tuple
|
|
8
|
+
|
|
9
|
+
from .base import ProbeBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PythonProbe(ProbeBase):
|
|
13
|
+
"""Collects information about the running Python interpreter.
|
|
14
|
+
|
|
15
|
+
Gathered fields:
|
|
16
|
+
- ``python_version``: 3-tuple of (major, minor, micro)
|
|
17
|
+
- ``python_abi_tag``: e.g. ``"cp312-cp312-linux_x86_64"``
|
|
18
|
+
- ``python_implementation``: e.g. ``"cpython"``, ``"pypy"``
|
|
19
|
+
- ``python_executable``: absolute path to the interpreter
|
|
20
|
+
- ``stable_abi_supported``: whether the stable ABI (abi3) is supported
|
|
21
|
+
- ``python_debug_build``: whether this is a debug build
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
name = "python"
|
|
25
|
+
|
|
26
|
+
def collect(self) -> Dict[str, Any]:
|
|
27
|
+
"""Collect Python interpreter details using only stdlib modules."""
|
|
28
|
+
data: Dict[str, Any] = {}
|
|
29
|
+
|
|
30
|
+
data["python_version"] = self._get_version()
|
|
31
|
+
data["python_abi_tag"] = self._get_abi_tag()
|
|
32
|
+
data["python_implementation"] = self._get_implementation()
|
|
33
|
+
data["python_executable"] = self._get_executable()
|
|
34
|
+
data["stable_abi_supported"] = self._check_stable_abi()
|
|
35
|
+
data["python_debug_build"] = self._check_debug_build()
|
|
36
|
+
|
|
37
|
+
return data
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# Internal helpers
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _get_version() -> Tuple[int, int, int]:
|
|
45
|
+
"""Return the Python version as a (major, minor, micro) tuple."""
|
|
46
|
+
try:
|
|
47
|
+
return (sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
|
|
48
|
+
except Exception:
|
|
49
|
+
return (0, 0, 0)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _get_abi_tag() -> str:
|
|
53
|
+
"""Build the ABI tag from sysconfig variables.
|
|
54
|
+
|
|
55
|
+
Falls back to a constructed tag from ``sys.implementation`` when
|
|
56
|
+
sysconfig does not provide ``SOABI``.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
soabi = sysconfig.get_config_var("SOABI")
|
|
60
|
+
if soabi:
|
|
61
|
+
return str(soabi)
|
|
62
|
+
|
|
63
|
+
# Fallback: build from implementation info
|
|
64
|
+
impl = sys.implementation.name[0:2] # "cp" for cpython
|
|
65
|
+
ver = f"{sys.version_info.major}{sys.version_info.minor}"
|
|
66
|
+
tag = f"{impl}{ver}"
|
|
67
|
+
|
|
68
|
+
# Append debug/unicode suffixes for older Python builds
|
|
69
|
+
abiflags = sysconfig.get_config_var("abiflags") or ""
|
|
70
|
+
if abiflags:
|
|
71
|
+
tag += abiflags
|
|
72
|
+
|
|
73
|
+
return tag
|
|
74
|
+
except Exception:
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _get_implementation() -> str:
|
|
79
|
+
"""Return a normalised implementation name (lowercase)."""
|
|
80
|
+
try:
|
|
81
|
+
return sys.implementation.name.lower()
|
|
82
|
+
except Exception:
|
|
83
|
+
return "cpython"
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _get_executable() -> str:
|
|
87
|
+
"""Return the absolute path to the Python executable."""
|
|
88
|
+
try:
|
|
89
|
+
return sys.executable or ""
|
|
90
|
+
except Exception:
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _check_stable_abi() -> bool:
|
|
95
|
+
"""Determine whether the stable ABI (abi3) is supported.
|
|
96
|
+
|
|
97
|
+
The stable ABI was introduced in CPython 3.2. For other
|
|
98
|
+
implementations it is generally not available.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
if sys.implementation.name.lower() != "cpython":
|
|
102
|
+
return False
|
|
103
|
+
return sys.version_info >= (3, 2)
|
|
104
|
+
except Exception:
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _check_debug_build() -> bool:
|
|
109
|
+
"""Detect whether the interpreter was compiled in debug mode.
|
|
110
|
+
|
|
111
|
+
Checks multiple indicators:
|
|
112
|
+
1. ``sys.flags.debug``
|
|
113
|
+
2. ``Py_DEBUG`` sysconfig variable
|
|
114
|
+
3. ``abiflags`` containing ``"d"``
|
|
115
|
+
4. Pointer size heuristic (debug builds often have larger objects)
|
|
116
|
+
-- this is a weak signal and is used only as a last resort.
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
# Primary: interpreter flag
|
|
120
|
+
if sys.flags.debug:
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
# Secondary: sysconfig variable
|
|
124
|
+
py_debug = sysconfig.get_config_var("Py_DEBUG")
|
|
125
|
+
if py_debug and int(py_debug):
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
# Tertiary: abiflags
|
|
129
|
+
abiflags = sysconfig.get_config_var("abiflags") or ""
|
|
130
|
+
if "d" in abiflags:
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
return False
|
|
134
|
+
except Exception:
|
|
135
|
+
return False
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Build toolchain probe — detects compilers and build tools on the system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import Any, Dict, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from .base import ProbeBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ToolchainProbe(ProbeBase):
|
|
15
|
+
"""Detect available build toolchain (gcc, g++, cmake, make, rustc, etc.).
|
|
16
|
+
|
|
17
|
+
This is critical for detecting packages built from source (sdist)
|
|
18
|
+
and validating that the compiler used is compatible with system binaries.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name = "toolchain"
|
|
22
|
+
|
|
23
|
+
def collect(self) -> Dict[str, Any]:
|
|
24
|
+
data: Dict[str, Any] = {}
|
|
25
|
+
|
|
26
|
+
gcc = self._detect_tool("gcc", r"(\d+\.\d+\.\d+)")
|
|
27
|
+
gpp = self._detect_tool("g++", r"(\d+\.\d+\.\d+)")
|
|
28
|
+
clang = self._detect_tool("clang", r"(\d+\.\d+\.\d+)")
|
|
29
|
+
cmake = self._detect_tool("cmake", r"(\d+\.\d+\.\d+)")
|
|
30
|
+
make = self._detect_tool("make", r"(\d+\.\d+)")
|
|
31
|
+
rustc = self._detect_tool("rustc", r"(\d+\.\d+\.\d+)")
|
|
32
|
+
|
|
33
|
+
toolchain: Dict[str, Optional[str]] = {
|
|
34
|
+
"gcc": gcc,
|
|
35
|
+
"gpp": gpp,
|
|
36
|
+
"clang": clang,
|
|
37
|
+
"cmake": cmake,
|
|
38
|
+
"make": make,
|
|
39
|
+
"rustc": rustc,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
data["toolchain_versions"] = {
|
|
43
|
+
k: v for k, v in toolchain.items() if v is not None
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Detect the default C compiler
|
|
47
|
+
cc = os.environ.get("CC", "")
|
|
48
|
+
cxx = os.environ.get("CXX", "")
|
|
49
|
+
data["default_cc"] = cc if cc else ("gcc" if gcc else "clang" if clang else "")
|
|
50
|
+
data["default_cxx"] = cxx if cxx else ("g++" if gpp else "clang++" if clang else "")
|
|
51
|
+
|
|
52
|
+
# Check if build-essential / dev headers are available
|
|
53
|
+
data["has_build_tools"] = gcc is not None or clang is not None
|
|
54
|
+
data["has_python_dev_headers"] = self._has_python_headers()
|
|
55
|
+
|
|
56
|
+
return data
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _detect_tool(tool: str, version_pattern: str) -> Optional[str]:
|
|
60
|
+
"""Run ``tool --version`` and extract version string."""
|
|
61
|
+
path = shutil.which(tool)
|
|
62
|
+
if not path:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
result = subprocess.run(
|
|
66
|
+
[path, "--version"],
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
timeout=5,
|
|
70
|
+
)
|
|
71
|
+
output = result.stdout + result.stderr
|
|
72
|
+
match = re.search(version_pattern, output)
|
|
73
|
+
if match:
|
|
74
|
+
return match.group(1)
|
|
75
|
+
return "unknown"
|
|
76
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _has_python_headers() -> bool:
|
|
81
|
+
"""Check if Python development headers are installed."""
|
|
82
|
+
try:
|
|
83
|
+
import sysconfig
|
|
84
|
+
include_dir = sysconfig.get_path("include")
|
|
85
|
+
if include_dir and os.path.isfile(os.path.join(include_dir, "Python.h")):
|
|
86
|
+
return True
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
return False
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Virtual environment probe — detects venv type and configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from .base import ProbeBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VenvProbe(ProbeBase):
|
|
14
|
+
"""Detect virtual environment type and configuration.
|
|
15
|
+
|
|
16
|
+
Identifies whether running in venv, conda, virtualenv, poetry, pdm,
|
|
17
|
+
pipenv, or system Python. Also detects common misconfigurations.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name = "venv"
|
|
21
|
+
|
|
22
|
+
def collect(self) -> Dict[str, Any]:
|
|
23
|
+
data: Dict[str, Any] = {}
|
|
24
|
+
|
|
25
|
+
venv_type = self._detect_venv_type()
|
|
26
|
+
data["venv_type"] = venv_type
|
|
27
|
+
data["is_system_python"] = venv_type == "system"
|
|
28
|
+
data["is_virtual_env"] = venv_type != "system"
|
|
29
|
+
|
|
30
|
+
# Base prefix vs prefix (tells us if we're in a venv)
|
|
31
|
+
data["base_prefix"] = getattr(sys, "base_prefix", sys.prefix)
|
|
32
|
+
data["prefix"] = sys.prefix
|
|
33
|
+
|
|
34
|
+
# Detect conda-specific info
|
|
35
|
+
if venv_type == "conda":
|
|
36
|
+
data["conda_env_name"] = os.environ.get("CONDA_DEFAULT_ENV", "")
|
|
37
|
+
data["conda_prefix"] = os.environ.get("CONDA_PREFIX", "")
|
|
38
|
+
|
|
39
|
+
# Check for common misconfigurations
|
|
40
|
+
data["pip_user_site_enabled"] = self._pip_user_site_active()
|
|
41
|
+
data["mixed_env_risk"] = self._detect_mixed_env()
|
|
42
|
+
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _detect_venv_type() -> str:
|
|
47
|
+
"""Identify the virtual environment manager in use."""
|
|
48
|
+
# Conda
|
|
49
|
+
if os.environ.get("CONDA_DEFAULT_ENV") or os.environ.get("CONDA_PREFIX"):
|
|
50
|
+
return "conda"
|
|
51
|
+
|
|
52
|
+
# Poetry
|
|
53
|
+
if os.environ.get("POETRY_ACTIVE") == "1":
|
|
54
|
+
return "poetry"
|
|
55
|
+
|
|
56
|
+
# Pipenv
|
|
57
|
+
if os.environ.get("PIPENV_ACTIVE") == "1":
|
|
58
|
+
return "pipenv"
|
|
59
|
+
|
|
60
|
+
# PDM
|
|
61
|
+
if os.environ.get("PDM_IN_VENV"):
|
|
62
|
+
return "pdm"
|
|
63
|
+
|
|
64
|
+
# Standard venv / virtualenv
|
|
65
|
+
real_prefix = getattr(sys, "real_prefix", None) # virtualenv sets this
|
|
66
|
+
base_prefix = getattr(sys, "base_prefix", sys.prefix)
|
|
67
|
+
|
|
68
|
+
if real_prefix is not None:
|
|
69
|
+
return "virtualenv"
|
|
70
|
+
|
|
71
|
+
if base_prefix != sys.prefix:
|
|
72
|
+
# Check for pyvenv.cfg to distinguish venv from virtualenv
|
|
73
|
+
cfg = os.path.join(sys.prefix, "pyvenv.cfg")
|
|
74
|
+
if os.path.isfile(cfg):
|
|
75
|
+
return "venv"
|
|
76
|
+
return "virtualenv"
|
|
77
|
+
|
|
78
|
+
return "system"
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _pip_user_site_active() -> bool:
|
|
82
|
+
"""Check if pip user-site installs are leaking into the env."""
|
|
83
|
+
try:
|
|
84
|
+
import site
|
|
85
|
+
user_site = getattr(site, "getusersitepackages", lambda: None)()
|
|
86
|
+
if user_site and os.path.isdir(user_site):
|
|
87
|
+
# Check if user site is in sys.path
|
|
88
|
+
return user_site in sys.path
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _detect_mixed_env() -> bool:
|
|
95
|
+
"""Detect if packages from multiple environments are on sys.path.
|
|
96
|
+
|
|
97
|
+
This happens when system packages leak into a venv or vice versa.
|
|
98
|
+
"""
|
|
99
|
+
site_packages_dirs = [
|
|
100
|
+
p for p in sys.path if "site-packages" in p or "dist-packages" in p
|
|
101
|
+
]
|
|
102
|
+
# If there are multiple distinct site-packages roots, it's mixed
|
|
103
|
+
prefixes = set()
|
|
104
|
+
for sp in site_packages_dirs:
|
|
105
|
+
# Get the env root (two levels up from site-packages)
|
|
106
|
+
parts = sp.split(os.sep)
|
|
107
|
+
for i, part in enumerate(parts):
|
|
108
|
+
if part in ("site-packages", "dist-packages") and i >= 2:
|
|
109
|
+
prefixes.add(os.sep.join(parts[:i - 1]))
|
|
110
|
+
break
|
|
111
|
+
return len(prefixes) > 1
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Board profile system for embedded device intelligence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .engine import BoardProfile, ProfileEngine, load_profile, match_board_profile
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"BoardProfile",
|
|
9
|
+
"ProfileEngine",
|
|
10
|
+
"load_profile",
|
|
11
|
+
"match_board_profile",
|
|
12
|
+
]
|