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,190 @@
1
+ """Plugin discovery and loading via ``importlib.metadata`` entry points.
2
+
3
+ Plugins declare themselves through the ``pybinaryguard.plugins`` entry-point
4
+ group in their ``pyproject.toml`` (or ``setup.cfg`` / ``setup.py``). Each
5
+ entry point must resolve to a module that exposes a ``register(registry)``
6
+ function.
7
+
8
+ Example ``pyproject.toml`` snippet::
9
+
10
+ [project.entry-points."pybinaryguard.plugins"]
11
+ my_plugin = "my_package.pybinaryguard_plugin"
12
+
13
+ The module ``my_package.pybinaryguard_plugin`` must define::
14
+
15
+ def register(registry: HookRegistry) -> None:
16
+ ...
17
+
18
+ This module also discovers the built-in contrib plugins that ship with
19
+ PyBinaryGuard itself (Jetson, OpenCV, TensorRT, GStreamer).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import importlib
25
+ import logging
26
+ import sys
27
+ from typing import List
28
+
29
+ from pybinaryguard.plugins.hooks import HookRegistry
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ ENTRY_POINT_GROUP = "pybinaryguard.plugins"
34
+
35
+ # Built-in contrib plugin module paths, loaded after external entry points.
36
+ _BUILTIN_CONTRIB_MODULES: List[str] = [
37
+ "pybinaryguard.plugins.contrib.jetson",
38
+ "pybinaryguard.plugins.contrib.opencv",
39
+ "pybinaryguard.plugins.contrib.tensorrt",
40
+ "pybinaryguard.plugins.contrib.gstreamer",
41
+ ]
42
+
43
+
44
+ def _load_entry_point_plugins(registry: HookRegistry) -> None:
45
+ """Discover and load external plugins via ``importlib.metadata`` entry points.
46
+
47
+ Handles both the Python 3.9 API (``entry_points()`` returns a dict)
48
+ and the Python 3.10+ API (``entry_points(group=...)``).
49
+ """
50
+ try:
51
+ from importlib.metadata import entry_points
52
+ except ImportError:
53
+ # Fallback for very old environments that lack importlib.metadata
54
+ # (should not happen on 3.9+ but guard defensively).
55
+ logger.debug("importlib.metadata not available; skipping entry-point discovery")
56
+ return
57
+
58
+ eps: list = []
59
+ try:
60
+ # Python 3.12+ and 3.10+ style: keyword argument filtering
61
+ selected = entry_points(group=ENTRY_POINT_GROUP)
62
+ # On 3.10-3.11 this returns a SelectableGroups or list
63
+ if isinstance(selected, dict):
64
+ # 3.9 fallback: entry_points() returns a dict of lists
65
+ eps = list(selected.get(ENTRY_POINT_GROUP, []))
66
+ else:
67
+ eps = list(selected)
68
+ except TypeError:
69
+ # Python 3.9: entry_points() does not accept keyword arguments.
70
+ all_eps = entry_points()
71
+ if isinstance(all_eps, dict):
72
+ eps = list(all_eps.get(ENTRY_POINT_GROUP, []))
73
+ else:
74
+ # Should not reach here, but handle gracefully
75
+ eps = []
76
+
77
+ for ep in eps:
78
+ try:
79
+ plugin_module = ep.load()
80
+ register_fn = getattr(plugin_module, "register", None)
81
+ if register_fn is None:
82
+ logger.warning(
83
+ "Plugin entry point %r resolved to %r but it has no "
84
+ "'register' function; skipping",
85
+ ep.name,
86
+ plugin_module,
87
+ )
88
+ continue
89
+ if not callable(register_fn):
90
+ logger.warning(
91
+ "Plugin %r has a 'register' attribute but it is not "
92
+ "callable; skipping",
93
+ ep.name,
94
+ )
95
+ continue
96
+ register_fn(registry)
97
+ logger.info("Loaded external plugin: %s", ep.name)
98
+ except Exception:
99
+ logger.warning(
100
+ "Failed to load plugin entry point %r; skipping",
101
+ ep.name,
102
+ exc_info=True,
103
+ )
104
+
105
+
106
+ def _load_builtin_contrib_plugins(registry: HookRegistry) -> None:
107
+ """Load the contrib plugins that ship with PyBinaryGuard.
108
+
109
+ Each contrib module is imported and its ``register(registry)`` function
110
+ is called. Errors in individual contrib plugins are logged and do not
111
+ prevent the rest from loading.
112
+ """
113
+ for module_path in _BUILTIN_CONTRIB_MODULES:
114
+ try:
115
+ mod = importlib.import_module(module_path)
116
+ except ImportError:
117
+ logger.debug(
118
+ "Could not import contrib plugin %s; skipping",
119
+ module_path,
120
+ )
121
+ continue
122
+
123
+ register_fn = getattr(mod, "register", None)
124
+ if register_fn is None:
125
+ logger.debug(
126
+ "Contrib module %s has no 'register' function; skipping",
127
+ module_path,
128
+ )
129
+ continue
130
+
131
+ if not callable(register_fn):
132
+ logger.debug(
133
+ "Contrib module %s 'register' attribute is not callable; skipping",
134
+ module_path,
135
+ )
136
+ continue
137
+
138
+ try:
139
+ register_fn(registry)
140
+ logger.debug("Loaded contrib plugin: %s", module_path)
141
+ except Exception:
142
+ logger.warning(
143
+ "Contrib plugin %s raised an error during registration; skipping",
144
+ module_path,
145
+ exc_info=True,
146
+ )
147
+
148
+
149
+ def discover_plugins(
150
+ *,
151
+ load_contrib: bool = True,
152
+ load_external: bool = True,
153
+ ) -> HookRegistry:
154
+ """Discover and load all available plugins.
155
+
156
+ This is the main entry point for the plugin subsystem. It creates a
157
+ fresh :class:`HookRegistry`, populates it from external entry points
158
+ and built-in contrib plugins, and returns it.
159
+
160
+ Args:
161
+ load_contrib: Whether to load the built-in contrib plugins that
162
+ ship with PyBinaryGuard (Jetson, OpenCV, TensorRT, GStreamer).
163
+ Defaults to ``True``.
164
+ load_external: Whether to discover and load externally-installed
165
+ plugins via ``importlib.metadata`` entry points. Defaults
166
+ to ``True``.
167
+
168
+ Returns:
169
+ A :class:`HookRegistry` populated with all successfully loaded
170
+ plugin extensions.
171
+ """
172
+ registry = HookRegistry()
173
+
174
+ if load_external:
175
+ _load_entry_point_plugins(registry)
176
+
177
+ if load_contrib:
178
+ _load_builtin_contrib_plugins(registry)
179
+
180
+ total = registry.summary()
181
+ registered = sum(total.values())
182
+ if registered:
183
+ logger.info(
184
+ "Plugin discovery complete: %s",
185
+ ", ".join(f"{k}={v}" for k, v in total.items() if v),
186
+ )
187
+ else:
188
+ logger.debug("Plugin discovery complete: no extensions registered")
189
+
190
+ return registry
@@ -0,0 +1,23 @@
1
+ """Predictive failure engine for runtime simulation.
2
+
3
+ This module predicts ImportErrors and runtime failures before they occur
4
+ by simulating dynamic linker resolution and symbol dependencies.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .resolver import SymbolResolver, resolve_symbol
10
+ from .dependency_graph import DependencyGraph, build_dependency_graph
11
+ from .linker_simulator import LinkerSimulator, simulate_import
12
+ from .predictor import predict_import_failures, PredictedFailure
13
+
14
+ __all__ = [
15
+ "SymbolResolver",
16
+ "resolve_symbol",
17
+ "DependencyGraph",
18
+ "build_dependency_graph",
19
+ "LinkerSimulator",
20
+ "simulate_import",
21
+ "predict_import_failures",
22
+ "PredictedFailure",
23
+ ]
@@ -0,0 +1,226 @@
1
+ """Dependency graph builder for shared library dependencies.
2
+
3
+ Builds a graph of DT_NEEDED dependencies to visualize and analyze
4
+ library dependency chains.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional, Set
12
+
13
+ from pybinaryguard.analyzers.elf_analyzer import MinimalELFParser
14
+
15
+
16
+ @dataclass
17
+ class DependencyNode:
18
+ """A node in the dependency graph representing a library."""
19
+
20
+ name: str # Library name (e.g., "libc.so.6")
21
+ path: Optional[str] = None # Full path if found
22
+ dependencies: List[str] = field(default_factory=list) # DT_NEEDED entries
23
+ unresolved: bool = False # Whether this library was found
24
+ circular: bool = False # Whether this is part of a circular dependency
25
+
26
+
27
+ class DependencyGraph:
28
+ """Dependency graph for analyzing shared library dependencies."""
29
+
30
+ def __init__(self, library_paths: Optional[List[str]] = None):
31
+ """Initialize the dependency graph builder.
32
+
33
+ Args:
34
+ library_paths: Directories to search for libraries
35
+ """
36
+ if library_paths is None:
37
+ library_paths = [
38
+ "/lib",
39
+ "/lib64",
40
+ "/usr/lib",
41
+ "/usr/lib64",
42
+ ]
43
+
44
+ self.library_paths = [Path(p) for p in library_paths if Path(p).exists()]
45
+ self.nodes: Dict[str, DependencyNode] = {}
46
+ self._visited: Set[str] = set()
47
+
48
+ def build_from_binary(self, binary_path: str) -> DependencyNode:
49
+ """Build dependency graph starting from a binary.
50
+
51
+ Args:
52
+ binary_path: Path to the ELF binary
53
+
54
+ Returns:
55
+ Root DependencyNode for the binary
56
+ """
57
+ self.nodes.clear()
58
+ self._visited.clear()
59
+
60
+ root_name = Path(binary_path).name
61
+ root_node = DependencyNode(name=root_name, path=binary_path)
62
+ self.nodes[root_name] = root_node
63
+
64
+ # Build the graph recursively
65
+ self._build_recursive(binary_path, root_node)
66
+
67
+ return root_node
68
+
69
+ def _build_recursive(self, lib_path: str, node: DependencyNode) -> None:
70
+ """Recursively build dependency graph.
71
+
72
+ Args:
73
+ lib_path: Path to current library
74
+ node: DependencyNode for current library
75
+ """
76
+ # Avoid infinite recursion
77
+ if lib_path in self._visited:
78
+ node.circular = True
79
+ return
80
+
81
+ self._visited.add(lib_path)
82
+
83
+ try:
84
+ # Parse ELF to get DT_NEEDED
85
+ parser = MinimalELFParser(lib_path)
86
+ needed = parser.get_needed()
87
+
88
+ node.dependencies = needed
89
+
90
+ # Process each dependency
91
+ for dep_name in needed:
92
+ # Check if we already have this node
93
+ if dep_name in self.nodes:
94
+ # Mark as circular if we're revisiting
95
+ if dep_name in self._visited:
96
+ self.nodes[dep_name].circular = True
97
+ continue
98
+
99
+ # Create new node
100
+ dep_node = DependencyNode(name=dep_name)
101
+
102
+ # Try to find the library
103
+ dep_path = self._find_library(dep_name)
104
+ if dep_path:
105
+ dep_node.path = dep_path
106
+ self.nodes[dep_name] = dep_node
107
+ # Recurse
108
+ self._build_recursive(dep_path, dep_node)
109
+ else:
110
+ # Unresolved dependency
111
+ dep_node.unresolved = True
112
+ self.nodes[dep_name] = dep_node
113
+
114
+ except Exception:
115
+ # If we can't parse, mark dependencies as unknown
116
+ node.unresolved = True
117
+
118
+ def _find_library(self, lib_name: str) -> Optional[str]:
119
+ """Find a library in the search paths.
120
+
121
+ Args:
122
+ lib_name: Library name
123
+
124
+ Returns:
125
+ Full path or None
126
+ """
127
+ for search_path in self.library_paths:
128
+ lib_path = search_path / lib_name
129
+ if lib_path.exists():
130
+ return str(lib_path)
131
+
132
+ return None
133
+
134
+ def get_unresolved_dependencies(self) -> List[str]:
135
+ """Get list of unresolved dependencies.
136
+
137
+ Returns:
138
+ List of library names that couldn't be found
139
+ """
140
+ return [
141
+ node.name
142
+ for node in self.nodes.values()
143
+ if node.unresolved
144
+ ]
145
+
146
+ def get_dependency_chain(self, target_lib: str) -> Optional[List[str]]:
147
+ """Get the dependency chain to a specific library.
148
+
149
+ Args:
150
+ target_lib: Target library name
151
+
152
+ Returns:
153
+ List of library names in the chain, or None if not found
154
+ """
155
+ # BFS to find shortest path
156
+ from collections import deque
157
+
158
+ if not self.nodes:
159
+ return None
160
+
161
+ # Start from root (first node)
162
+ root_name = next(iter(self.nodes.keys()))
163
+
164
+ queue = deque([(root_name, [root_name])])
165
+ visited = {root_name}
166
+
167
+ while queue:
168
+ current, path = queue.popleft()
169
+
170
+ if current == target_lib:
171
+ return path
172
+
173
+ node = self.nodes.get(current)
174
+ if node:
175
+ for dep in node.dependencies:
176
+ if dep not in visited:
177
+ visited.add(dep)
178
+ queue.append((dep, path + [dep]))
179
+
180
+ return None
181
+
182
+ def has_circular_dependencies(self) -> bool:
183
+ """Check if the graph has circular dependencies.
184
+
185
+ Returns:
186
+ True if circular dependencies exist
187
+ """
188
+ return any(node.circular for node in self.nodes.values())
189
+
190
+ def to_dict(self) -> Dict[str, object]:
191
+ """Convert graph to dictionary representation.
192
+
193
+ Returns:
194
+ Dictionary with graph structure
195
+ """
196
+ return {
197
+ "nodes": {
198
+ name: {
199
+ "path": node.path,
200
+ "dependencies": node.dependencies,
201
+ "unresolved": node.unresolved,
202
+ "circular": node.circular,
203
+ }
204
+ for name, node in self.nodes.items()
205
+ },
206
+ "unresolved_count": len(self.get_unresolved_dependencies()),
207
+ "has_circular": self.has_circular_dependencies(),
208
+ }
209
+
210
+
211
+ def build_dependency_graph(
212
+ binary_path: str,
213
+ library_paths: Optional[List[str]] = None
214
+ ) -> DependencyGraph:
215
+ """Convenience function to build a dependency graph.
216
+
217
+ Args:
218
+ binary_path: Path to the ELF binary
219
+ library_paths: Optional library search paths
220
+
221
+ Returns:
222
+ DependencyGraph instance
223
+ """
224
+ graph = DependencyGraph(library_paths)
225
+ graph.build_from_binary(binary_path)
226
+ return graph
@@ -0,0 +1,161 @@
1
+ """Dynamic linker simulation for predicting runtime failures.
2
+
3
+ Simulates the behavior of ld.so to predict symbol resolution failures
4
+ before they occur at runtime.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Dict, List, Optional
11
+
12
+ from pybinaryguard.predictor.dependency_graph import DependencyGraph
13
+ from pybinaryguard.predictor.resolver import SymbolResolver
14
+
15
+
16
+ @dataclass
17
+ class LinkageResult:
18
+ """Result of simulated dynamic linking."""
19
+
20
+ success: bool
21
+ missing_symbols: List[str]
22
+ missing_libraries: List[str]
23
+ symbol_sources: Dict[str, Optional[str]] # symbol -> providing library
24
+ error_message: Optional[str] = None
25
+
26
+
27
+ class LinkerSimulator:
28
+ """Simulates dynamic linker behavior to predict failures."""
29
+
30
+ def __init__(self, library_paths: Optional[List[str]] = None):
31
+ """Initialize the linker simulator.
32
+
33
+ Args:
34
+ library_paths: Directories to search for libraries
35
+ """
36
+ self.resolver = SymbolResolver(library_paths)
37
+ self.library_paths = library_paths
38
+
39
+ def simulate_load(self, binary_path: str) -> LinkageResult:
40
+ """Simulate loading a binary and resolving all symbols.
41
+
42
+ Args:
43
+ binary_path: Path to the ELF binary
44
+
45
+ Returns:
46
+ LinkageResult with success status and details
47
+ """
48
+ # Build dependency graph
49
+ dep_graph = DependencyGraph(self.library_paths)
50
+ root_node = dep_graph.build_from_binary(binary_path)
51
+
52
+ # Check for missing libraries
53
+ missing_libs = dep_graph.get_unresolved_dependencies()
54
+
55
+ if missing_libs:
56
+ return LinkageResult(
57
+ success=False,
58
+ missing_symbols=[],
59
+ missing_libraries=missing_libs,
60
+ symbol_sources={},
61
+ error_message=f"Missing libraries: {', '.join(missing_libs)}"
62
+ )
63
+
64
+ # Try to resolve symbols
65
+ needed_libs = root_node.dependencies
66
+ symbol_resolution = self.resolver.resolve_undefined_symbols(
67
+ binary_path,
68
+ needed_libs
69
+ )
70
+
71
+ # Find unresolved symbols
72
+ missing_symbols = [
73
+ sym for sym, provider in symbol_resolution.items()
74
+ if provider is None
75
+ ]
76
+
77
+ if missing_symbols:
78
+ # Format error message similar to actual ImportError
79
+ first_missing = missing_symbols[0]
80
+ error_msg = (
81
+ f"ImportError: {binary_path}: "
82
+ f"undefined symbol: {first_missing}"
83
+ )
84
+
85
+ return LinkageResult(
86
+ success=False,
87
+ missing_symbols=missing_symbols,
88
+ missing_libraries=[],
89
+ symbol_sources=symbol_resolution,
90
+ error_message=error_msg
91
+ )
92
+
93
+ # Check for circular dependencies
94
+ if dep_graph.has_circular_dependencies():
95
+ return LinkageResult(
96
+ success=True, # Still successful, but warn
97
+ missing_symbols=[],
98
+ missing_libraries=[],
99
+ symbol_sources=symbol_resolution,
100
+ error_message="Warning: Circular dependencies detected"
101
+ )
102
+
103
+ return LinkageResult(
104
+ success=True,
105
+ missing_symbols=[],
106
+ missing_libraries=[],
107
+ symbol_sources=symbol_resolution,
108
+ )
109
+
110
+ def predict_import_error(
111
+ self,
112
+ module_path: str,
113
+ extension_name: str
114
+ ) -> Optional[str]:
115
+ """Predict ImportError for a Python extension module.
116
+
117
+ Args:
118
+ module_path: Path to the extension module (.so file)
119
+ extension_name: Name of the extension (for error message)
120
+
121
+ Returns:
122
+ Predicted error message, or None if import should succeed
123
+ """
124
+ result = self.simulate_load(module_path)
125
+
126
+ if not result.success:
127
+ if result.missing_libraries:
128
+ # Format like actual ImportError
129
+ missing = result.missing_libraries[0]
130
+ return (
131
+ f"ImportError: {extension_name}: "
132
+ f"cannot open shared object file: {missing}: "
133
+ f"No such file or directory"
134
+ )
135
+
136
+ if result.missing_symbols:
137
+ # Format like actual ImportError
138
+ missing = result.missing_symbols[0]
139
+ return (
140
+ f"ImportError: {module_path}: "
141
+ f"undefined symbol: {missing}"
142
+ )
143
+
144
+ return None
145
+
146
+
147
+ def simulate_import(
148
+ binary_path: str,
149
+ library_paths: Optional[List[str]] = None
150
+ ) -> LinkageResult:
151
+ """Convenience function to simulate importing a binary.
152
+
153
+ Args:
154
+ binary_path: Path to the ELF binary
155
+ library_paths: Optional library search paths
156
+
157
+ Returns:
158
+ LinkageResult with simulation results
159
+ """
160
+ simulator = LinkerSimulator(library_paths)
161
+ return simulator.simulate_load(binary_path)