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