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,19 @@
1
+ """Advanced Health Scoring v2 — multi-dimensional weighted environment scoring.
2
+
3
+ Provides category-based scoring across four dimensions:
4
+ - Binary Stability (GLIBC, architecture, ELF integrity)
5
+ - GPU Compatibility (CUDA stack, cuDNN, compute capability)
6
+ - Dependency Health (missing libraries, symbol resolution)
7
+ - Platform Risk (embedded thermal, container issues, board compat)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .engine import HealthScoreV2, ScoreBreakdown, CategoryScore, compute_health_score
13
+
14
+ __all__ = [
15
+ "HealthScoreV2",
16
+ "ScoreBreakdown",
17
+ "CategoryScore",
18
+ "compute_health_score",
19
+ ]
@@ -0,0 +1,416 @@
1
+ """Advanced Health Scoring v2 engine.
2
+
3
+ Multi-dimensional weighted scoring that evaluates environment health
4
+ across four categories rather than a simple linear penalty.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Dict, List, Optional, Set, Tuple
11
+
12
+ from pybinaryguard.models.enums import Severity
13
+ from pybinaryguard.models.finding import Finding
14
+
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Category definitions
18
+ # ---------------------------------------------------------------------------
19
+
20
+ # Map rule_id prefixes/patterns to scoring categories
21
+ _CATEGORY_MAP: Dict[str, str] = {
22
+ # Binary Stability
23
+ "GLIBC_": "binary_stability",
24
+ "MUSL_": "binary_stability",
25
+ "MANYLINUX_": "binary_stability",
26
+ "ARCH_": "binary_stability",
27
+ "PYTHON_ABI_": "binary_stability",
28
+ "PYTHON_VERSION_": "binary_stability",
29
+ "ABI3_": "binary_stability",
30
+ "DEBUG_RELEASE_": "binary_stability",
31
+ "ELF_": "binary_stability",
32
+ "LIBSTDCXX_": "binary_stability",
33
+ # GPU Compatibility
34
+ "CUDA_": "gpu_compat",
35
+ "CUDNN_": "gpu_compat",
36
+ "COMPUTE_CAPABILITY_": "gpu_compat",
37
+ "PYTORCH_CUDA_": "gpu_compat",
38
+ "TENSORFLOW_COMPUTE_": "gpu_compat",
39
+ "TENSORFLOW_CUDA_": "gpu_compat",
40
+ "TENSORRT_": "gpu_compat",
41
+ "ONNX_RUNTIME_": "gpu_compat",
42
+ "PYTORCH_TORCHVISION_": "gpu_compat",
43
+ # Dependency Health
44
+ "MISSING_SHARED_LIB": "dependency_health",
45
+ "NUMPY_ABI_": "dependency_health",
46
+ "PREDICTED_IMPORT_": "dependency_health",
47
+ "UNRESOLVED_DEPENDENCY_": "dependency_health",
48
+ "PACKAGE_NOT_FOUND": "dependency_health",
49
+ # Platform Risk
50
+ "CONTAINER_": "platform_risk",
51
+ "JETPACK_": "platform_risk",
52
+ "JETSON_": "platform_risk",
53
+ "OPENCV_": "platform_risk",
54
+ "BOARD_": "platform_risk",
55
+ "KNOWN_BROKEN_": "platform_risk",
56
+ "AVX": "binary_stability",
57
+ "ILLEGAL_INSTRUCTION_": "binary_stability",
58
+ "CPU_": "binary_stability",
59
+ # Source build
60
+ "SOURCE_BUILD_": "binary_stability",
61
+ # Dependency health
62
+ "DEPENDENCY_": "dependency_health",
63
+ # Virtual environment
64
+ "VENV_": "platform_risk",
65
+ }
66
+
67
+ # Category display metadata
68
+ _CATEGORY_INFO: Dict[str, Tuple[str, str]] = {
69
+ "binary_stability": ("Binary Stability", "GLIBC, architecture, ABI, and ELF integrity"),
70
+ "gpu_compat": ("GPU Compatibility", "CUDA stack, cuDNN, compute capability, framework builds"),
71
+ "dependency_health": ("Dependency Health", "Shared library resolution and symbol availability"),
72
+ "platform_risk": ("Platform Risk", "Container, embedded board, and platform-specific issues"),
73
+ }
74
+
75
+ # Default weights — sum to 1.0
76
+ _DEFAULT_WEIGHTS: Dict[str, float] = {
77
+ "binary_stability": 0.35,
78
+ "gpu_compat": 0.30,
79
+ "dependency_health": 0.25,
80
+ "platform_risk": 0.10,
81
+ }
82
+
83
+ # Severity penalties (applied within each category, out of 100)
84
+ _SEVERITY_PENALTY: Dict[Severity, float] = {
85
+ Severity.CRITICAL: 30.0,
86
+ Severity.WARNING: 10.0,
87
+ Severity.INFO: 2.0,
88
+ Severity.PASSED: 0.0,
89
+ }
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Data classes
94
+ # ---------------------------------------------------------------------------
95
+
96
+ @dataclass
97
+ class CategoryScore:
98
+ """Score for a single health category."""
99
+
100
+ name: str
101
+ display_name: str
102
+ description: str
103
+ score: float # 0-100
104
+ weight: float
105
+ weighted_score: float # score * weight
106
+ finding_count: int = 0
107
+ critical_count: int = 0
108
+ warning_count: int = 0
109
+ top_issues: List[str] = field(default_factory=list)
110
+
111
+ @property
112
+ def label(self) -> str:
113
+ if self.score >= 90:
114
+ return "Excellent"
115
+ elif self.score >= 70:
116
+ return "Good"
117
+ elif self.score >= 50:
118
+ return "Fair"
119
+ elif self.score >= 30:
120
+ return "Poor"
121
+ else:
122
+ return "Critical"
123
+
124
+ def as_dict(self) -> Dict[str, object]:
125
+ return {
126
+ "name": self.name,
127
+ "display_name": self.display_name,
128
+ "score": round(self.score, 1),
129
+ "weight": self.weight,
130
+ "weighted_score": round(self.weighted_score, 1),
131
+ "label": self.label,
132
+ "finding_count": self.finding_count,
133
+ "critical_count": self.critical_count,
134
+ "warning_count": self.warning_count,
135
+ "top_issues": self.top_issues,
136
+ }
137
+
138
+
139
+ @dataclass
140
+ class ScoreBreakdown:
141
+ """Complete health score breakdown across all categories."""
142
+
143
+ overall_score: float # 0-100
144
+ overall_label: str
145
+ categories: Dict[str, CategoryScore] = field(default_factory=dict)
146
+ total_findings: int = 0
147
+ total_critical: int = 0
148
+ total_warnings: int = 0
149
+
150
+ @property
151
+ def weakest_category(self) -> Optional[CategoryScore]:
152
+ if not self.categories:
153
+ return None
154
+ return min(self.categories.values(), key=lambda c: c.score)
155
+
156
+ @property
157
+ def strongest_category(self) -> Optional[CategoryScore]:
158
+ if not self.categories:
159
+ return None
160
+ return max(self.categories.values(), key=lambda c: c.score)
161
+
162
+ def as_dict(self) -> Dict[str, object]:
163
+ result: Dict[str, object] = {
164
+ "overall_score": round(self.overall_score, 1),
165
+ "overall_label": self.overall_label,
166
+ "total_findings": self.total_findings,
167
+ "total_critical": self.total_critical,
168
+ "total_warnings": self.total_warnings,
169
+ "categories": {
170
+ name: cat.as_dict() for name, cat in self.categories.items()
171
+ },
172
+ }
173
+ weakest = self.weakest_category
174
+ if weakest:
175
+ result["weakest_category"] = weakest.name
176
+ return result
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Scoring engine
181
+ # ---------------------------------------------------------------------------
182
+
183
+ class HealthScoreV2:
184
+ """Multi-dimensional health scoring engine.
185
+
186
+ Evaluates findings across four categories with configurable weights,
187
+ producing a breakdown that shows exactly where environment health
188
+ is strong and where it needs attention.
189
+ """
190
+
191
+ def __init__(
192
+ self,
193
+ weights: Optional[Dict[str, float]] = None,
194
+ has_gpu: bool = False,
195
+ is_embedded: bool = False,
196
+ ) -> None:
197
+ self._weights = dict(weights or _DEFAULT_WEIGHTS)
198
+ self._has_gpu = has_gpu
199
+ self._is_embedded = is_embedded
200
+
201
+ # Adjust weights based on environment
202
+ self._adjust_weights()
203
+
204
+ def _adjust_weights(self) -> None:
205
+ """Redistribute weights based on environment context.
206
+
207
+ If no GPU is present, GPU weight is redistributed.
208
+ If not on embedded hardware, platform risk weight is redistributed.
209
+ """
210
+ redistribute = 0.0
211
+
212
+ if not self._has_gpu and self._weights.get("gpu_compat", 0) > 0:
213
+ redistribute += self._weights["gpu_compat"]
214
+ self._weights["gpu_compat"] = 0.0
215
+
216
+ if not self._is_embedded and self._weights.get("platform_risk", 0) > 0:
217
+ extra = self._weights["platform_risk"] * 0.5
218
+ redistribute += extra
219
+ self._weights["platform_risk"] -= extra
220
+
221
+ if redistribute > 0:
222
+ # Distribute to non-zero categories
223
+ active = [k for k, v in self._weights.items() if v > 0]
224
+ if active:
225
+ per_category = redistribute / len(active)
226
+ for key in active:
227
+ self._weights[key] += per_category
228
+
229
+ # Normalize to sum to 1.0
230
+ total = sum(self._weights.values())
231
+ if total > 0:
232
+ for key in self._weights:
233
+ self._weights[key] /= total
234
+
235
+ def score(self, findings: List[Finding]) -> ScoreBreakdown:
236
+ """Compute health score breakdown from findings.
237
+
238
+ Parameters
239
+ ----------
240
+ findings:
241
+ All findings from the scan.
242
+
243
+ Returns
244
+ -------
245
+ ScoreBreakdown
246
+ Detailed breakdown with per-category scores and overall score.
247
+ """
248
+ # Categorize findings
249
+ categorized: Dict[str, List[Finding]] = {
250
+ "binary_stability": [],
251
+ "gpu_compat": [],
252
+ "dependency_health": [],
253
+ "platform_risk": [],
254
+ }
255
+
256
+ uncategorized: List[Finding] = []
257
+
258
+ for finding in findings:
259
+ category = self._classify_finding(finding)
260
+ if category and category in categorized:
261
+ categorized[category].append(finding)
262
+ else:
263
+ uncategorized.append(finding)
264
+
265
+ # Assign uncategorized to binary_stability (safest default)
266
+ categorized["binary_stability"].extend(uncategorized)
267
+
268
+ # Score each category
269
+ categories: Dict[str, CategoryScore] = {}
270
+ for cat_name, cat_findings in categorized.items():
271
+ display_name, description = _CATEGORY_INFO.get(
272
+ cat_name, (cat_name, "")
273
+ )
274
+ weight = self._weights.get(cat_name, 0.0)
275
+ cat_score = self._score_category(
276
+ cat_name, display_name, description, cat_findings, weight
277
+ )
278
+ categories[cat_name] = cat_score
279
+
280
+ # Compute overall weighted score
281
+ overall = sum(cat.weighted_score for cat in categories.values())
282
+ overall = max(0.0, min(100.0, overall))
283
+
284
+ # Determine overall label
285
+ overall_label = self._score_label(overall)
286
+
287
+ total_findings = sum(
288
+ 1 for f in findings if f.severity != Severity.PASSED
289
+ )
290
+ total_critical = sum(
291
+ 1 for f in findings if f.severity == Severity.CRITICAL
292
+ )
293
+ total_warnings = sum(
294
+ 1 for f in findings if f.severity == Severity.WARNING
295
+ )
296
+
297
+ return ScoreBreakdown(
298
+ overall_score=overall,
299
+ overall_label=overall_label,
300
+ categories=categories,
301
+ total_findings=total_findings,
302
+ total_critical=total_critical,
303
+ total_warnings=total_warnings,
304
+ )
305
+
306
+ def _classify_finding(self, finding: Finding) -> Optional[str]:
307
+ """Classify a finding into a scoring category."""
308
+ rule_id = finding.rule_id
309
+ for prefix, category in _CATEGORY_MAP.items():
310
+ if rule_id.startswith(prefix):
311
+ return category
312
+ return None
313
+
314
+ @staticmethod
315
+ def _score_category(
316
+ name: str,
317
+ display_name: str,
318
+ description: str,
319
+ findings: List[Finding],
320
+ weight: float,
321
+ ) -> CategoryScore:
322
+ """Score a single category from its findings."""
323
+ # Start at 100, subtract penalties
324
+ score = 100.0
325
+
326
+ critical_count = 0
327
+ warning_count = 0
328
+ top_issues: List[str] = []
329
+
330
+ for finding in findings:
331
+ if finding.severity == Severity.PASSED:
332
+ continue
333
+
334
+ penalty = _SEVERITY_PENALTY.get(finding.severity, 0.0)
335
+
336
+ # Scale penalty by confidence
337
+ penalty *= finding.confidence
338
+
339
+ score -= penalty
340
+
341
+ if finding.severity == Severity.CRITICAL:
342
+ critical_count += 1
343
+ if len(top_issues) < 3:
344
+ top_issues.append(finding.title)
345
+ elif finding.severity == Severity.WARNING:
346
+ warning_count += 1
347
+ if len(top_issues) < 3 and critical_count == 0:
348
+ top_issues.append(finding.title)
349
+
350
+ score = max(0.0, min(100.0, score))
351
+
352
+ finding_count = sum(
353
+ 1 for f in findings if f.severity != Severity.PASSED
354
+ )
355
+
356
+ return CategoryScore(
357
+ name=name,
358
+ display_name=display_name,
359
+ description=description,
360
+ score=score,
361
+ weight=weight,
362
+ weighted_score=score * weight,
363
+ finding_count=finding_count,
364
+ critical_count=critical_count,
365
+ warning_count=warning_count,
366
+ top_issues=top_issues,
367
+ )
368
+
369
+ @staticmethod
370
+ def _score_label(score: float) -> str:
371
+ if score >= 90:
372
+ return "Excellent"
373
+ elif score >= 70:
374
+ return "Good"
375
+ elif score >= 50:
376
+ return "Needs Attention"
377
+ elif score >= 30:
378
+ return "Poor"
379
+ else:
380
+ return "Critical"
381
+
382
+
383
+ # ---------------------------------------------------------------------------
384
+ # Convenience function
385
+ # ---------------------------------------------------------------------------
386
+
387
+ def compute_health_score(
388
+ findings: List[Finding],
389
+ has_gpu: bool = False,
390
+ is_embedded: bool = False,
391
+ weights: Optional[Dict[str, float]] = None,
392
+ ) -> ScoreBreakdown:
393
+ """Compute the v2 health score breakdown.
394
+
395
+ Parameters
396
+ ----------
397
+ findings:
398
+ All findings from the scan.
399
+ has_gpu:
400
+ Whether the system has a GPU (affects weight distribution).
401
+ is_embedded:
402
+ Whether running on an embedded board (affects weight distribution).
403
+ weights:
404
+ Optional custom category weights (must sum to 1.0).
405
+
406
+ Returns
407
+ -------
408
+ ScoreBreakdown
409
+ Complete health score breakdown.
410
+ """
411
+ engine = HealthScoreV2(
412
+ weights=weights,
413
+ has_gpu=has_gpu,
414
+ is_embedded=is_embedded,
415
+ )
416
+ return engine.score(findings)
@@ -0,0 +1,20 @@
1
+ """Environment snapshot and verification system.
2
+
3
+ Captures complete binary compatibility state including hashes, GPU stack,
4
+ and system libraries - information that pip freeze/conda export don't provide.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .generator import SnapshotGenerator, create_snapshot
10
+ from .verifier import SnapshotVerifier, verify_snapshot
11
+ from .lockfile import Lockfile, load_lockfile
12
+
13
+ __all__ = [
14
+ "SnapshotGenerator",
15
+ "create_snapshot",
16
+ "SnapshotVerifier",
17
+ "verify_snapshot",
18
+ "Lockfile",
19
+ "load_lockfile",
20
+ ]
@@ -0,0 +1,168 @@
1
+ """Environment snapshot generator.
2
+
3
+ Creates lockfiles with binary hashes, GPU stack info, and system configuration.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import platform
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import List, Optional
13
+
14
+ from pybinaryguard.models.system import SystemProfile
15
+ from pybinaryguard.models.package import PackageBinaryInfo
16
+ from pybinaryguard.snapshot.lockfile import Lockfile, PackageSnapshot, SystemSnapshot
17
+ from pybinaryguard.profiles import match_board_profile
18
+
19
+
20
+ class SnapshotGenerator:
21
+ """Generates environment snapshots with binary fingerprints."""
22
+
23
+ def __init__(
24
+ self,
25
+ profile: SystemProfile,
26
+ packages: List[PackageBinaryInfo]
27
+ ):
28
+ """Initialize snapshot generator.
29
+
30
+ Args:
31
+ profile: System profile
32
+ packages: List of installed packages
33
+ """
34
+ self.profile = profile
35
+ self.packages = packages
36
+
37
+ def generate(self, include_hashes: bool = True) -> Lockfile:
38
+ """Generate a complete environment snapshot.
39
+
40
+ Args:
41
+ include_hashes: Whether to compute binary hashes (slower but more secure)
42
+
43
+ Returns:
44
+ Lockfile with complete environment state
45
+ """
46
+ # Build system snapshot
47
+ system_snapshot = self._build_system_snapshot()
48
+
49
+ # Build package snapshots
50
+ package_snapshots = []
51
+ for pkg in self.packages:
52
+ pkg_snapshot = self._build_package_snapshot(pkg, include_hashes)
53
+ package_snapshots.append(pkg_snapshot)
54
+
55
+ # Sort packages by name for consistency
56
+ package_snapshots.sort(key=lambda p: p.name.lower())
57
+
58
+ # Create lockfile
59
+ lockfile = Lockfile(
60
+ version="1.0",
61
+ timestamp=datetime.utcnow().isoformat() + "Z",
62
+ system=system_snapshot,
63
+ packages=package_snapshots,
64
+ metadata={
65
+ "generated_by": "pybinaryguard",
66
+ "total_packages": len(package_snapshots),
67
+ "packages_with_binaries": sum(
68
+ 1 for p in package_snapshots if p.binary_hashes
69
+ ),
70
+ },
71
+ )
72
+
73
+ return lockfile
74
+
75
+ def _build_system_snapshot(self) -> SystemSnapshot:
76
+ """Build system configuration snapshot."""
77
+ # Detect board profile
78
+ board_profile = match_board_profile(self.profile)
79
+ detected_board = board_profile.display_name if board_profile else None
80
+
81
+ # Get CPU flags
82
+ cpu_flags = []
83
+ if self.profile.cpu_flags:
84
+ # Extract key flags
85
+ flags_str = self.profile.cpu_flags.lower()
86
+ for flag in ["avx", "avx2", "avx512", "sse4_2", "neon", "fma"]:
87
+ if flag in flags_str:
88
+ cpu_flags.append(flag)
89
+
90
+ # Build Python version string
91
+ py_ver = self.profile.python_version
92
+ python_version = f"{py_ver[0]}.{py_ver[1]}.{py_ver[2]}" if py_ver else ""
93
+
94
+ return SystemSnapshot(
95
+ python_version=python_version,
96
+ platform=platform.system(),
97
+ architecture=self.profile.architecture.value if self.profile.architecture else "",
98
+ glibc_version=self.profile.glibc_version,
99
+ cuda_runtime=self.profile.cuda_runtime_version,
100
+ cuda_driver=self.profile.cuda_driver_version,
101
+ cuda_compute_capability=self.profile.cuda_compute_capability,
102
+ tensorrt_version=self.profile.tensorrt_version,
103
+ cudnn_version=self.profile.cudnn_version,
104
+ detected_board=detected_board,
105
+ cpu_flags=cpu_flags,
106
+ container_runtime=self.profile.container_runtime.value if self.profile.container_runtime else None,
107
+ )
108
+
109
+ def _build_package_snapshot(
110
+ self,
111
+ package: PackageBinaryInfo,
112
+ include_hashes: bool
113
+ ) -> PackageSnapshot:
114
+ """Build snapshot for a single package."""
115
+ binary_hashes = {}
116
+
117
+ if include_hashes and package.has_binaries:
118
+ for so in package.shared_objects:
119
+ if so.path and Path(so.path).exists():
120
+ # Compute SHA256 hash
121
+ hash_value = self._compute_sha256(so.path)
122
+ # Use relative path as key
123
+ rel_path = so.path.replace(package.install_path, "").lstrip("/")
124
+ binary_hashes[rel_path] = hash_value
125
+
126
+ # Get wheel tags if available
127
+ wheel_tags = []
128
+ if hasattr(package, 'wheel_tags') and package.wheel_tags:
129
+ for tag in package.wheel_tags:
130
+ wheel_tags.append(f"{tag.python_tag}-{tag.abi_tag}-{tag.platform_tag}")
131
+
132
+ return PackageSnapshot(
133
+ name=package.name,
134
+ version=package.version or "unknown",
135
+ cuda_version=package.cuda_version,
136
+ binary_hashes=binary_hashes,
137
+ manylinux_tag=package.manylinux_tag,
138
+ wheel_tags=wheel_tags,
139
+ )
140
+
141
+ @staticmethod
142
+ def _compute_sha256(file_path: str) -> str:
143
+ """Compute SHA256 hash of a file."""
144
+ sha256 = hashlib.sha256()
145
+ with open(file_path, "rb") as f:
146
+ # Read in chunks to handle large files
147
+ for chunk in iter(lambda: f.read(65536), b""):
148
+ sha256.update(chunk)
149
+ return sha256.hexdigest()
150
+
151
+
152
+ def create_snapshot(
153
+ profile: SystemProfile,
154
+ packages: List[PackageBinaryInfo],
155
+ include_hashes: bool = True
156
+ ) -> Lockfile:
157
+ """Convenience function to create an environment snapshot.
158
+
159
+ Args:
160
+ profile: System profile
161
+ packages: List of installed packages
162
+ include_hashes: Whether to compute binary hashes
163
+
164
+ Returns:
165
+ Generated lockfile
166
+ """
167
+ generator = SnapshotGenerator(profile, packages)
168
+ return generator.generate(include_hashes)