fbuild 1.2.8__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 (121) hide show
  1. fbuild/__init__.py +390 -0
  2. fbuild/assets/example.txt +1 -0
  3. fbuild/build/__init__.py +117 -0
  4. fbuild/build/archive_creator.py +186 -0
  5. fbuild/build/binary_generator.py +444 -0
  6. fbuild/build/build_component_factory.py +131 -0
  7. fbuild/build/build_info_generator.py +624 -0
  8. fbuild/build/build_state.py +325 -0
  9. fbuild/build/build_utils.py +93 -0
  10. fbuild/build/compilation_executor.py +422 -0
  11. fbuild/build/compiler.py +165 -0
  12. fbuild/build/compiler_avr.py +574 -0
  13. fbuild/build/configurable_compiler.py +664 -0
  14. fbuild/build/configurable_linker.py +637 -0
  15. fbuild/build/flag_builder.py +214 -0
  16. fbuild/build/library_dependency_processor.py +185 -0
  17. fbuild/build/linker.py +708 -0
  18. fbuild/build/orchestrator.py +67 -0
  19. fbuild/build/orchestrator_avr.py +651 -0
  20. fbuild/build/orchestrator_esp32.py +878 -0
  21. fbuild/build/orchestrator_rp2040.py +719 -0
  22. fbuild/build/orchestrator_stm32.py +696 -0
  23. fbuild/build/orchestrator_teensy.py +580 -0
  24. fbuild/build/source_compilation_orchestrator.py +218 -0
  25. fbuild/build/source_scanner.py +516 -0
  26. fbuild/cli.py +717 -0
  27. fbuild/cli_utils.py +314 -0
  28. fbuild/config/__init__.py +16 -0
  29. fbuild/config/board_config.py +542 -0
  30. fbuild/config/board_loader.py +92 -0
  31. fbuild/config/ini_parser.py +369 -0
  32. fbuild/config/mcu_specs.py +88 -0
  33. fbuild/daemon/__init__.py +42 -0
  34. fbuild/daemon/async_client.py +531 -0
  35. fbuild/daemon/client.py +1505 -0
  36. fbuild/daemon/compilation_queue.py +293 -0
  37. fbuild/daemon/configuration_lock.py +865 -0
  38. fbuild/daemon/daemon.py +585 -0
  39. fbuild/daemon/daemon_context.py +293 -0
  40. fbuild/daemon/error_collector.py +263 -0
  41. fbuild/daemon/file_cache.py +332 -0
  42. fbuild/daemon/firmware_ledger.py +546 -0
  43. fbuild/daemon/lock_manager.py +508 -0
  44. fbuild/daemon/logging_utils.py +149 -0
  45. fbuild/daemon/messages.py +957 -0
  46. fbuild/daemon/operation_registry.py +288 -0
  47. fbuild/daemon/port_state_manager.py +249 -0
  48. fbuild/daemon/process_tracker.py +366 -0
  49. fbuild/daemon/processors/__init__.py +18 -0
  50. fbuild/daemon/processors/build_processor.py +248 -0
  51. fbuild/daemon/processors/deploy_processor.py +664 -0
  52. fbuild/daemon/processors/install_deps_processor.py +431 -0
  53. fbuild/daemon/processors/locking_processor.py +777 -0
  54. fbuild/daemon/processors/monitor_processor.py +285 -0
  55. fbuild/daemon/request_processor.py +457 -0
  56. fbuild/daemon/shared_serial.py +819 -0
  57. fbuild/daemon/status_manager.py +238 -0
  58. fbuild/daemon/subprocess_manager.py +316 -0
  59. fbuild/deploy/__init__.py +21 -0
  60. fbuild/deploy/deployer.py +67 -0
  61. fbuild/deploy/deployer_esp32.py +310 -0
  62. fbuild/deploy/docker_utils.py +315 -0
  63. fbuild/deploy/monitor.py +519 -0
  64. fbuild/deploy/qemu_runner.py +603 -0
  65. fbuild/interrupt_utils.py +34 -0
  66. fbuild/ledger/__init__.py +52 -0
  67. fbuild/ledger/board_ledger.py +560 -0
  68. fbuild/output.py +352 -0
  69. fbuild/packages/__init__.py +66 -0
  70. fbuild/packages/archive_utils.py +1098 -0
  71. fbuild/packages/arduino_core.py +412 -0
  72. fbuild/packages/cache.py +256 -0
  73. fbuild/packages/concurrent_manager.py +510 -0
  74. fbuild/packages/downloader.py +518 -0
  75. fbuild/packages/fingerprint.py +423 -0
  76. fbuild/packages/framework_esp32.py +538 -0
  77. fbuild/packages/framework_rp2040.py +349 -0
  78. fbuild/packages/framework_stm32.py +459 -0
  79. fbuild/packages/framework_teensy.py +346 -0
  80. fbuild/packages/github_utils.py +96 -0
  81. fbuild/packages/header_trampoline_cache.py +394 -0
  82. fbuild/packages/library_compiler.py +203 -0
  83. fbuild/packages/library_manager.py +549 -0
  84. fbuild/packages/library_manager_esp32.py +725 -0
  85. fbuild/packages/package.py +163 -0
  86. fbuild/packages/platform_esp32.py +383 -0
  87. fbuild/packages/platform_rp2040.py +400 -0
  88. fbuild/packages/platform_stm32.py +581 -0
  89. fbuild/packages/platform_teensy.py +312 -0
  90. fbuild/packages/platform_utils.py +131 -0
  91. fbuild/packages/platformio_registry.py +369 -0
  92. fbuild/packages/sdk_utils.py +231 -0
  93. fbuild/packages/toolchain.py +436 -0
  94. fbuild/packages/toolchain_binaries.py +196 -0
  95. fbuild/packages/toolchain_esp32.py +489 -0
  96. fbuild/packages/toolchain_metadata.py +185 -0
  97. fbuild/packages/toolchain_rp2040.py +436 -0
  98. fbuild/packages/toolchain_stm32.py +417 -0
  99. fbuild/packages/toolchain_teensy.py +404 -0
  100. fbuild/platform_configs/esp32.json +150 -0
  101. fbuild/platform_configs/esp32c2.json +144 -0
  102. fbuild/platform_configs/esp32c3.json +143 -0
  103. fbuild/platform_configs/esp32c5.json +151 -0
  104. fbuild/platform_configs/esp32c6.json +151 -0
  105. fbuild/platform_configs/esp32p4.json +149 -0
  106. fbuild/platform_configs/esp32s3.json +151 -0
  107. fbuild/platform_configs/imxrt1062.json +56 -0
  108. fbuild/platform_configs/rp2040.json +70 -0
  109. fbuild/platform_configs/rp2350.json +76 -0
  110. fbuild/platform_configs/stm32f1.json +59 -0
  111. fbuild/platform_configs/stm32f4.json +63 -0
  112. fbuild/py.typed +0 -0
  113. fbuild-1.2.8.dist-info/METADATA +468 -0
  114. fbuild-1.2.8.dist-info/RECORD +121 -0
  115. fbuild-1.2.8.dist-info/WHEEL +5 -0
  116. fbuild-1.2.8.dist-info/entry_points.txt +5 -0
  117. fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
  118. fbuild-1.2.8.dist-info/top_level.txt +2 -0
  119. fbuild_lint/__init__.py +0 -0
  120. fbuild_lint/ruff_plugins/__init__.py +0 -0
  121. fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
