fbuild 1.1.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.

Potentially problematic release.


This version of fbuild might be problematic. Click here for more details.

Files changed (93) hide show
  1. fbuild/__init__.py +0 -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_state.py +325 -0
  8. fbuild/build/build_utils.py +98 -0
  9. fbuild/build/compilation_executor.py +422 -0
  10. fbuild/build/compiler.py +165 -0
  11. fbuild/build/compiler_avr.py +574 -0
  12. fbuild/build/configurable_compiler.py +612 -0
  13. fbuild/build/configurable_linker.py +637 -0
  14. fbuild/build/flag_builder.py +186 -0
  15. fbuild/build/library_dependency_processor.py +185 -0
  16. fbuild/build/linker.py +708 -0
  17. fbuild/build/orchestrator.py +67 -0
  18. fbuild/build/orchestrator_avr.py +656 -0
  19. fbuild/build/orchestrator_esp32.py +797 -0
  20. fbuild/build/orchestrator_teensy.py +543 -0
  21. fbuild/build/source_compilation_orchestrator.py +220 -0
  22. fbuild/build/source_scanner.py +516 -0
  23. fbuild/cli.py +566 -0
  24. fbuild/cli_utils.py +312 -0
  25. fbuild/config/__init__.py +16 -0
  26. fbuild/config/board_config.py +457 -0
  27. fbuild/config/board_loader.py +92 -0
  28. fbuild/config/ini_parser.py +209 -0
  29. fbuild/config/mcu_specs.py +88 -0
  30. fbuild/daemon/__init__.py +34 -0
  31. fbuild/daemon/client.py +929 -0
  32. fbuild/daemon/compilation_queue.py +293 -0
  33. fbuild/daemon/daemon.py +474 -0
  34. fbuild/daemon/daemon_context.py +196 -0
  35. fbuild/daemon/error_collector.py +263 -0
  36. fbuild/daemon/file_cache.py +332 -0
  37. fbuild/daemon/lock_manager.py +270 -0
  38. fbuild/daemon/logging_utils.py +149 -0
  39. fbuild/daemon/messages.py +301 -0
  40. fbuild/daemon/operation_registry.py +288 -0
  41. fbuild/daemon/process_tracker.py +366 -0
  42. fbuild/daemon/processors/__init__.py +12 -0
  43. fbuild/daemon/processors/build_processor.py +157 -0
  44. fbuild/daemon/processors/deploy_processor.py +327 -0
  45. fbuild/daemon/processors/monitor_processor.py +146 -0
  46. fbuild/daemon/request_processor.py +401 -0
  47. fbuild/daemon/status_manager.py +216 -0
  48. fbuild/daemon/subprocess_manager.py +316 -0
  49. fbuild/deploy/__init__.py +17 -0
  50. fbuild/deploy/deployer.py +67 -0
  51. fbuild/deploy/deployer_esp32.py +314 -0
  52. fbuild/deploy/monitor.py +495 -0
  53. fbuild/interrupt_utils.py +34 -0
  54. fbuild/packages/__init__.py +53 -0
  55. fbuild/packages/archive_utils.py +1098 -0
  56. fbuild/packages/arduino_core.py +412 -0
  57. fbuild/packages/cache.py +249 -0
  58. fbuild/packages/downloader.py +366 -0
  59. fbuild/packages/framework_esp32.py +538 -0
  60. fbuild/packages/framework_teensy.py +346 -0
  61. fbuild/packages/github_utils.py +96 -0
  62. fbuild/packages/header_trampoline_cache.py +394 -0
  63. fbuild/packages/library_compiler.py +203 -0
  64. fbuild/packages/library_manager.py +549 -0
  65. fbuild/packages/library_manager_esp32.py +413 -0
  66. fbuild/packages/package.py +163 -0
  67. fbuild/packages/platform_esp32.py +383 -0
  68. fbuild/packages/platform_teensy.py +312 -0
  69. fbuild/packages/platform_utils.py +131 -0
  70. fbuild/packages/platformio_registry.py +325 -0
  71. fbuild/packages/sdk_utils.py +231 -0
  72. fbuild/packages/toolchain.py +436 -0
  73. fbuild/packages/toolchain_binaries.py +196 -0
  74. fbuild/packages/toolchain_esp32.py +484 -0
  75. fbuild/packages/toolchain_metadata.py +185 -0
  76. fbuild/packages/toolchain_teensy.py +404 -0
  77. fbuild/platform_configs/esp32.json +150 -0
  78. fbuild/platform_configs/esp32c2.json +144 -0
  79. fbuild/platform_configs/esp32c3.json +143 -0
  80. fbuild/platform_configs/esp32c5.json +151 -0
  81. fbuild/platform_configs/esp32c6.json +151 -0
  82. fbuild/platform_configs/esp32p4.json +149 -0
  83. fbuild/platform_configs/esp32s3.json +151 -0
  84. fbuild/platform_configs/imxrt1062.json +56 -0
  85. fbuild-1.1.0.dist-info/METADATA +447 -0
  86. fbuild-1.1.0.dist-info/RECORD +93 -0
  87. fbuild-1.1.0.dist-info/WHEEL +5 -0
  88. fbuild-1.1.0.dist-info/entry_points.txt +5 -0
  89. fbuild-1.1.0.dist-info/licenses/LICENSE +21 -0
  90. fbuild-1.1.0.dist-info/top_level.txt +2 -0
  91. fbuild_lint/__init__.py +0 -0
  92. fbuild_lint/ruff_plugins/__init__.py +0 -0
  93. fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
