fbuild 1.2.8__py3-none-any.whl → 1.2.15__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.
- fbuild/__init__.py +5 -1
- fbuild/build/configurable_compiler.py +49 -6
- fbuild/build/configurable_linker.py +14 -9
- fbuild/build/orchestrator_esp32.py +6 -3
- fbuild/build/orchestrator_rp2040.py +6 -2
- fbuild/cli.py +300 -5
- fbuild/config/ini_parser.py +13 -1
- fbuild/daemon/__init__.py +11 -0
- fbuild/daemon/async_client.py +5 -4
- fbuild/daemon/async_client_lib.py +1543 -0
- fbuild/daemon/async_protocol.py +825 -0
- fbuild/daemon/async_server.py +2100 -0
- fbuild/daemon/client.py +425 -13
- fbuild/daemon/configuration_lock.py +13 -13
- fbuild/daemon/connection.py +508 -0
- fbuild/daemon/connection_registry.py +579 -0
- fbuild/daemon/daemon.py +517 -164
- fbuild/daemon/daemon_context.py +72 -1
- fbuild/daemon/device_discovery.py +477 -0
- fbuild/daemon/device_manager.py +821 -0
- fbuild/daemon/error_collector.py +263 -263
- fbuild/daemon/file_cache.py +332 -332
- fbuild/daemon/firmware_ledger.py +46 -123
- fbuild/daemon/lock_manager.py +508 -508
- fbuild/daemon/messages.py +431 -0
- fbuild/daemon/operation_registry.py +288 -288
- fbuild/daemon/processors/build_processor.py +34 -1
- fbuild/daemon/processors/deploy_processor.py +1 -3
- fbuild/daemon/processors/locking_processor.py +7 -7
- fbuild/daemon/request_processor.py +457 -457
- fbuild/daemon/shared_serial.py +7 -7
- fbuild/daemon/status_manager.py +238 -238
- fbuild/daemon/subprocess_manager.py +316 -316
- fbuild/deploy/docker_utils.py +182 -2
- fbuild/deploy/monitor.py +1 -1
- fbuild/deploy/qemu_runner.py +71 -13
- fbuild/ledger/board_ledger.py +46 -122
- fbuild/output.py +238 -2
- fbuild/packages/library_compiler.py +15 -5
- fbuild/packages/library_manager.py +12 -6
- fbuild-1.2.15.dist-info/METADATA +569 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/RECORD +46 -39
- fbuild-1.2.8.dist-info/METADATA +0 -468
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/WHEEL +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/entry_points.txt +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/licenses/LICENSE +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/top_level.txt +0 -0
fbuild/output.py
CHANGED
|
@@ -32,12 +32,13 @@ import sys
|
|
|
32
32
|
import time
|
|
33
33
|
from pathlib import Path
|
|
34
34
|
from types import TracebackType
|
|
35
|
-
from typing import Optional, TextIO
|
|
35
|
+
from typing import Optional, Protocol, TextIO, runtime_checkable
|
|
36
36
|
|
|
37
37
|
# Global state for the timer
|
|
38
38
|
_start_time: Optional[float] = None
|
|
39
39
|
_output_stream: TextIO = sys.stdout
|
|
40
40
|
_verbose: bool = True
|
|
41
|
+
_output_file: Optional[TextIO] = None
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
def init_timer(output_stream: Optional[TextIO] = None) -> None:
|
|
@@ -77,6 +78,27 @@ def set_verbose(verbose: bool) -> None:
|
|
|
77
78
|
_verbose = verbose
|
|
78
79
|
|
|
79
80
|
|
|
81
|
+
def set_output_file(output_file: Optional[TextIO]) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Set a file to receive all log output (in addition to stdout).
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
output_file: File object to receive output, or None to disable file output
|
|
87
|
+
"""
|
|
88
|
+
global _output_file
|
|
89
|
+
_output_file = output_file
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_output_file() -> Optional[TextIO]:
|
|
93
|
+
"""
|
|
94
|
+
Get the current output file.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The current output file, or None if not set
|
|
98
|
+
"""
|
|
99
|
+
return _output_file
|
|
100
|
+
|
|
101
|
+
|
|
80
102
|
def get_elapsed() -> float:
|
|
81
103
|
"""
|
|
82
104
|
Get elapsed time since timer initialization.
|
|
@@ -112,9 +134,15 @@ def _print(message: str, end: str = "\n") -> None:
|
|
|
112
134
|
end: End character (default newline)
|
|
113
135
|
"""
|
|
114
136
|
timestamp = format_timestamp()
|
|
115
|
-
|
|
137
|
+
line = f"{timestamp} {message}{end}"
|
|
138
|
+
_output_stream.write(line)
|
|
116
139
|
_output_stream.flush()
|
|
117
140
|
|
|
141
|
+
# Also write to output file if set
|
|
142
|
+
if _output_file is not None:
|
|
143
|
+
_output_file.write(line)
|
|
144
|
+
_output_file.flush()
|
|
145
|
+
|
|
118
146
|
|
|
119
147
|
def log(message: str, verbose_only: bool = False) -> None:
|
|
120
148
|
"""
|
|
@@ -350,3 +378,211 @@ class TimedLogger:
|
|
|
350
378
|
def log(self, message: str) -> None:
|
|
351
379
|
"""Log a message within this operation."""
|
|
352
380
|
log(message, self.verbose_only)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# =============================================================================
|
|
384
|
+
# Progress Callback Protocol and Helpers
|
|
385
|
+
# =============================================================================
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def format_size(size_bytes: int) -> str:
|
|
389
|
+
"""
|
|
390
|
+
Format a byte count as a human-readable size.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
size_bytes: Size in bytes
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Human-readable size string (e.g., "1.2 MB", "340 KB", "12 B")
|
|
397
|
+
"""
|
|
398
|
+
if size_bytes < 0:
|
|
399
|
+
return "0 B"
|
|
400
|
+
if size_bytes < 1024:
|
|
401
|
+
return f"{size_bytes} B"
|
|
402
|
+
elif size_bytes < 1024 * 1024:
|
|
403
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
404
|
+
elif size_bytes < 1024 * 1024 * 1024:
|
|
405
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
406
|
+
else:
|
|
407
|
+
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def format_progress_bar(current: int, total: int, width: int = 28) -> str:
|
|
411
|
+
"""
|
|
412
|
+
Generate an ASCII progress bar.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
current: Current progress value
|
|
416
|
+
total: Total progress value
|
|
417
|
+
width: Width of the progress bar in characters (default 28)
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
ASCII progress bar string like "████████░░░░░░░░░░░░░░░░░░░░"
|
|
421
|
+
|
|
422
|
+
Examples:
|
|
423
|
+
>>> format_progress_bar(5, 10, 20)
|
|
424
|
+
'██████████░░░░░░░░░░'
|
|
425
|
+
>>> format_progress_bar(0, 10, 10)
|
|
426
|
+
'░░░░░░░░░░'
|
|
427
|
+
>>> format_progress_bar(10, 10, 10)
|
|
428
|
+
'██████████'
|
|
429
|
+
"""
|
|
430
|
+
if total <= 0:
|
|
431
|
+
return "░" * width
|
|
432
|
+
ratio = min(1.0, max(0.0, current / total))
|
|
433
|
+
filled = int(width * ratio)
|
|
434
|
+
empty = width - filled
|
|
435
|
+
return "█" * filled + "░" * empty
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@runtime_checkable
|
|
439
|
+
class ProgressCallback(Protocol):
|
|
440
|
+
"""
|
|
441
|
+
Protocol for progress callback implementations.
|
|
442
|
+
|
|
443
|
+
This protocol defines the interface for tracking build progress events.
|
|
444
|
+
Implementations can log to terminal, update TUI elements, or report to daemons.
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
def on_file_start(self, file: str, index: int, total: int) -> None:
|
|
448
|
+
"""
|
|
449
|
+
Called when compilation of a file starts.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
file: Name or path of the file being compiled
|
|
453
|
+
index: Current file index (1-based)
|
|
454
|
+
total: Total number of files to compile
|
|
455
|
+
"""
|
|
456
|
+
...
|
|
457
|
+
|
|
458
|
+
def on_file_complete(self, file: str, index: int, total: int, cached: bool = False) -> None:
|
|
459
|
+
"""
|
|
460
|
+
Called when compilation of a file completes.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
file: Name or path of the file that was compiled
|
|
464
|
+
index: Current file index (1-based)
|
|
465
|
+
total: Total number of files to compile
|
|
466
|
+
cached: If True, the file was served from cache (not recompiled)
|
|
467
|
+
"""
|
|
468
|
+
...
|
|
469
|
+
|
|
470
|
+
def on_download_progress(self, url: str, downloaded: int, total: int) -> None:
|
|
471
|
+
"""
|
|
472
|
+
Called during file download to report progress.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
url: URL being downloaded
|
|
476
|
+
downloaded: Bytes downloaded so far
|
|
477
|
+
total: Total bytes to download (0 if unknown)
|
|
478
|
+
"""
|
|
479
|
+
...
|
|
480
|
+
|
|
481
|
+
def on_extract_progress(self, archive: str, extracted: int, total: int) -> None:
|
|
482
|
+
"""
|
|
483
|
+
Called during archive extraction to report progress.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
archive: Archive file being extracted
|
|
487
|
+
extracted: Number of files/items extracted so far
|
|
488
|
+
total: Total number of files/items to extract
|
|
489
|
+
"""
|
|
490
|
+
...
|
|
491
|
+
|
|
492
|
+
def on_phase_start(self, phase: int, total: int, message: str) -> None:
|
|
493
|
+
"""
|
|
494
|
+
Called when a build phase starts.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
phase: Current phase number (1-based)
|
|
498
|
+
total: Total number of phases
|
|
499
|
+
message: Phase description
|
|
500
|
+
"""
|
|
501
|
+
...
|
|
502
|
+
|
|
503
|
+
def on_phase_complete(self, phase: int, total: int, elapsed: float) -> None:
|
|
504
|
+
"""
|
|
505
|
+
Called when a build phase completes.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
phase: Completed phase number (1-based)
|
|
509
|
+
total: Total number of phases
|
|
510
|
+
elapsed: Time spent in this phase (seconds)
|
|
511
|
+
"""
|
|
512
|
+
...
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
class DefaultProgressCallback:
|
|
516
|
+
"""
|
|
517
|
+
Default implementation of ProgressCallback that logs to the existing output system.
|
|
518
|
+
|
|
519
|
+
This implementation uses log_detail() and log_phase() to display progress
|
|
520
|
+
in a format consistent with the rest of fbuild's output.
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
def __init__(self, verbose_only: bool = False, show_progress_bar: bool = True):
|
|
524
|
+
"""
|
|
525
|
+
Initialize the default progress callback.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
verbose_only: If True, only log when verbose mode is enabled
|
|
529
|
+
show_progress_bar: If True, show ASCII progress bars for downloads/extractions
|
|
530
|
+
"""
|
|
531
|
+
self.verbose_only = verbose_only
|
|
532
|
+
self.show_progress_bar = show_progress_bar
|
|
533
|
+
self._last_download_percent: int = -1
|
|
534
|
+
self._last_extract_percent: int = -1
|
|
535
|
+
|
|
536
|
+
def on_file_start(self, file: str, index: int, total: int) -> None:
|
|
537
|
+
"""Log file compilation start."""
|
|
538
|
+
log_detail(f"[{index}/{total}] Compiling {file}...", verbose_only=self.verbose_only)
|
|
539
|
+
|
|
540
|
+
def on_file_complete(self, file: str, index: int, total: int, cached: bool = False) -> None:
|
|
541
|
+
"""Log file compilation completion."""
|
|
542
|
+
suffix = " (cached)" if cached else ""
|
|
543
|
+
log_detail(f"[{index}/{total}] {file}{suffix}", verbose_only=True)
|
|
544
|
+
|
|
545
|
+
def on_download_progress(self, url: str, downloaded: int, total: int) -> None:
|
|
546
|
+
"""Log download progress (rate-limited to avoid excessive output)."""
|
|
547
|
+
if total <= 0:
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
percent = int(100 * downloaded / total)
|
|
551
|
+
# Only log at 10% intervals to avoid spam
|
|
552
|
+
if percent // 10 == self._last_download_percent // 10 and percent != 100:
|
|
553
|
+
return
|
|
554
|
+
self._last_download_percent = percent
|
|
555
|
+
|
|
556
|
+
filename = url.split("/")[-1][:30]
|
|
557
|
+
if self.show_progress_bar:
|
|
558
|
+
bar = format_progress_bar(downloaded, total, 20)
|
|
559
|
+
log_detail(f"{bar} {percent:3d}% {filename} ({format_size(downloaded)}/{format_size(total)})", verbose_only=self.verbose_only)
|
|
560
|
+
else:
|
|
561
|
+
log_detail(f"Downloading {filename}: {percent}% ({format_size(downloaded)}/{format_size(total)})", verbose_only=self.verbose_only)
|
|
562
|
+
|
|
563
|
+
def on_extract_progress(self, archive: str, extracted: int, total: int) -> None:
|
|
564
|
+
"""Log extraction progress (rate-limited to avoid excessive output)."""
|
|
565
|
+
if total <= 0:
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
percent = int(100 * extracted / total)
|
|
569
|
+
# Only log at 25% intervals to avoid spam
|
|
570
|
+
if percent // 25 == self._last_extract_percent // 25 and percent != 100:
|
|
571
|
+
return
|
|
572
|
+
self._last_extract_percent = percent
|
|
573
|
+
|
|
574
|
+
archive_name = Path(archive).name[:30]
|
|
575
|
+
if self.show_progress_bar:
|
|
576
|
+
bar = format_progress_bar(extracted, total, 20)
|
|
577
|
+
log_detail(f"{bar} {percent:3d}% Extracting {archive_name} ({extracted}/{total} files)", verbose_only=self.verbose_only)
|
|
578
|
+
else:
|
|
579
|
+
log_detail(f"Extracting {archive_name}: {percent}% ({extracted}/{total} files)", verbose_only=self.verbose_only)
|
|
580
|
+
|
|
581
|
+
def on_phase_start(self, phase: int, total: int, message: str) -> None:
|
|
582
|
+
"""Log phase start using log_phase."""
|
|
583
|
+
log_phase(phase, total, f"{message}...", verbose_only=self.verbose_only)
|
|
584
|
+
|
|
585
|
+
def on_phase_complete(self, phase: int, total: int, elapsed: float) -> None:
|
|
586
|
+
"""Log phase completion with elapsed time."""
|
|
587
|
+
del phase, total # Unused in default implementation
|
|
588
|
+
log_detail(f"Done ({elapsed:.2f}s)", verbose_only=self.verbose_only)
|
|
@@ -8,6 +8,8 @@ import subprocess
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple
|
|
10
10
|
|
|
11
|
+
from fbuild.output import ProgressCallback, log_detail
|
|
12
|
+
|
|
11
13
|
if TYPE_CHECKING:
|
|
12
14
|
from .library_manager import LibraryInfo
|
|
13
15
|
|
|
@@ -70,6 +72,7 @@ class LibraryCompiler:
|
|
|
70
72
|
defines: List[str],
|
|
71
73
|
extra_flags: List[str],
|
|
72
74
|
show_progress: bool = True,
|
|
75
|
+
progress_callback: ProgressCallback | None = None,
|
|
73
76
|
) -> Tuple[Path, List[Path], List[str]]:
|
|
74
77
|
"""Compile a library into a static archive (.a file).
|
|
75
78
|
|
|
@@ -97,7 +100,7 @@ class LibraryCompiler:
|
|
|
97
100
|
"""
|
|
98
101
|
try:
|
|
99
102
|
if show_progress:
|
|
100
|
-
|
|
103
|
+
log_detail(f"Compiling library: {library_name}")
|
|
101
104
|
|
|
102
105
|
if not source_files:
|
|
103
106
|
raise LibraryCompilationError(f"No source files found in library '{library_name}'")
|
|
@@ -155,14 +158,21 @@ class LibraryCompiler:
|
|
|
155
158
|
compile_commands.append(" ".join(cmd))
|
|
156
159
|
|
|
157
160
|
# Compile
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
file_index = source_files.index(source) + 1 # 1-based indexing
|
|
162
|
+
total_files = len(source_files)
|
|
163
|
+
if progress_callback:
|
|
164
|
+
progress_callback.on_file_start(source.name, file_index, total_files)
|
|
165
|
+
elif show_progress:
|
|
166
|
+
log_detail(f"Compiling {source.name}...")
|
|
160
167
|
|
|
161
168
|
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
|
|
162
169
|
|
|
163
170
|
if result.returncode != 0:
|
|
164
171
|
raise LibraryCompilationError(f"Failed to compile {source}:\n{result.stderr}")
|
|
165
172
|
|
|
173
|
+
if progress_callback:
|
|
174
|
+
progress_callback.on_file_complete(source.name, file_index, total_files)
|
|
175
|
+
|
|
166
176
|
object_files.append(obj_file)
|
|
167
177
|
|
|
168
178
|
# Create static archive using avr-ar
|
|
@@ -170,7 +180,7 @@ class LibraryCompiler:
|
|
|
170
180
|
archive_file = lib_dir / f"lib{library_name}.a"
|
|
171
181
|
|
|
172
182
|
if show_progress:
|
|
173
|
-
|
|
183
|
+
log_detail(f"Creating archive: {archive_file.name}")
|
|
174
184
|
|
|
175
185
|
# Remove old archive if exists
|
|
176
186
|
if archive_file.exists():
|
|
@@ -188,7 +198,7 @@ class LibraryCompiler:
|
|
|
188
198
|
# Object files are needed for proper LTO symbol resolution
|
|
189
199
|
|
|
190
200
|
if show_progress:
|
|
191
|
-
|
|
201
|
+
log_detail(f"Library '{library_name}' compiled successfully")
|
|
192
202
|
|
|
193
203
|
return archive_file, object_files, compile_commands
|
|
194
204
|
|
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Dict, List, Optional, Tuple
|
|
11
11
|
from urllib.parse import urlparse
|
|
12
12
|
|
|
13
|
+
from fbuild.output import log_detail
|
|
13
14
|
from fbuild.packages.downloader import PackageDownloader
|
|
14
15
|
from fbuild.packages.github_utils import GitHubURLOptimizer
|
|
15
16
|
from fbuild.packages.library_compiler import LibraryCompilationError, LibraryCompiler
|
|
@@ -269,7 +270,7 @@ class LibraryManager:
|
|
|
269
270
|
if GitHubURLOptimizer.is_github_url(url):
|
|
270
271
|
url = GitHubURLOptimizer.optimize_url(url)
|
|
271
272
|
if show_progress:
|
|
272
|
-
|
|
273
|
+
log_detail(f"Optimized GitHub URL: {url}")
|
|
273
274
|
|
|
274
275
|
# Extract library name
|
|
275
276
|
lib_name = self._extract_library_name(original_url)
|
|
@@ -278,7 +279,7 @@ class LibraryManager:
|
|
|
278
279
|
# Skip if already downloaded
|
|
279
280
|
if library.exists:
|
|
280
281
|
if show_progress:
|
|
281
|
-
|
|
282
|
+
log_detail(f"Library '{lib_name}' already downloaded")
|
|
282
283
|
return library
|
|
283
284
|
|
|
284
285
|
# Create library directory
|
|
@@ -289,13 +290,13 @@ class LibraryManager:
|
|
|
289
290
|
temp_archive = library.lib_dir / filename
|
|
290
291
|
|
|
291
292
|
if show_progress:
|
|
292
|
-
|
|
293
|
+
log_detail(f"Downloading library: {lib_name}")
|
|
293
294
|
|
|
294
295
|
self.downloader.download(url, temp_archive, show_progress=show_progress)
|
|
295
296
|
|
|
296
297
|
# Extract to src directory
|
|
297
298
|
if show_progress:
|
|
298
|
-
|
|
299
|
+
log_detail(f"Extracting library: {lib_name}")
|
|
299
300
|
|
|
300
301
|
temp_extract = library.lib_dir / "_extract"
|
|
301
302
|
temp_extract.mkdir(exist_ok=True)
|
|
@@ -475,8 +476,13 @@ class LibraryManager:
|
|
|
475
476
|
List of compiled Library instances
|
|
476
477
|
"""
|
|
477
478
|
libraries = []
|
|
479
|
+
total = len(lib_deps)
|
|
480
|
+
|
|
481
|
+
for index, url in enumerate(lib_deps, 1):
|
|
482
|
+
# Extract library name for logging
|
|
483
|
+
library_name = self._extract_library_name(url)
|
|
484
|
+
log_detail(f"[{index}/{total}] {library_name}")
|
|
478
485
|
|
|
479
|
-
for url in lib_deps:
|
|
480
486
|
# Download library
|
|
481
487
|
library = self.download_library(url, show_progress)
|
|
482
488
|
|
|
@@ -485,7 +491,7 @@ class LibraryManager:
|
|
|
485
491
|
|
|
486
492
|
if needs_rebuild:
|
|
487
493
|
if show_progress and reason:
|
|
488
|
-
|
|
494
|
+
log_detail(f"Rebuilding library '{library.name}': {reason}")
|
|
489
495
|
|
|
490
496
|
self.compile_library(
|
|
491
497
|
library,
|