@@ -0,0 +1,423 @@
1
+ """Package fingerprinting for artifact validation and persistence.
2
+
3
+ This module provides fingerprinting capabilities for downloaded packages,
4
+ enabling validation of installation state and detection of corruption or
5
+ incomplete installations.
6
+ """
7
+
8
+ import hashlib
9
+ import json
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional
14
+
15
+
16
+ @dataclass
17
+ class PackageFingerprint:
18
+ """Fingerprint for validating package installation state.
19
+
20
+ A fingerprint captures the identity and state of a downloaded and
21
+ extracted package, enabling:
22
+ - Verification that an installation matches expected state
23
+ - Detection of corruption or incomplete extraction
24
+ - Cache validation without re-downloading
25
+ """
26
+
27
+ url: str
28
+ version: str
29
+ url_hash: str # SHA256[:16] of URL (for cache key)
30
+ content_hash: str # SHA256 of downloaded archive
31
+ extracted_files: List[str] = field(default_factory=list) # Key files to verify
32
+ install_timestamp: float = field(default_factory=time.time)
33
+ file_count: int = 0
34
+ total_size: int = 0
35
+ metadata: Dict[str, Any] = field(default_factory=dict)
36
+
37
+ FINGERPRINT_FILENAME = ".fbuild_fingerprint.json"
38
+
39
+ @staticmethod
40
+ def hash_url(url: str) -> str:
41
+ """Generate a SHA256 hash of a URL for cache directory naming.
42
+
43
+ Args:
44
+ url: The base URL to hash
45
+
46
+ Returns:
47
+ First 16 characters of SHA256 hash (sufficient for uniqueness)
48
+ """
49
+ return hashlib.sha256(url.encode("utf-8")).hexdigest()[:16]
50
+
51
+ @staticmethod
52
+ def hash_file(file_path: Path, chunk_size: int = 8192) -> str:
53
+ """Generate SHA256 hash of a file.
54
+
55
+ Args:
56
+ file_path: Path to file to hash
57
+ chunk_size: Size of chunks for reading
58
+
59
+ Returns:
60
+ SHA256 hex digest
61
+ """
62
+ sha256 = hashlib.sha256()
63
+ with open(file_path, "rb") as f:
64
+ for chunk in iter(lambda: f.read(chunk_size), b""):
65
+ sha256.update(chunk)
66
+ return sha256.hexdigest()
67
+
68
+ @classmethod
69
+ def from_archive(
70
+ cls,
71
+ url: str,
72
+ version: str,
73
+ archive_path: Path,
74
+ extracted_dir: Optional[Path] = None,
75
+ key_files: Optional[List[str]] = None,
76
+ metadata: Optional[Dict[str, Any]] = None,
77
+ ) -> "PackageFingerprint":
78
+ """Create fingerprint from a downloaded archive.
79
+
80
+ Args:
81
+ url: Original download URL
82
+ version: Package version string
83
+ archive_path: Path to downloaded archive
84
+ extracted_dir: Optional path to extracted directory for file enumeration
85
+ key_files: Optional list of key file paths to track (relative to extracted_dir)
86
+ metadata: Optional additional metadata to store
87
+
88
+ Returns:
89
+ PackageFingerprint instance
90
+ """
91
+ url_hash = cls.hash_url(url)
92
+ content_hash = cls.hash_file(archive_path)
93
+
94
+ extracted_files: List[str] = []
95
+ file_count = 0
96
+ total_size = 0
97
+
98
+ if extracted_dir and extracted_dir.exists():
99
+ # Enumerate files in extracted directory
100
+ for file_path in extracted_dir.rglob("*"):
101
+ if file_path.is_file():
102
+ file_count += 1
103
+ total_size += file_path.stat().st_size
104
+
105
+ # Track key files for quick validation
106
+ if key_files:
107
+ for key_file in key_files:
108
+ full_path = extracted_dir / key_file
109
+ if full_path.exists():
110
+ extracted_files.append(key_file)
111
+
112
+ return cls(
113
+ url=url,
114
+ version=version,
115
+ url_hash=url_hash,
116
+ content_hash=content_hash,
117
+ extracted_files=extracted_files,
118
+ install_timestamp=time.time(),
119
+ file_count=file_count,
120
+ total_size=total_size,
121
+ metadata=metadata or {},
122
+ )
123
+
124
+ def to_dict(self) -> Dict[str, Any]:
125
+ """Convert fingerprint to dictionary for JSON serialization."""
126
+ return {
127
+ "url": self.url,
128
+ "version": self.version,
129
+ "url_hash": self.url_hash,
130
+ "content_hash": self.content_hash,
131
+ "extracted_files": self.extracted_files,
132
+ "install_timestamp": self.install_timestamp,
133
+ "file_count": self.file_count,
134
+ "total_size": self.total_size,
135
+ "metadata": self.metadata,
136
+ }
137
+
138
+ @classmethod
139
+ def from_dict(cls, data: Dict[str, Any]) -> "PackageFingerprint":
140
+ """Create fingerprint from dictionary."""
141
+ return cls(
142
+ url=data["url"],
143
+ version=data["version"],
144
+ url_hash=data["url_hash"],
145
+ content_hash=data["content_hash"],
146
+ extracted_files=data.get("extracted_files", []),
147
+ install_timestamp=data.get("install_timestamp", 0),
148
+ file_count=data.get("file_count", 0),
149
+ total_size=data.get("total_size", 0),
150
+ metadata=data.get("metadata", {}),
151
+ )
152
+
153
+ def save(self, directory: Path) -> Path:
154
+ """Save fingerprint to directory.
155
+
156
+ Args:
157
+ directory: Directory to save fingerprint file
158
+
159
+ Returns:
160
+ Path to saved fingerprint file
161
+ """
162
+ directory.mkdir(parents=True, exist_ok=True)
163
+ fingerprint_path = directory / self.FINGERPRINT_FILENAME
164
+
165
+ with open(fingerprint_path, "w", encoding="utf-8") as f:
166
+ json.dump(self.to_dict(), f, indent=2)
167
+
168
+ return fingerprint_path
169
+
170
+ @classmethod
171
+ def load(cls, directory: Path) -> Optional["PackageFingerprint"]:
172
+ """Load fingerprint from directory.
173
+
174
+ Args:
175
+ directory: Directory containing fingerprint file
176
+
177
+ Returns:
178
+ PackageFingerprint instance or None if not found/invalid
179
+ """
180
+ fingerprint_path = directory / cls.FINGERPRINT_FILENAME
181
+
182
+ if not fingerprint_path.exists():
183
+ return None
184
+
185
+ try:
186
+ with open(fingerprint_path, "r", encoding="utf-8") as f:
187
+ data = json.load(f)
188
+ return cls.from_dict(data)
189
+ except (json.JSONDecodeError, KeyError, TypeError):
190
+ return None
191
+
192
+ def matches(self, other: "PackageFingerprint") -> bool:
193
+ """Check if fingerprints match (same content).
194
+
195
+ Args:
196
+ other: Another fingerprint to compare
197
+
198
+ Returns:
199
+ True if fingerprints represent the same package installation
200
+ """
201
+ return self.url == other.url and self.version == other.version and self.content_hash == other.content_hash
202
+
203
+ def validate_installation(self, directory: Path) -> tuple[bool, str]:
204
+ """Validate that installation matches fingerprint.
205
+
206
+ Args:
207
+ directory: Directory containing extracted package
208
+
209
+ Returns:
210
+ Tuple of (is_valid, reason)
211
+ """
212
+ if not directory.exists():
213
+ return False, "Directory does not exist"
214
+
215
+ # Check key files exist
216
+ for key_file in self.extracted_files:
217
+ full_path = directory / key_file
218
+ if not full_path.exists():
219
+ return False, f"Missing key file: {key_file}"
220
+
221
+ # Quick file count check (allows for some variance due to temp files)
222
+ if self.file_count > 0:
223
+ actual_count = sum(1 for _ in directory.rglob("*") if _.is_file())
224
+ # Allow 10% variance in file count
225
+ if actual_count < self.file_count * 0.9:
226
+ return False, f"File count mismatch: expected ~{self.file_count}, found {actual_count}"
227
+
228
+ return True, "Installation valid"
229
+
230
+
231
+ class FingerprintRegistry:
232
+ """Registry for managing package fingerprints across a cache.
233
+
234
+ Provides a centralized way to track all installed packages and
235
+ their fingerprints.
236
+ """
237
+
238
+ REGISTRY_FILENAME = ".fbuild_package_registry.json"
239
+
240
+ def __init__(self, cache_root: Path):
241
+ """Initialize fingerprint registry.
242
+
243
+ Args:
244
+ cache_root: Root directory of the cache
245
+ """
246
+ self.cache_root = cache_root
247
+ self.registry_path = cache_root / self.REGISTRY_FILENAME
248
+ self._registry: Dict[str, Dict[str, Any]] = {}
249
+ self._load()
250
+
251
+ def _load(self) -> None:
252
+ """Load registry from disk."""
253
+ if self.registry_path.exists():
254
+ try:
255
+ with open(self.registry_path, "r", encoding="utf-8") as f:
256
+ self._registry = json.load(f)
257
+ except (json.JSONDecodeError, IOError):
258
+ self._registry = {}
259
+
260
+ def _save(self) -> None:
261
+ """Save registry to disk."""
262
+ self.cache_root.mkdir(parents=True, exist_ok=True)
263
+ with open(self.registry_path, "w", encoding="utf-8") as f:
264
+ json.dump(self._registry, f, indent=2)
265
+
266
+ def register(self, fingerprint: PackageFingerprint, install_path: Path) -> None:
267
+ """Register a package installation.
268
+
269
+ Args:
270
+ fingerprint: Package fingerprint
271
+ install_path: Path where package is installed
272
+ """
273
+ key = f"{fingerprint.url_hash}:{fingerprint.version}"
274
+ self._registry[key] = {
275
+ "fingerprint": fingerprint.to_dict(),
276
+ "install_path": str(install_path),
277
+ "registered_at": time.time(),
278
+ }
279
+ self._save()
280
+
281
+ def get_fingerprint(self, url: str, version: str) -> Optional[PackageFingerprint]:
282
+ """Get fingerprint for a package.
283
+
284
+ Args:
285
+ url: Package URL
286
+ version: Package version
287
+
288
+ Returns:
289
+ PackageFingerprint or None if not registered
290
+ """
291
+ url_hash = PackageFingerprint.hash_url(url)
292
+ key = f"{url_hash}:{version}"
293
+
294
+ if key not in self._registry:
295
+ return None
296
+
297
+ try:
298
+ return PackageFingerprint.from_dict(self._registry[key]["fingerprint"])
299
+ except (KeyError, TypeError):
300
+ return None
301
+
302
+ def get_install_path(self, url: str, version: str) -> Optional[Path]:
303
+ """Get installation path for a package.
304
+
305
+ Args:
306
+ url: Package URL
307
+ version: Package version
308
+
309
+ Returns:
310
+ Installation path or None if not registered
311
+ """
312
+ url_hash = PackageFingerprint.hash_url(url)
313
+ key = f"{url_hash}:{version}"
314
+
315
+ if key not in self._registry:
316
+ return None
317
+
318
+ try:
319
+ return Path(self._registry[key]["install_path"])
320
+ except (KeyError, TypeError):
321
+ return None
322
+
323
+ def is_installed(self, url: str, version: str) -> bool:
324
+ """Check if a package is installed and valid.
325
+
326
+ Args:
327
+ url: Package URL
328
+ version: Package version
329
+
330
+ Returns:
331
+ True if package is installed and fingerprint is valid
332
+ """
333
+ fingerprint = self.get_fingerprint(url, version)
334
+ if fingerprint is None:
335
+ return False
336
+
337
+ install_path = self.get_install_path(url, version)
338
+ if install_path is None or not install_path.exists():
339
+ return False
340
+
341
+ is_valid, _ = fingerprint.validate_installation(install_path)
342
+ return is_valid
343
+
344
+ def unregister(self, url: str, version: str) -> bool:
345
+ """Unregister a package.
346
+
347
+ Args:
348
+ url: Package URL
349
+ version: Package version
350
+
351
+ Returns:
352
+ True if package was registered and removed
353
+ """
354
+ url_hash = PackageFingerprint.hash_url(url)
355
+ key = f"{url_hash}:{version}"
356
+
357
+ if key in self._registry:
358
+ del self._registry[key]
359
+ self._save()
360
+ return True
361
+ return False
362
+
363
+ def list_packages(self) -> List[Dict[str, Any]]:
364
+ """List all registered packages.
365
+
366
+ Returns:
367
+ List of package information dictionaries
368
+ """
369
+ packages = []
370
+ for _key, entry in self._registry.items():
371
+ try:
372
+ fingerprint = PackageFingerprint.from_dict(entry["fingerprint"])
373
+ install_path = Path(entry["install_path"])
374
+ is_valid, reason = fingerprint.validate_installation(install_path)
375
+
376
+ packages.append(
377
+ {
378
+ "url": fingerprint.url,
379
+ "version": fingerprint.version,
380
+ "url_hash": fingerprint.url_hash,
381
+ "install_path": str(install_path),
382
+ "is_valid": is_valid,
383
+ "validation_reason": reason,
384
+ "file_count": fingerprint.file_count,
385
+ "total_size": fingerprint.total_size,
386
+ "install_timestamp": fingerprint.install_timestamp,
387
+ }
388
+ )
389
+ except (KeyError, TypeError):
390
+ continue
391
+
392
+ return packages
393
+
394
+ def cleanup_invalid(self) -> int:
395
+ """Remove entries for invalid/missing installations.
396
+
397
+ Returns:
398
+ Number of entries removed
399
+ """
400
+ keys_to_remove = []
401
+
402
+ for key, entry in self._registry.items():
403
+ try:
404
+ fingerprint = PackageFingerprint.from_dict(entry["fingerprint"])
405
+ install_path = Path(entry["install_path"])
406
+
407
+ if not install_path.exists():
408
+ keys_to_remove.append(key)
409
+ continue
410
+
411
+ is_valid, _ = fingerprint.validate_installation(install_path)
412
+ if not is_valid:
413
+ keys_to_remove.append(key)
414
+ except (KeyError, TypeError):
415
+ keys_to_remove.append(key)
416
+
417
+ for key in keys_to_remove:
418
+ del self._registry[key]
419
+
420
+ if keys_to_remove:
421
+ self._save()
422
+
423
+ return len(keys_to_remove)