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.
- fbuild/__init__.py +0 -0
- fbuild/assets/example.txt +1 -0
- fbuild/build/__init__.py +117 -0
- fbuild/build/archive_creator.py +186 -0
- fbuild/build/binary_generator.py +444 -0
- fbuild/build/build_component_factory.py +131 -0
- fbuild/build/build_state.py +325 -0
- fbuild/build/build_utils.py +98 -0
- fbuild/build/compilation_executor.py +422 -0
- fbuild/build/compiler.py +165 -0
- fbuild/build/compiler_avr.py +574 -0
- fbuild/build/configurable_compiler.py +612 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +186 -0
- fbuild/build/library_dependency_processor.py +185 -0
- fbuild/build/linker.py +708 -0
- fbuild/build/orchestrator.py +67 -0
- fbuild/build/orchestrator_avr.py +656 -0
- fbuild/build/orchestrator_esp32.py +797 -0
- fbuild/build/orchestrator_teensy.py +543 -0
- fbuild/build/source_compilation_orchestrator.py +220 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +566 -0
- fbuild/cli_utils.py +312 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +457 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +209 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +34 -0
- fbuild/daemon/client.py +929 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/daemon.py +474 -0
- fbuild/daemon/daemon_context.py +196 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/lock_manager.py +270 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +301 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +12 -0
- fbuild/daemon/processors/build_processor.py +157 -0
- fbuild/daemon/processors/deploy_processor.py +327 -0
- fbuild/daemon/processors/monitor_processor.py +146 -0
- fbuild/daemon/request_processor.py +401 -0
- fbuild/daemon/status_manager.py +216 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +17 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +314 -0
- fbuild/deploy/monitor.py +495 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/packages/__init__.py +53 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +249 -0
- fbuild/packages/downloader.py +366 -0
- fbuild/packages/framework_esp32.py +538 -0
- fbuild/packages/framework_teensy.py +346 -0
- fbuild/packages/github_utils.py +96 -0
- fbuild/packages/header_trampoline_cache.py +394 -0
- fbuild/packages/library_compiler.py +203 -0
- fbuild/packages/library_manager.py +549 -0
- fbuild/packages/library_manager_esp32.py +413 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +325 -0
- fbuild/packages/sdk_utils.py +231 -0
- fbuild/packages/toolchain.py +436 -0
- fbuild/packages/toolchain_binaries.py +196 -0
- fbuild/packages/toolchain_esp32.py +484 -0
- fbuild/packages/toolchain_metadata.py +185 -0
- fbuild/packages/toolchain_teensy.py +404 -0
- fbuild/platform_configs/esp32.json +150 -0
- fbuild/platform_configs/esp32c2.json +144 -0
- fbuild/platform_configs/esp32c3.json +143 -0
- fbuild/platform_configs/esp32c5.json +151 -0
- fbuild/platform_configs/esp32c6.json +151 -0
- fbuild/platform_configs/esp32p4.json +149 -0
- fbuild/platform_configs/esp32s3.json +151 -0
- fbuild/platform_configs/imxrt1062.json +56 -0
- fbuild-1.1.0.dist-info/METADATA +447 -0
- fbuild-1.1.0.dist-info/RECORD +93 -0
- fbuild-1.1.0.dist-info/WHEEL +5 -0
- fbuild-1.1.0.dist-info/entry_points.txt +5 -0
- fbuild-1.1.0.dist-info/licenses/LICENSE +21 -0
- fbuild-1.1.0.dist-info/top_level.txt +2 -0
- fbuild_lint/__init__.py +0 -0
- fbuild_lint/ruff_plugins/__init__.py +0 -0
- 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
|