@@ -0,0 +1,366 @@
1
+ """Package downloader with progress tracking and checksum verification.
2
+
3
+ This module handles downloading packages from URLs, extracting archives,
4
+ and verifying integrity with checksums.
5
+ """
6
+
7
+ import gc
8
+ import hashlib
9
+ import platform
10
+ import tarfile
11
+ import time
12
+ import zipfile
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar
15
+ from urllib.parse import urlparse
16
+
17
+ if TYPE_CHECKING:
18
+ import requests
19
+ from tqdm import tqdm
20
+
21
+ try:
22
+ import requests
23
+ from tqdm import tqdm
24
+
25
+ REQUESTS_AVAILABLE = True
26
+ except ImportError:
27
+ REQUESTS_AVAILABLE = False
28
+ requests: Any = None
29
+ tqdm: Any = None
30
+
31
+
32
+ class DownloadError(Exception):
33
+ """Raised when download fails."""
34
+
35
+ pass
36
+
37
+
38
+ class ChecksumError(Exception):
39
+ """Raised when checksum verification fails."""
40
+
41
+ pass
42
+
43
+
44
+ class ExtractionError(Exception):
45
+ """Raised when archive extraction fails."""
46
+
47
+ pass
48
+
49
+
50
+ T = TypeVar("T")
51
+
52
+
53
+ def _retry_windows_file_operation(
54
+ operation: Callable[[], T],
55
+ max_retries: int = 10,
56
+ initial_delay: float = 0.05,
57
+ ) -> T:
58
+ """Retry a file operation on Windows to handle transient locking issues.
59
+
60
+ Windows file handles can be delayed in release due to antivirus scanning,
61
+ delayed garbage collection, or OS-level file caching. This function retries
62
+ file operations with exponential backoff to handle these transient issues.
63
+
64
+ Args:
65
+ operation: Callable that performs the file operation
66
+ max_retries: Maximum number of retry attempts
67
+ initial_delay: Initial delay in seconds (doubles each retry)
68
+
69
+ Returns:
70
+ Result of the operation
71
+
72
+ Raises:
73
+ The last exception encountered if all retries fail
74
+ """
75
+ is_windows = platform.system() == "Windows"
76
+
77
+ if not is_windows:
78
+ # On non-Windows systems, just call the function directly
79
+ return operation()
80
+
81
+ # Windows: use retry logic for file operations
82
+ delay = initial_delay
83
+ last_exception = None
84
+
85
+ for attempt in range(max_retries):
86
+ try:
87
+ # Force garbage collection to release file handles
88
+ if attempt > 0:
89
+ gc.collect()
90
+ time.sleep(delay)
91
+
92
+ return operation()
93
+
94
+ except (PermissionError, OSError, FileNotFoundError) as e:
95
+ # WinError 32: File is being used by another process
96
+ # WinError 2: File not found (temp file disappeared due to handle delays)
97
+ # Also catch OSError with errno 13 (access denied) or 32 (in use)
98
+ last_exception = e
99
+
100
+ # Check if this is a retriable error
101
+ is_retriable = False
102
+ if isinstance(e, (PermissionError, FileNotFoundError)):
103
+ is_retriable = True
104
+ elif hasattr(e, "errno") and e.errno in (2, 13, 32):
105
+ is_retriable = True
106
+
107
+ if is_retriable and attempt < max_retries - 1:
108
+ delay = min(delay * 2, 2.0) # Exponential backoff, max 2s
109
+ continue
110
+
111
+ # Not retriable or exhausted retries
112
+ raise
113
+
114
+ # If we get here, all retries failed
115
+ if last_exception:
116
+ raise last_exception
117
+ raise PermissionError(f"Failed to perform file operation after {max_retries} attempts")
118
+
119
+
120
+ class PackageDownloader:
121
+ """Downloads and extracts packages with progress tracking."""
122
+
123
+ def __init__(self, chunk_size: int = 8192):
124
+ """Initialize downloader.
125
+
126
+ Args:
127
+ chunk_size: Size of chunks for downloading and hashing
128
+ """
129
+ self.chunk_size = chunk_size
130
+
131
+ if not REQUESTS_AVAILABLE:
132
+ raise ImportError("requests and tqdm are required for downloading. " + "Install with: pip install requests tqdm")
133
+
134
+ def download(
135
+ self,
136
+ url: str,
137
+ dest_path: Path,
138
+ checksum: Optional[str] = None,
139
+ show_progress: bool = True,
140
+ ) -> Path:
141
+ """Download a file from a URL.
142
+
143
+ Args:
144
+ url: URL to download from
145
+ dest_path: Destination file path
146
+ checksum: Optional SHA256 checksum for verification
147
+ show_progress: Whether to show progress bar
148
+
149
+ Returns:
150
+ Path to the downloaded file
151
+
152
+ Raises:
153
+ DownloadError: If download fails
154
+ ChecksumError: If checksum verification fails
155
+ """
156
+ dest_path = Path(dest_path)
157
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
158
+
159
+ # Use temporary file during download
160
+ temp_file = dest_path.with_suffix(dest_path.suffix + ".tmp")
161
+
162
+ try:
163
+ # Start download with streaming
164
+ response = requests.get(url, stream=True, timeout=30)
165
+ response.raise_for_status()
166
+
167
+ # Get file size for progress bar
168
+ total_size = int(response.headers.get("content-length", 0))
169
+
170
+ # Setup progress bar
171
+ progress_bar = None
172
+ if show_progress and total_size > 0:
173
+ filename = Path(urlparse(url).path).name
174
+ progress_bar = tqdm(
175
+ total=total_size,
176
+ unit="B",
177
+ unit_scale=True,
178
+ unit_divisor=1024,
179
+ desc=f"Downloading {filename}",
180
+ )
181
+
182
+ # Download file
183
+ sha256 = hashlib.sha256() if checksum else None
184
+
185
+ with open(temp_file, "wb") as f:
186
+ for chunk in response.iter_content(chunk_size=self.chunk_size):
187
+ if chunk:
188
+ f.write(chunk)
189
+ if progress_bar:
190
+ progress_bar.update(len(chunk))
191
+ if sha256:
192
+ sha256.update(chunk)
193
+
194
+ if progress_bar:
195
+ progress_bar.close()
196
+
197
+ # Force garbage collection to help release file handles (Windows)
198
+ gc.collect()
199
+
200
+ # On Windows, add delay to let file handles stabilize after write
201
+ if platform.system() == "Windows":
202
+ time.sleep(0.2)
203
+
204
+ # Verify checksum if provided
205
+ if checksum and sha256:
206
+ actual_checksum = sha256.hexdigest()
207
+ if actual_checksum.lower() != checksum.lower():
208
+ # Delete temp file before raising (not inside retry wrapper)
209
+ try:
210
+ _retry_windows_file_operation(lambda: temp_file.unlink())
211
+ except KeyboardInterrupt as ke:
212
+ from fbuild.interrupt_utils import (
213
+ handle_keyboard_interrupt_properly,
214
+ )
215
+
216
+ handle_keyboard_interrupt_properly(ke)
217
+ except Exception:
218
+ pass # Ignore unlink errors, we're about to raise checksum error anyway
219
+ # Raise checksum error (NOT inside retry wrapper)
220
+ raise ChecksumError(f"Checksum mismatch for {url}\n" + f"Expected: {checksum}\n" + f"Got: {actual_checksum}")
221
+
222
+ # Move temp file to final destination
223
+ if dest_path.exists():
224
+ _retry_windows_file_operation(lambda: dest_path.unlink())
225
+ _retry_windows_file_operation(lambda: temp_file.rename(dest_path))
226
+
227
+ return dest_path
228
+
229
+ except requests.RequestException as e:
230
+ if temp_file.exists():
231
+ _retry_windows_file_operation(lambda: temp_file.unlink())
232
+ raise DownloadError(f"Failed to download {url}: {e}")
233
+
234
+ except KeyboardInterrupt as ke:
235
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
236
+
237
+ handle_keyboard_interrupt_properly(ke)
238
+ raise # Never reached, but satisfies type checker
239
+ except Exception:
240
+ if temp_file.exists():
241
+ _retry_windows_file_operation(lambda: temp_file.unlink())
242
+ raise
243
+
244
+ def extract_archive(self, archive_path: Path, dest_dir: Path, show_progress: bool = True) -> Path:
245
+ """Extract an archive file.
246
+
247
+ Supports .tar.gz, .tar.bz2, .tar.xz, and .zip formats.
248
+
249
+ Args:
250
+ archive_path: Path to the archive file
251
+ dest_dir: Destination directory for extraction
252
+ show_progress: Whether to show progress information
253
+
254
+ Returns:
255
+ Path to the extracted directory
256
+
257
+ Raises:
258
+ ExtractionError: If extraction fails
259
+ """
260
+ archive_path = Path(archive_path)
261
+ dest_dir = Path(dest_dir)
262
+
263
+ if not archive_path.exists():
264
+ raise ExtractionError(f"Archive not found: {archive_path}")
265
+
266
+ dest_dir.mkdir(parents=True, exist_ok=True)
267
+
268
+ try:
269
+ if show_progress:
270
+ print(f"Extracting {archive_path.name}...")
271
+
272
+ # Determine archive type and extract
273
+ if archive_path.suffix == ".zip":
274
+ self._extract_zip(archive_path, dest_dir)
275
+ elif archive_path.name.endswith((".tar.gz", ".tar.bz2", ".tar.xz")):
276
+ self._extract_tar(archive_path, dest_dir)
277
+ else:
278
+ raise ExtractionError(f"Unsupported archive format: {archive_path.suffix}")
279
+
280
+ return dest_dir
281
+
282
+ except KeyboardInterrupt as ke:
283
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
284
+
285
+ handle_keyboard_interrupt_properly(ke)
286
+ raise # Never reached, but satisfies type checker
287
+ except Exception as e:
288
+ raise ExtractionError(f"Failed to extract {archive_path}: {e}")
289
+
290
+ def _extract_tar(self, archive_path: Path, dest_dir: Path) -> None:
291
+ """Extract a tar archive.
292
+
293
+ Args:
294
+ archive_path: Path to tar archive
295
+ dest_dir: Destination directory
296
+ """
297
+ with tarfile.open(archive_path, "r:*") as tar:
298
+ tar.extractall(dest_dir)
299
+
300
+ def _extract_zip(self, archive_path: Path, dest_dir: Path) -> None:
301
+ """Extract a zip archive.
302
+
303
+ Args:
304
+ archive_path: Path to zip archive
305
+ dest_dir: Destination directory
306
+ """
307
+ with zipfile.ZipFile(archive_path, "r") as zip_file:
308
+ zip_file.extractall(dest_dir)
309
+
310
+ def download_and_extract(
311
+ self,
312
+ url: str,
313
+ cache_dir: Path,
314
+ extract_dir: Path,
315
+ checksum: Optional[str] = None,
316
+ show_progress: bool = True,
317
+ ) -> Path:
318
+ """Download and extract a package in one operation.
319
+
320
+ Args:
321
+ url: URL to download from
322
+ cache_dir: Directory to cache the downloaded archive
323
+ extract_dir: Directory to extract to
324
+ checksum: Optional SHA256 checksum
325
+ show_progress: Whether to show progress
326
+
327
+ Returns:
328
+ Path to the extracted directory
329
+ """
330
+ # Determine archive filename from URL
331
+ filename = Path(urlparse(url).path).name
332
+ archive_path = cache_dir / filename
333
+
334
+ # Download if not cached
335
+ if not archive_path.exists():
336
+ self.download(url, archive_path, checksum, show_progress)
337
+ elif show_progress:
338
+ print(f"Using cached {filename}")
339
+
340
+ # Extract
341
+ return self.extract_archive(archive_path, extract_dir, show_progress)
342
+
343
+ def verify_checksum(self, file_path: Path, expected: str) -> bool:
344
+ """Verify SHA256 checksum of a file.
345
+
346
+ Args:
347
+ file_path: Path to file to verify
348
+ expected: Expected SHA256 checksum (hex string)
349
+
350
+ Returns:
351
+ True if checksum matches
352
+
353
+ Raises:
354
+ ChecksumError: If checksum doesn't match
355
+ """
356
+ sha256 = hashlib.sha256()
357
+
358
+ with open(file_path, "rb") as f:
359
+ for chunk in iter(lambda: f.read(self.chunk_size), b""):
360
+ sha256.update(chunk)
361
+
362
+ actual = sha256.hexdigest()
363
+ if actual.lower() != expected.lower():
364
+ raise ChecksumError(f"Checksum mismatch for {file_path}\n" + f"Expected: {expected}\n" + f"Got: {actual}")
365
+
366
+ return True