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/__init__.py
CHANGED
|
@@ -4,7 +4,9 @@ from dataclasses import dataclass
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
from fbuild.daemon.connection import DaemonConnection, connect_daemon
|
|
8
|
+
|
|
9
|
+
__version__ = "1.2.15"
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def is_available() -> bool:
|
|
@@ -387,4 +389,6 @@ __all__ = [
|
|
|
387
389
|
"is_available",
|
|
388
390
|
"BuildContext",
|
|
389
391
|
"Daemon",
|
|
392
|
+
"DaemonConnection",
|
|
393
|
+
"connect_daemon",
|
|
390
394
|
]
|
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, List, Dict, Optional, Union, TYPE_CHECKING
|
|
15
15
|
|
|
16
16
|
from ..packages.package import IPackage, IToolchain, IFramework
|
|
17
|
+
from ..output import ProgressCallback
|
|
17
18
|
from .flag_builder import FlagBuilder
|
|
18
19
|
from .compilation_executor import CompilationExecutor
|
|
19
20
|
from .archive_creator import ArchiveCreator
|
|
@@ -335,6 +336,11 @@ class ConfigurableCompiler(ICompiler):
|
|
|
335
336
|
def compile_sketch(self, sketch_path: Path) -> List[Path]:
|
|
336
337
|
"""Compile an Arduino sketch.
|
|
337
338
|
|
|
339
|
+
This method handles Arduino sketches that may contain multiple source files:
|
|
340
|
+
- The main .ino file is preprocessed and compiled
|
|
341
|
+
- Additional .cpp files in the sketch directory are also compiled
|
|
342
|
+
- The sketch directory is added to include paths for header file resolution
|
|
343
|
+
|
|
338
344
|
Args:
|
|
339
345
|
sketch_path: Path to .ino file
|
|
340
346
|
|
|
@@ -346,6 +352,12 @@ class ConfigurableCompiler(ICompiler):
|
|
|
346
352
|
"""
|
|
347
353
|
object_files = []
|
|
348
354
|
|
|
355
|
+
# Add sketch directory to include paths so headers like ValidationConfig.h can be found
|
|
356
|
+
sketch_dir = sketch_path.parent
|
|
357
|
+
include_paths = self.get_include_paths()
|
|
358
|
+
if sketch_dir not in include_paths:
|
|
359
|
+
include_paths.insert(0, sketch_dir) # Add at front for priority
|
|
360
|
+
|
|
349
361
|
# Preprocess .ino to .cpp
|
|
350
362
|
cpp_path = self.preprocess_ino(sketch_path)
|
|
351
363
|
|
|
@@ -357,19 +369,38 @@ class ConfigurableCompiler(ICompiler):
|
|
|
357
369
|
# Skip compilation if object file is up-to-date
|
|
358
370
|
if not self.needs_rebuild(cpp_path, obj_path):
|
|
359
371
|
object_files.append(obj_path)
|
|
360
|
-
|
|
372
|
+
else:
|
|
373
|
+
# Compile preprocessed .cpp
|
|
374
|
+
compiled_obj = self.compile_source(cpp_path, obj_path)
|
|
375
|
+
object_files.append(compiled_obj)
|
|
376
|
+
|
|
377
|
+
# Find and compile additional .cpp files in the sketch directory
|
|
378
|
+
# (Arduino IDE compiles all .cpp files in the sketch folder)
|
|
379
|
+
for cpp_file in sketch_dir.glob("*.cpp"):
|
|
380
|
+
cpp_obj_path = obj_dir / f"{cpp_file.stem}.o"
|
|
381
|
+
|
|
382
|
+
# Skip compilation if object file is up-to-date
|
|
383
|
+
if not self.needs_rebuild(cpp_file, cpp_obj_path):
|
|
384
|
+
object_files.append(cpp_obj_path)
|
|
385
|
+
continue
|
|
361
386
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
387
|
+
try:
|
|
388
|
+
compiled_obj = self.compile_source(cpp_file, cpp_obj_path)
|
|
389
|
+
object_files.append(compiled_obj)
|
|
390
|
+
except ConfigurableCompilerError as e:
|
|
391
|
+
# Re-raise with more context about which file failed
|
|
392
|
+
raise ConfigurableCompilerError(
|
|
393
|
+
f"Failed to compile sketch source file {cpp_file.name}: {e}"
|
|
394
|
+
)
|
|
365
395
|
|
|
366
396
|
return object_files
|
|
367
397
|
|
|
368
|
-
def compile_core(self, progress_bar: Optional[Any] = None) -> List[Path]:
|
|
398
|
+
def compile_core(self, progress_bar: Optional[Any] = None, progress_callback: ProgressCallback | None = None) -> List[Path]:
|
|
369
399
|
"""Compile Arduino core sources.
|
|
370
400
|
|
|
371
401
|
Args:
|
|
372
402
|
progress_bar: Optional tqdm progress bar to update during compilation
|
|
403
|
+
progress_callback: Optional callback for progress notifications
|
|
373
404
|
|
|
374
405
|
Returns:
|
|
375
406
|
List of generated object file paths
|
|
@@ -394,9 +425,15 @@ class ConfigurableCompiler(ICompiler):
|
|
|
394
425
|
if progress_bar is not None:
|
|
395
426
|
self.compilation_executor.show_progress = False
|
|
396
427
|
|
|
428
|
+
total_sources = len(core_sources)
|
|
429
|
+
|
|
397
430
|
try:
|
|
398
431
|
# Compile each core source
|
|
399
|
-
for source in core_sources:
|
|
432
|
+
for idx, source in enumerate(core_sources, 1):
|
|
433
|
+
# Notify progress callback of file start
|
|
434
|
+
if progress_callback is not None:
|
|
435
|
+
progress_callback.on_file_start(source.name, idx, total_sources)
|
|
436
|
+
|
|
400
437
|
# Update progress bar BEFORE compilation for better UX
|
|
401
438
|
if progress_bar is not None:
|
|
402
439
|
progress_bar.set_description(f'Compiling {source.name[:30]}')
|
|
@@ -407,17 +444,23 @@ class ConfigurableCompiler(ICompiler):
|
|
|
407
444
|
# Skip compilation if object file is up-to-date
|
|
408
445
|
if not self.needs_rebuild(source, obj_path):
|
|
409
446
|
object_files.append(obj_path)
|
|
447
|
+
if progress_callback is not None:
|
|
448
|
+
progress_callback.on_file_complete(source.name, idx, total_sources, cached=True)
|
|
410
449
|
if progress_bar is not None:
|
|
411
450
|
progress_bar.update(1)
|
|
412
451
|
continue
|
|
413
452
|
|
|
414
453
|
compiled_obj = self.compile_source(source, obj_path)
|
|
415
454
|
object_files.append(compiled_obj)
|
|
455
|
+
if progress_callback is not None:
|
|
456
|
+
progress_callback.on_file_complete(source.name, idx, total_sources, cached=False)
|
|
416
457
|
if progress_bar is not None:
|
|
417
458
|
progress_bar.update(1)
|
|
418
459
|
except ConfigurableCompilerError as e:
|
|
419
460
|
if self.show_progress:
|
|
420
461
|
print(f"Warning: Failed to compile {source.name}: {e}")
|
|
462
|
+
if progress_callback is not None:
|
|
463
|
+
progress_callback.on_file_complete(source.name, idx, total_sources, cached=False)
|
|
421
464
|
if progress_bar is not None:
|
|
422
465
|
progress_bar.update(1)
|
|
423
466
|
finally:
|
|
@@ -18,6 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
from typing import List, Dict, Any, Optional, Union
|
|
19
19
|
|
|
20
20
|
from ..packages.package import IPackage, IToolchain, IFramework
|
|
21
|
+
from ..output import log_detail, format_size
|
|
21
22
|
from .binary_generator import BinaryGenerator
|
|
22
23
|
from .compiler import ILinker, LinkerError
|
|
23
24
|
|
|
@@ -322,11 +323,15 @@ class ConfigurableLinker(ILinker):
|
|
|
322
323
|
|
|
323
324
|
# Execute linker
|
|
324
325
|
if self.show_progress:
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
326
|
+
log_detail("Linking firmware.elf...")
|
|
327
|
+
log_detail(f"Object files: {len(object_files)}")
|
|
328
|
+
log_detail(f"Core archive: {core_archive.name}")
|
|
329
|
+
log_detail(f"SDK libraries: {len(sdk_libs)}")
|
|
330
|
+
log_detail(f"Linker scripts: {len(linker_scripts)}")
|
|
331
|
+
core_size = core_archive.stat().st_size if core_archive.exists() else 0
|
|
332
|
+
lib_size = sum(a.stat().st_size for a in library_archives if a.exists())
|
|
333
|
+
obj_size = sum(o.stat().st_size for o in object_files if o.exists())
|
|
334
|
+
log_detail(f"Inputs: core ({format_size(core_size)}) + {len(library_archives)} libs ({format_size(lib_size)}) + {len(object_files)} objects ({format_size(obj_size)})")
|
|
330
335
|
|
|
331
336
|
# Add retry logic for Windows file locking issues
|
|
332
337
|
is_windows = platform.system() == "Windows"
|
|
@@ -340,7 +345,7 @@ class ConfigurableLinker(ILinker):
|
|
|
340
345
|
gc.collect()
|
|
341
346
|
time.sleep(delay)
|
|
342
347
|
if self.show_progress:
|
|
343
|
-
|
|
348
|
+
log_detail(f"Retrying linking (attempt {attempt + 1}/{max_retries})...")
|
|
344
349
|
|
|
345
350
|
result = subprocess.run(
|
|
346
351
|
cmd,
|
|
@@ -362,7 +367,7 @@ class ConfigurableLinker(ILinker):
|
|
|
362
367
|
if is_windows and is_file_locking_error:
|
|
363
368
|
if attempt < max_retries - 1:
|
|
364
369
|
if self.show_progress:
|
|
365
|
-
|
|
370
|
+
log_detail("[Windows] Detected file locking error, retrying...")
|
|
366
371
|
delay = min(delay * 2, 1.0) # Exponential backoff, max 1s
|
|
367
372
|
continue
|
|
368
373
|
else:
|
|
@@ -386,7 +391,7 @@ class ConfigurableLinker(ILinker):
|
|
|
386
391
|
|
|
387
392
|
if self.show_progress:
|
|
388
393
|
size = output_elf.stat().st_size
|
|
389
|
-
|
|
394
|
+
log_detail(f"Created firmware.elf: {format_size(size)}")
|
|
390
395
|
|
|
391
396
|
return output_elf
|
|
392
397
|
|
|
@@ -477,7 +482,7 @@ class ConfigurableLinker(ILinker):
|
|
|
477
482
|
|
|
478
483
|
if self.show_progress:
|
|
479
484
|
size = output_hex.stat().st_size
|
|
480
|
-
|
|
485
|
+
log_detail(f"Created firmware.hex: {format_size(size)}")
|
|
481
486
|
|
|
482
487
|
return output_hex
|
|
483
488
|
|
|
@@ -25,7 +25,7 @@ from .orchestrator import IBuildOrchestrator, BuildResult
|
|
|
25
25
|
from .build_utils import safe_rmtree
|
|
26
26
|
from .build_state import BuildStateTracker
|
|
27
27
|
from .build_info_generator import BuildInfoGenerator
|
|
28
|
-
from ..output import log_phase, log_detail, log_warning
|
|
28
|
+
from ..output import log_phase, log_detail, log_warning, DefaultProgressCallback
|
|
29
29
|
|
|
30
30
|
# Module-level logger
|
|
31
31
|
logger = logging.getLogger(__name__)
|
|
@@ -299,9 +299,12 @@ class OrchestratorESP32(IBuildOrchestrator):
|
|
|
299
299
|
compilation_queue=compilation_queue
|
|
300
300
|
)
|
|
301
301
|
|
|
302
|
+
# Create progress callback for detailed file-by-file tracking
|
|
303
|
+
progress_callback = DefaultProgressCallback(verbose_only=not verbose)
|
|
304
|
+
|
|
302
305
|
# Compile Arduino core with progress bar
|
|
303
306
|
if verbose:
|
|
304
|
-
core_obj_files = compiler.compile_core()
|
|
307
|
+
core_obj_files = compiler.compile_core(progress_callback=progress_callback)
|
|
305
308
|
else:
|
|
306
309
|
# Use tqdm progress bar for non-verbose mode
|
|
307
310
|
from tqdm import tqdm
|
|
@@ -318,7 +321,7 @@ class OrchestratorESP32(IBuildOrchestrator):
|
|
|
318
321
|
ncols=80,
|
|
319
322
|
leave=False
|
|
320
323
|
) as pbar:
|
|
321
|
-
core_obj_files = compiler.compile_core(progress_bar=pbar)
|
|
324
|
+
core_obj_files = compiler.compile_core(progress_bar=pbar, progress_callback=progress_callback)
|
|
322
325
|
|
|
323
326
|
# Print completion message
|
|
324
327
|
log_detail(f"Compiled {len(core_obj_files)} core files")
|
|
@@ -19,6 +19,7 @@ from ..packages.toolchain_rp2040 import ToolchainRP2040
|
|
|
19
19
|
from ..packages.library_manager import LibraryManager, LibraryError
|
|
20
20
|
from ..config.board_config import BoardConfig
|
|
21
21
|
from ..cli_utils import BannerFormatter
|
|
22
|
+
from ..output import DefaultProgressCallback
|
|
22
23
|
from .configurable_compiler import ConfigurableCompiler
|
|
23
24
|
from .configurable_linker import ConfigurableLinker
|
|
24
25
|
from .linker import SizeInfo
|
|
@@ -276,9 +277,12 @@ class OrchestratorRP2040(IBuildOrchestrator):
|
|
|
276
277
|
user_build_flags=build_flags
|
|
277
278
|
)
|
|
278
279
|
|
|
280
|
+
# Create progress callback for detailed file-by-file tracking
|
|
281
|
+
progress_callback = DefaultProgressCallback(verbose_only=not verbose)
|
|
282
|
+
|
|
279
283
|
# Compile Arduino core with progress bar
|
|
280
284
|
if verbose:
|
|
281
|
-
core_obj_files = compiler.compile_core()
|
|
285
|
+
core_obj_files = compiler.compile_core(progress_callback=progress_callback)
|
|
282
286
|
else:
|
|
283
287
|
# Use tqdm progress bar for non-verbose mode
|
|
284
288
|
from tqdm import tqdm
|
|
@@ -295,7 +299,7 @@ class OrchestratorRP2040(IBuildOrchestrator):
|
|
|
295
299
|
ncols=80,
|
|
296
300
|
leave=False
|
|
297
301
|
) as pbar:
|
|
298
|
-
core_obj_files = compiler.compile_core(progress_bar=pbar)
|
|
302
|
+
core_obj_files = compiler.compile_core(progress_bar=pbar, progress_callback=progress_callback)
|
|
299
303
|
|
|
300
304
|
# Print completion message
|
|
301
305
|
logger.info(f"Compiled {len(core_obj_files)} core files")
|
fbuild/cli.py
CHANGED
|
@@ -272,7 +272,194 @@ def monitor_command(args: MonitorArgs) -> None:
|
|
|
272
272
|
ErrorFormatter.handle_unexpected_error(e, args.verbose)
|
|
273
273
|
|
|
274
274
|
|
|
275
|
-
def
|
|
275
|
+
def device_command(
|
|
276
|
+
action: str,
|
|
277
|
+
device_id: Optional[str] = None,
|
|
278
|
+
lease_type: str = "exclusive",
|
|
279
|
+
description: str = "",
|
|
280
|
+
reason: str = "",
|
|
281
|
+
refresh: bool = False,
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Manage devices connected to the daemon.
|
|
284
|
+
|
|
285
|
+
Examples:
|
|
286
|
+
fbuild device list # List all connected devices
|
|
287
|
+
fbuild device list --refresh # Refresh device discovery before listing
|
|
288
|
+
fbuild device status <device_id> # Show detailed device status
|
|
289
|
+
fbuild device lease <device_id> # Acquire exclusive lease on device
|
|
290
|
+
fbuild device lease <device_id> --monitor # Acquire monitor (read-only) lease
|
|
291
|
+
fbuild device release <device_id> # Release lease on device
|
|
292
|
+
fbuild device take <device_id> --reason "Urgent deployment" # Preempt current holder
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
if action == "list":
|
|
296
|
+
# List all devices
|
|
297
|
+
devices = daemon_client.list_devices(refresh=refresh)
|
|
298
|
+
if devices is None:
|
|
299
|
+
ErrorFormatter.print_error("Failed to list devices", "Daemon may not be running")
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
|
|
302
|
+
if not devices:
|
|
303
|
+
print("No devices found")
|
|
304
|
+
sys.exit(0)
|
|
305
|
+
|
|
306
|
+
print(f"Found {len(devices)} device(s):\n")
|
|
307
|
+
for device in devices:
|
|
308
|
+
device_id = device.get("device_id", "unknown")
|
|
309
|
+
port = device.get("port", "unknown")
|
|
310
|
+
connected = "✅ connected" if device.get("is_connected", False) else "❌ disconnected"
|
|
311
|
+
exclusive = device.get("exclusive_holder")
|
|
312
|
+
monitor_count = device.get("monitor_count", 0)
|
|
313
|
+
|
|
314
|
+
print(f" {device_id}")
|
|
315
|
+
print(f" Port: {port}")
|
|
316
|
+
print(f" Status: {connected}")
|
|
317
|
+
if exclusive:
|
|
318
|
+
print(f" Exclusive holder: {exclusive}")
|
|
319
|
+
if monitor_count > 0:
|
|
320
|
+
print(f" Monitor sessions: {monitor_count}")
|
|
321
|
+
print()
|
|
322
|
+
|
|
323
|
+
sys.exit(0)
|
|
324
|
+
|
|
325
|
+
elif action == "status":
|
|
326
|
+
if not device_id:
|
|
327
|
+
ErrorFormatter.print_error("Device ID required", "Usage: fbuild device status <device_id>")
|
|
328
|
+
sys.exit(1)
|
|
329
|
+
|
|
330
|
+
status = daemon_client.get_device_status(device_id)
|
|
331
|
+
if status is None:
|
|
332
|
+
ErrorFormatter.print_error(f"Device not found: {device_id}", "")
|
|
333
|
+
sys.exit(1)
|
|
334
|
+
|
|
335
|
+
print(f"Device: {device_id}")
|
|
336
|
+
print(f" Connected: {'✅ Yes' if status.get('is_connected') else '❌ No'}")
|
|
337
|
+
print(f" Port: {status.get('device_info', {}).get('port', 'unknown')}")
|
|
338
|
+
print(f" Available for exclusive: {'✅ Yes' if status.get('is_available_for_exclusive') else '❌ No'}")
|
|
339
|
+
|
|
340
|
+
if status.get("exclusive_lease"):
|
|
341
|
+
lease = status["exclusive_lease"]
|
|
342
|
+
print(f" Exclusive holder: {lease.get('client_id', 'unknown')}")
|
|
343
|
+
print(f" Description: {lease.get('description', 'N/A')}")
|
|
344
|
+
|
|
345
|
+
if status.get("monitor_count", 0) > 0:
|
|
346
|
+
print(f" Monitor sessions: {status['monitor_count']}")
|
|
347
|
+
for monitor in status.get("monitor_leases", []):
|
|
348
|
+
print(f" - {monitor.get('client_id', 'unknown')}")
|
|
349
|
+
|
|
350
|
+
sys.exit(0)
|
|
351
|
+
|
|
352
|
+
elif action == "lease":
|
|
353
|
+
if not device_id:
|
|
354
|
+
ErrorFormatter.print_error("Device ID required", "Usage: fbuild device lease <device_id>")
|
|
355
|
+
sys.exit(1)
|
|
356
|
+
|
|
357
|
+
result = daemon_client.acquire_device_lease(
|
|
358
|
+
device_id=device_id,
|
|
359
|
+
lease_type=lease_type,
|
|
360
|
+
description=description,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if result is None:
|
|
364
|
+
ErrorFormatter.print_error("Failed to acquire lease", "Daemon may not be running")
|
|
365
|
+
sys.exit(1)
|
|
366
|
+
|
|
367
|
+
if result.get("success"):
|
|
368
|
+
lease_id = result.get("lease_id", "unknown")
|
|
369
|
+
print(f"✅ Acquired {lease_type} lease on device {device_id}")
|
|
370
|
+
print(f" Lease ID: {lease_id}")
|
|
371
|
+
sys.exit(0)
|
|
372
|
+
else:
|
|
373
|
+
ErrorFormatter.print_error(f"Failed to acquire lease: {result.get('message', 'unknown error')}", "")
|
|
374
|
+
sys.exit(1)
|
|
375
|
+
|
|
376
|
+
elif action == "release":
|
|
377
|
+
if not device_id:
|
|
378
|
+
ErrorFormatter.print_error("Device ID or lease ID required", "Usage: fbuild device release <device_id>")
|
|
379
|
+
sys.exit(1)
|
|
380
|
+
|
|
381
|
+
result = daemon_client.release_device_lease(device_id)
|
|
382
|
+
|
|
383
|
+
if result is None:
|
|
384
|
+
ErrorFormatter.print_error("Failed to release lease", "Daemon may not be running")
|
|
385
|
+
sys.exit(1)
|
|
386
|
+
|
|
387
|
+
if result.get("success"):
|
|
388
|
+
print(f"✅ Released lease on device {device_id}")
|
|
389
|
+
sys.exit(0)
|
|
390
|
+
else:
|
|
391
|
+
ErrorFormatter.print_error(f"Failed to release lease: {result.get('message', 'unknown error')}", "")
|
|
392
|
+
sys.exit(1)
|
|
393
|
+
|
|
394
|
+
elif action == "take":
|
|
395
|
+
if not device_id:
|
|
396
|
+
ErrorFormatter.print_error("Device ID required", 'Usage: fbuild device take <device_id> --reason "..."')
|
|
397
|
+
sys.exit(1)
|
|
398
|
+
|
|
399
|
+
if not reason:
|
|
400
|
+
ErrorFormatter.print_error("Reason required for preemption", 'Usage: fbuild device take <device_id> --reason "..."')
|
|
401
|
+
sys.exit(1)
|
|
402
|
+
|
|
403
|
+
result = daemon_client.preempt_device(device_id, reason)
|
|
404
|
+
|
|
405
|
+
if result is None:
|
|
406
|
+
ErrorFormatter.print_error("Failed to preempt device", "Daemon may not be running")
|
|
407
|
+
sys.exit(1)
|
|
408
|
+
|
|
409
|
+
if result.get("success"):
|
|
410
|
+
preempted = result.get("preempted_client_id")
|
|
411
|
+
print(f"✅ Preempted device {device_id}")
|
|
412
|
+
if preempted:
|
|
413
|
+
print(f" Previous holder: {preempted}")
|
|
414
|
+
print(f" Lease ID: {result.get('lease_id', 'unknown')}")
|
|
415
|
+
sys.exit(0)
|
|
416
|
+
else:
|
|
417
|
+
ErrorFormatter.print_error(f"Failed to preempt device: {result.get('message', 'unknown error')}", "")
|
|
418
|
+
sys.exit(1)
|
|
419
|
+
|
|
420
|
+
else:
|
|
421
|
+
ErrorFormatter.print_error(f"Unknown device action: {action}", "")
|
|
422
|
+
print("Valid actions: list, status, lease, release, take")
|
|
423
|
+
sys.exit(1)
|
|
424
|
+
|
|
425
|
+
except KeyboardInterrupt as ke:
|
|
426
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
427
|
+
|
|
428
|
+
handle_keyboard_interrupt_properly(ke)
|
|
429
|
+
except Exception as e:
|
|
430
|
+
ErrorFormatter.handle_unexpected_error(e, verbose=False)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def show_command(target: str, follow: bool = True, lines: int = 50) -> None:
|
|
434
|
+
"""Show daemon logs or other information.
|
|
435
|
+
|
|
436
|
+
Examples:
|
|
437
|
+
fbuild show daemon # Tail daemon logs (Ctrl-C to stop, daemon continues)
|
|
438
|
+
fbuild show daemon --no-follow # Show last 50 lines and exit
|
|
439
|
+
fbuild show daemon --lines 100 # Show last 100 lines then follow
|
|
440
|
+
"""
|
|
441
|
+
try:
|
|
442
|
+
if target == "daemon":
|
|
443
|
+
daemon_client.tail_daemon_logs(follow=follow, lines=lines)
|
|
444
|
+
sys.exit(0)
|
|
445
|
+
else:
|
|
446
|
+
from fbuild.cli_utils import ErrorFormatter
|
|
447
|
+
|
|
448
|
+
ErrorFormatter.print_error(f"Unknown target: {target}", "")
|
|
449
|
+
print("Valid targets: daemon")
|
|
450
|
+
sys.exit(1)
|
|
451
|
+
|
|
452
|
+
except KeyboardInterrupt as ke:
|
|
453
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
454
|
+
|
|
455
|
+
handle_keyboard_interrupt_properly(ke)
|
|
456
|
+
except Exception as e:
|
|
457
|
+
from fbuild.cli_utils import ErrorFormatter
|
|
458
|
+
|
|
459
|
+
ErrorFormatter.handle_unexpected_error(e, verbose=False)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def daemon_command(action: str, pid: Optional[int] = None, force: bool = False, follow: bool = True, lines: int = 50) -> None:
|
|
276
463
|
"""Manage the fbuild daemon.
|
|
277
464
|
|
|
278
465
|
Examples:
|
|
@@ -282,6 +469,7 @@ def daemon_command(action: str, pid: Optional[int] = None, force: bool = False)
|
|
|
282
469
|
fbuild daemon list # List all daemon instances
|
|
283
470
|
fbuild daemon locks # Show lock status
|
|
284
471
|
fbuild daemon clear-locks # Clear stale locks
|
|
472
|
+
fbuild daemon monitor # Tail daemon logs (alias for 'fbuild show daemon')
|
|
285
473
|
fbuild daemon kill --pid 12345 # Kill specific daemon
|
|
286
474
|
fbuild daemon kill-all # Kill all daemons
|
|
287
475
|
fbuild daemon kill-all --force # Force kill all daemons
|
|
@@ -374,9 +562,14 @@ def daemon_command(action: str, pid: Optional[int] = None, force: bool = False)
|
|
|
374
562
|
print("No daemon instances found to kill")
|
|
375
563
|
sys.exit(0)
|
|
376
564
|
|
|
565
|
+
elif action == "monitor":
|
|
566
|
+
# Monitor daemon logs (tail the log file)
|
|
567
|
+
daemon_client.tail_daemon_logs(follow=follow, lines=lines)
|
|
568
|
+
sys.exit(0)
|
|
569
|
+
|
|
377
570
|
else:
|
|
378
571
|
ErrorFormatter.print_error(f"Unknown daemon action: {action}", "")
|
|
379
|
-
print("Valid actions: status, stop, restart, list, locks, clear-locks, kill, kill-all")
|
|
572
|
+
print("Valid actions: status, stop, restart, list, locks, clear-locks, monitor, kill, kill-all")
|
|
380
573
|
sys.exit(1)
|
|
381
574
|
|
|
382
575
|
except KeyboardInterrupt as ke:
|
|
@@ -470,9 +663,16 @@ def main() -> None:
|
|
|
470
663
|
|
|
471
664
|
Replace PlatformIO with URL-based platform/toolchain management.
|
|
472
665
|
"""
|
|
666
|
+
# Display daemon stats as the first action (unless --version or --help anywhere)
|
|
667
|
+
help_flags = {"--version", "-V", "--help", "-h"}
|
|
668
|
+
skip_stats = any(arg in help_flags for arg in sys.argv)
|
|
669
|
+
if len(sys.argv) >= 2 and not skip_stats:
|
|
670
|
+
daemon_client.display_daemon_stats_compact()
|
|
671
|
+
print() # Blank line after stats
|
|
672
|
+
|
|
473
673
|
# Handle default action: fbuild <project_dir> [flags] → deploy with monitor
|
|
474
674
|
# This check must happen before argparse to avoid conflicts
|
|
475
|
-
if len(sys.argv) >= 2 and not sys.argv[1].startswith("-") and sys.argv[1] not in ["build", "deploy", "monitor", "daemon"]:
|
|
675
|
+
if len(sys.argv) >= 2 and not sys.argv[1].startswith("-") and sys.argv[1] not in ["build", "deploy", "monitor", "daemon", "device", "show"]:
|
|
476
676
|
# User provided a path without a subcommand - use default action
|
|
477
677
|
deploy_args = parse_default_action_args(sys.argv)
|
|
478
678
|
deploy_command(deploy_args)
|
|
@@ -640,6 +840,29 @@ def main() -> None:
|
|
|
640
840
|
help="Show verbose output",
|
|
641
841
|
)
|
|
642
842
|
|
|
843
|
+
# Show command
|
|
844
|
+
show_parser = subparsers.add_parser(
|
|
845
|
+
"show",
|
|
846
|
+
help="Show daemon logs or other information",
|
|
847
|
+
)
|
|
848
|
+
show_parser.add_argument(
|
|
849
|
+
"target",
|
|
850
|
+
choices=["daemon"],
|
|
851
|
+
help="What to show (currently only 'daemon' for daemon logs)",
|
|
852
|
+
)
|
|
853
|
+
show_parser.add_argument(
|
|
854
|
+
"--no-follow",
|
|
855
|
+
action="store_true",
|
|
856
|
+
dest="no_follow",
|
|
857
|
+
help="Don't follow the log file (just print last lines and exit)",
|
|
858
|
+
)
|
|
859
|
+
show_parser.add_argument(
|
|
860
|
+
"--lines",
|
|
861
|
+
type=int,
|
|
862
|
+
default=50,
|
|
863
|
+
help="Number of lines to show initially (default: 50)",
|
|
864
|
+
)
|
|
865
|
+
|
|
643
866
|
# Daemon command
|
|
644
867
|
daemon_parser = subparsers.add_parser(
|
|
645
868
|
"daemon",
|
|
@@ -647,7 +870,7 @@ def main() -> None:
|
|
|
647
870
|
)
|
|
648
871
|
daemon_parser.add_argument(
|
|
649
872
|
"action",
|
|
650
|
-
choices=["status", "stop", "restart", "list", "locks", "clear-locks", "kill", "kill-all"],
|
|
873
|
+
choices=["status", "stop", "restart", "list", "locks", "clear-locks", "monitor", "kill", "kill-all"],
|
|
651
874
|
help="Daemon action to perform",
|
|
652
875
|
)
|
|
653
876
|
daemon_parser.add_argument(
|
|
@@ -661,6 +884,56 @@ def main() -> None:
|
|
|
661
884
|
action="store_true",
|
|
662
885
|
help="Force kill without graceful shutdown (for 'kill' and 'kill-all' actions)",
|
|
663
886
|
)
|
|
887
|
+
daemon_parser.add_argument(
|
|
888
|
+
"--no-follow",
|
|
889
|
+
action="store_true",
|
|
890
|
+
dest="no_follow",
|
|
891
|
+
help="Don't follow the log file, just print last lines and exit (for 'monitor' action)",
|
|
892
|
+
)
|
|
893
|
+
daemon_parser.add_argument(
|
|
894
|
+
"--lines",
|
|
895
|
+
type=int,
|
|
896
|
+
default=50,
|
|
897
|
+
help="Number of lines to show initially (for 'monitor' action, default: 50)",
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
# Device command
|
|
901
|
+
device_parser = subparsers.add_parser(
|
|
902
|
+
"device",
|
|
903
|
+
help="Manage devices connected to the daemon",
|
|
904
|
+
)
|
|
905
|
+
device_parser.add_argument(
|
|
906
|
+
"action",
|
|
907
|
+
choices=["list", "status", "lease", "release", "take"],
|
|
908
|
+
help="Device action to perform",
|
|
909
|
+
)
|
|
910
|
+
device_parser.add_argument(
|
|
911
|
+
"device_id",
|
|
912
|
+
nargs="?",
|
|
913
|
+
default=None,
|
|
914
|
+
help="Device ID (required for status, lease, release, take)",
|
|
915
|
+
)
|
|
916
|
+
device_parser.add_argument(
|
|
917
|
+
"--monitor",
|
|
918
|
+
action="store_true",
|
|
919
|
+
dest="lease_monitor",
|
|
920
|
+
help="Acquire monitor (read-only) lease instead of exclusive (for 'lease' action)",
|
|
921
|
+
)
|
|
922
|
+
device_parser.add_argument(
|
|
923
|
+
"--description",
|
|
924
|
+
default="",
|
|
925
|
+
help="Description for lease (for 'lease' action)",
|
|
926
|
+
)
|
|
927
|
+
device_parser.add_argument(
|
|
928
|
+
"--reason",
|
|
929
|
+
default="",
|
|
930
|
+
help="Reason for preemption (required for 'take' action)",
|
|
931
|
+
)
|
|
932
|
+
device_parser.add_argument(
|
|
933
|
+
"--refresh",
|
|
934
|
+
action="store_true",
|
|
935
|
+
help="Refresh device discovery before listing (for 'list' action)",
|
|
936
|
+
)
|
|
664
937
|
|
|
665
938
|
# Parse arguments
|
|
666
939
|
parsed_args = parser.parse_args()
|
|
@@ -710,7 +983,29 @@ def main() -> None:
|
|
|
710
983
|
)
|
|
711
984
|
monitor_command(monitor_args)
|
|
712
985
|
elif parsed_args.command == "daemon":
|
|
713
|
-
daemon_command(
|
|
986
|
+
daemon_command(
|
|
987
|
+
parsed_args.action,
|
|
988
|
+
pid=parsed_args.pid,
|
|
989
|
+
force=parsed_args.force,
|
|
990
|
+
follow=not parsed_args.no_follow,
|
|
991
|
+
lines=parsed_args.lines,
|
|
992
|
+
)
|
|
993
|
+
elif parsed_args.command == "show":
|
|
994
|
+
show_command(
|
|
995
|
+
target=parsed_args.target,
|
|
996
|
+
follow=not parsed_args.no_follow,
|
|
997
|
+
lines=parsed_args.lines,
|
|
998
|
+
)
|
|
999
|
+
elif parsed_args.command == "device":
|
|
1000
|
+
lease_type = "monitor" if parsed_args.lease_monitor else "exclusive"
|
|
1001
|
+
device_command(
|
|
1002
|
+
action=parsed_args.action,
|
|
1003
|
+
device_id=parsed_args.device_id,
|
|
1004
|
+
lease_type=lease_type,
|
|
1005
|
+
description=parsed_args.description,
|
|
1006
|
+
reason=parsed_args.reason,
|
|
1007
|
+
refresh=parsed_args.refresh,
|
|
1008
|
+
)
|
|
714
1009
|
|
|
715
1010
|
|
|
716
1011
|
if __name__ == "__main__":
|
fbuild/config/ini_parser.py
CHANGED
|
@@ -6,6 +6,7 @@ environment configurations for building embedded projects.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import configparser
|
|
9
|
+
import os
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Dict, List, Optional
|
|
11
12
|
|
|
@@ -301,14 +302,25 @@ class PlatformIOConfig:
|
|
|
301
302
|
|
|
302
303
|
def get_src_dir(self) -> Optional[str]:
|
|
303
304
|
"""
|
|
304
|
-
Get source directory override
|
|
305
|
+
Get source directory override.
|
|
306
|
+
|
|
307
|
+
Checks in order:
|
|
308
|
+
1. PLATFORMIO_SRC_DIR environment variable (matches PlatformIO behavior)
|
|
309
|
+
2. src_dir in [platformio] section of platformio.ini
|
|
305
310
|
|
|
306
311
|
Returns:
|
|
307
312
|
Source directory path relative to project root, or None if not specified
|
|
308
313
|
|
|
309
314
|
Example:
|
|
315
|
+
If PLATFORMIO_SRC_DIR=examples/Validation, returns 'examples/Validation'
|
|
310
316
|
If [platformio] section has src_dir = examples/Blink, returns 'examples/Blink'
|
|
311
317
|
"""
|
|
318
|
+
# First check environment variable (PlatformIO standard behavior)
|
|
319
|
+
env_src_dir = os.environ.get("PLATFORMIO_SRC_DIR", "").strip()
|
|
320
|
+
if env_src_dir:
|
|
321
|
+
return env_src_dir
|
|
322
|
+
|
|
323
|
+
# Fall back to platformio.ini setting
|
|
312
324
|
if "platformio" in self.config:
|
|
313
325
|
src_dir = self.config["platformio"].get("src_dir", "").strip()
|
|
314
326
|
# Remove inline comments (everything after ';')
|