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
fbuild/cli.py ADDED
@@ -0,0 +1,717 @@
1
+ """
2
+ Command-line interface for fbuild.
3
+
4
+ This module provides the `fbuild` CLI tool for building embedded firmware.
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from fbuild import __version__
14
+ from fbuild.cli_utils import (
15
+ EnvironmentDetector,
16
+ ErrorFormatter,
17
+ MonitorFlagParser,
18
+ PathValidator,
19
+ )
20
+ from fbuild.daemon import client as daemon_client
21
+ from fbuild.output import init_timer, log, log_header, set_verbose
22
+
23
+
24
+ @dataclass
25
+ class BuildArgs:
26
+ """Arguments for the build command."""
27
+
28
+ project_dir: Path
29
+ environment: Optional[str] = None
30
+ clean: bool = False
31
+ verbose: bool = False
32
+
33
+
34
+ @dataclass
35
+ class DeployArgs:
36
+ """Arguments for the deploy command."""
37
+
38
+ project_dir: Path
39
+ environment: Optional[str] = None
40
+ port: Optional[str] = None
41
+ clean: bool = False
42
+ monitor: Optional[str] = None
43
+ verbose: bool = False
44
+ qemu: bool = False
45
+ qemu_timeout: int = 30
46
+
47
+
48
+ @dataclass
49
+ class MonitorArgs:
50
+ """Arguments for the monitor command."""
51
+
52
+ project_dir: Path
53
+ environment: Optional[str] = None
54
+ port: Optional[str] = None
55
+ baud: int = 115200
56
+ timeout: Optional[int] = None
57
+ halt_on_error: Optional[str] = None
58
+ halt_on_success: Optional[str] = None
59
+ expect: Optional[str] = None
60
+ verbose: bool = False
61
+ timestamp: bool = True
62
+
63
+
64
+ def build_command(args: BuildArgs) -> None:
65
+ """Build firmware for embedded target.
66
+
67
+ Examples:
68
+ fbuild build # Build default environment
69
+ fbuild build tests/uno # Build specific project
70
+ fbuild build -e uno # Build 'uno' environment
71
+ fbuild build --clean # Clean build
72
+ fbuild build --verbose # Verbose output
73
+ """
74
+ # Initialize timer and verbose mode
75
+ init_timer()
76
+ set_verbose(args.verbose)
77
+
78
+ # Print header
79
+ log_header("fbuild Build System", __version__)
80
+
81
+ try:
82
+ # Determine environment name
83
+ env_name = EnvironmentDetector.detect_environment(args.project_dir, args.environment)
84
+
85
+ # Show build start message
86
+ if args.verbose:
87
+ log(f"Building project: {args.project_dir}")
88
+ log(f"Environment: {env_name}")
89
+ log("")
90
+ else:
91
+ log(f"Building environment: {env_name}...")
92
+
93
+ # Route build through daemon for background processing
94
+ success = daemon_client.request_build(
95
+ project_dir=args.project_dir,
96
+ environment=env_name,
97
+ clean_build=args.clean,
98
+ verbose=args.verbose,
99
+ )
100
+
101
+ # Exit with appropriate code
102
+ sys.exit(0 if success else 1)
103
+
104
+ except FileNotFoundError as e:
105
+ ErrorFormatter.handle_file_not_found(e)
106
+ except PermissionError as e:
107
+ ErrorFormatter.handle_permission_error(e)
108
+ except KeyboardInterrupt as ke:
109
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
110
+
111
+ handle_keyboard_interrupt_properly(ke)
112
+ except Exception as e:
113
+ ErrorFormatter.handle_unexpected_error(e, args.verbose)
114
+
115
+
116
+ def deploy_command(args: DeployArgs) -> None:
117
+ """Deploy firmware to embedded target.
118
+
119
+ Examples:
120
+ fbuild deploy # Deploy default environment
121
+ fbuild deploy tests/esp32c6 # Deploy specific project
122
+ fbuild deploy -e esp32c6 # Deploy 'esp32c6' environment
123
+ fbuild deploy -p COM3 # Deploy to specific port
124
+ fbuild deploy --clean # Clean build before deploy
125
+ fbuild deploy --monitor="--timeout 60 --halt-on-success \"TEST PASSED\"" # Deploy and monitor
126
+ fbuild deploy --qemu # Deploy to QEMU emulator (requires Docker)
127
+ fbuild deploy --qemu --qemu-timeout 60 # Deploy to QEMU with 60s timeout
128
+ """
129
+ # Initialize timer and verbose mode
130
+ init_timer()
131
+ set_verbose(args.verbose)
132
+
133
+ log_header("fbuild Deployment System", __version__)
134
+
135
+ try:
136
+ # Determine environment name
137
+ env_name = EnvironmentDetector.detect_environment(args.project_dir, args.environment)
138
+
139
+ # Handle QEMU deployment
140
+ if args.qemu:
141
+ log("Deploying to QEMU emulator...")
142
+ from fbuild.config import PlatformIOConfig
143
+ from fbuild.deploy.qemu_runner import (
144
+ QEMURunner,
145
+ ensure_docker_available,
146
+ map_board_to_machine,
147
+ )
148
+
149
+ # Ensure Docker is available (attempts auto-start if not running)
150
+ if not ensure_docker_available():
151
+ ErrorFormatter.print_error("Docker is not available", "QEMU deployment requires Docker to be installed and running")
152
+ print("\nInstall Docker:")
153
+ print(" - Windows/Mac: https://www.docker.com/products/docker-desktop")
154
+ print(" - Linux: https://docs.docker.com/engine/install/")
155
+ sys.exit(1)
156
+
157
+ # Load config to get board type
158
+ ini_path = args.project_dir / "platformio.ini"
159
+ if not ini_path.exists():
160
+ ErrorFormatter.print_error("platformio.ini not found", str(ini_path))
161
+ sys.exit(1)
162
+
163
+ config = PlatformIOConfig(ini_path)
164
+ env_config = config.get_env_config(env_name)
165
+ board_id = env_config.get("board", "esp32dev")
166
+
167
+ # Map board to QEMU machine type
168
+ machine = map_board_to_machine(board_id)
169
+
170
+ # Find firmware
171
+ build_dir = args.project_dir / ".fbuild" / "build" / env_name
172
+ firmware_bin = build_dir / "firmware.bin"
173
+
174
+ if not firmware_bin.exists():
175
+ ErrorFormatter.print_error("Firmware not found", f"Run 'fbuild build' first. Expected: {firmware_bin}")
176
+ sys.exit(1)
177
+
178
+ # Run QEMU
179
+ runner = QEMURunner(verbose=args.verbose)
180
+ exit_code = runner.run(
181
+ firmware_path=firmware_bin,
182
+ machine=machine,
183
+ timeout=args.qemu_timeout,
184
+ )
185
+
186
+ sys.exit(exit_code)
187
+
188
+ # Parse monitor flags if provided
189
+ monitor_after = args.monitor is not None
190
+ monitor_timeout = None
191
+ monitor_halt_on_error = None
192
+ monitor_halt_on_success = None
193
+ monitor_expect = None
194
+ monitor_show_timestamp = False
195
+ if monitor_after and args.monitor is not None:
196
+ flags = MonitorFlagParser.parse_monitor_flags(args.monitor)
197
+ monitor_timeout = flags.timeout
198
+ monitor_halt_on_error = flags.halt_on_error
199
+ monitor_halt_on_success = flags.halt_on_success
200
+ monitor_expect = flags.expect
201
+ monitor_show_timestamp = flags.timestamp
202
+
203
+ # Use daemon for concurrent deploy management
204
+ success = daemon_client.request_deploy(
205
+ project_dir=args.project_dir,
206
+ environment=env_name,
207
+ port=args.port,
208
+ clean_build=args.clean,
209
+ monitor_after=monitor_after,
210
+ monitor_timeout=monitor_timeout,
211
+ monitor_halt_on_error=monitor_halt_on_error,
212
+ monitor_halt_on_success=monitor_halt_on_success,
213
+ monitor_expect=monitor_expect,
214
+ monitor_show_timestamp=monitor_show_timestamp,
215
+ timeout=1800, # 30 minute timeout for deploy
216
+ )
217
+
218
+ if success:
219
+ sys.exit(0)
220
+ else:
221
+ sys.exit(1)
222
+
223
+ except FileNotFoundError as e:
224
+ ErrorFormatter.handle_file_not_found(e)
225
+ except KeyboardInterrupt as ke:
226
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
227
+
228
+ handle_keyboard_interrupt_properly(ke)
229
+ except Exception as e:
230
+ ErrorFormatter.handle_unexpected_error(e, args.verbose)
231
+
232
+
233
+ def monitor_command(args: MonitorArgs) -> None:
234
+ """Monitor serial output from embedded target.
235
+
236
+ Examples:
237
+ fbuild monitor # Monitor default environment
238
+ fbuild monitor -p COM3 # Monitor specific port
239
+ fbuild monitor --timeout 60 # Monitor with 60s timeout
240
+ fbuild monitor --halt-on-error "ERROR" # Exit on error
241
+ fbuild monitor --halt-on-success "TEST PASSED" # Exit on success
242
+ """
243
+ try:
244
+ # Determine environment name
245
+ env_name = EnvironmentDetector.detect_environment(args.project_dir, args.environment)
246
+
247
+ # Use daemon for concurrent monitor management
248
+ success = daemon_client.request_monitor(
249
+ project_dir=args.project_dir,
250
+ environment=env_name,
251
+ port=args.port,
252
+ baud_rate=args.baud,
253
+ halt_on_error=args.halt_on_error,
254
+ halt_on_success=args.halt_on_success,
255
+ expect=args.expect,
256
+ timeout=args.timeout,
257
+ show_timestamp=args.timestamp,
258
+ )
259
+
260
+ if success:
261
+ sys.exit(0)
262
+ else:
263
+ sys.exit(1)
264
+
265
+ except FileNotFoundError as e:
266
+ ErrorFormatter.handle_file_not_found(e)
267
+ except KeyboardInterrupt as ke:
268
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
269
+
270
+ handle_keyboard_interrupt_properly(ke)
271
+ except Exception as e:
272
+ ErrorFormatter.handle_unexpected_error(e, args.verbose)
273
+
274
+
275
+ def daemon_command(action: str, pid: Optional[int] = None, force: bool = False) -> None:
276
+ """Manage the fbuild daemon.
277
+
278
+ Examples:
279
+ fbuild daemon status # Show daemon status
280
+ fbuild daemon stop # Stop the daemon
281
+ fbuild daemon restart # Restart the daemon
282
+ fbuild daemon list # List all daemon instances
283
+ fbuild daemon locks # Show lock status
284
+ fbuild daemon clear-locks # Clear stale locks
285
+ fbuild daemon kill --pid 12345 # Kill specific daemon
286
+ fbuild daemon kill-all # Kill all daemons
287
+ fbuild daemon kill-all --force # Force kill all daemons
288
+ """
289
+ try:
290
+ if action == "status":
291
+ # Get daemon status
292
+ status = daemon_client.get_daemon_status()
293
+
294
+ if status["running"]:
295
+ print("✅ Daemon is running")
296
+ print(f" PID: {status.get('pid', 'unknown')}")
297
+
298
+ if "current_status" in status:
299
+ current = status["current_status"]
300
+ print(f" State: {current.get('state', 'unknown')}")
301
+ print(f" Message: {current.get('message', 'N/A')}")
302
+
303
+ if current.get("operation_in_progress"):
304
+ print(" 🔄 Operation in progress:")
305
+ print(f" Environment: {current.get('environment', 'N/A')}")
306
+ print(f" Project: {current.get('project_dir', 'N/A')}")
307
+ else:
308
+ print("❌ Daemon is not running")
309
+
310
+ elif action == "stop":
311
+ # Stop daemon
312
+ if daemon_client.stop_daemon():
313
+ sys.exit(0)
314
+ else:
315
+ ErrorFormatter.print_error("Failed to stop daemon", "")
316
+ sys.exit(1)
317
+
318
+ elif action == "restart":
319
+ # Restart daemon
320
+ print("Restarting daemon...")
321
+ if daemon_client.is_daemon_running():
322
+ if not daemon_client.stop_daemon():
323
+ ErrorFormatter.print_error("Failed to stop daemon", "")
324
+ sys.exit(1)
325
+
326
+ # Start fresh daemon
327
+ if daemon_client.ensure_daemon_running():
328
+ print("✅ Daemon restarted successfully")
329
+ sys.exit(0)
330
+ else:
331
+ ErrorFormatter.print_error("Failed to restart daemon", "")
332
+ sys.exit(1)
333
+
334
+ elif action == "list":
335
+ # List all daemon instances
336
+ daemon_client.display_daemon_list()
337
+
338
+ elif action == "locks":
339
+ # Show lock status
340
+ daemon_client.display_lock_status()
341
+
342
+ elif action == "clear-locks":
343
+ # Clear stale locks
344
+ if daemon_client.request_clear_stale_locks():
345
+ sys.exit(0)
346
+ else:
347
+ sys.exit(1)
348
+
349
+ elif action == "kill":
350
+ # Kill specific daemon by PID
351
+ if pid is None:
352
+ ErrorFormatter.print_error("--pid required for kill action", "")
353
+ print("Usage: fbuild daemon kill --pid <PID> [--force]")
354
+ sys.exit(1)
355
+
356
+ if force:
357
+ success = daemon_client.force_kill_daemon(pid)
358
+ else:
359
+ success = daemon_client.graceful_kill_daemon(pid)
360
+
361
+ if success:
362
+ print(f"✅ Daemon (PID {pid}) terminated")
363
+ sys.exit(0)
364
+ else:
365
+ ErrorFormatter.print_error(f"Failed to terminate daemon (PID {pid})", "Process may not exist")
366
+ sys.exit(1)
367
+
368
+ elif action == "kill-all":
369
+ # Kill all daemon instances
370
+ killed = daemon_client.kill_all_daemons(force=force)
371
+ if killed > 0:
372
+ print(f"✅ Killed {killed} daemon instance(s)")
373
+ else:
374
+ print("No daemon instances found to kill")
375
+ sys.exit(0)
376
+
377
+ else:
378
+ ErrorFormatter.print_error(f"Unknown daemon action: {action}", "")
379
+ print("Valid actions: status, stop, restart, list, locks, clear-locks, kill, kill-all")
380
+ sys.exit(1)
381
+
382
+ except KeyboardInterrupt as ke:
383
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
384
+
385
+ handle_keyboard_interrupt_properly(ke)
386
+ except Exception as e:
387
+ ErrorFormatter.handle_unexpected_error(e, verbose=False)
388
+
389
+
390
+ def parse_default_action_args(argv: list[str]) -> DeployArgs:
391
+ """Parse arguments for the default action (fbuild <project_dir> [flags]).
392
+
393
+ Args:
394
+ argv: Command-line arguments (sys.argv)
395
+
396
+ Returns:
397
+ DeployArgs with parsed values
398
+
399
+ Raises:
400
+ SystemExit: If project directory is invalid or required arguments are missing
401
+ """
402
+ if len(argv) < 2:
403
+ ErrorFormatter.print_error("Missing project directory", "")
404
+ sys.exit(1)
405
+
406
+ project_dir = Path(argv[1])
407
+ PathValidator.validate_project_dir(project_dir)
408
+
409
+ # Parse remaining arguments
410
+ monitor: Optional[str] = None
411
+ port: Optional[str] = None
412
+ environment: Optional[str] = None
413
+ clean = False
414
+ verbose = False
415
+
416
+ i = 2
417
+ while i < len(argv):
418
+ arg = argv[i]
419
+
420
+ # Handle --monitor flag
421
+ if arg.startswith("--monitor="):
422
+ monitor = arg.split("=", 1)[1]
423
+ i += 1
424
+ elif arg == "--monitor" and i + 1 < len(argv):
425
+ monitor = argv[i + 1]
426
+ i += 2
427
+ # Handle --port flag
428
+ elif arg.startswith("--port="):
429
+ port = arg.split("=", 1)[1]
430
+ i += 1
431
+ elif arg in ("-p", "--port") and i + 1 < len(argv):
432
+ port = argv[i + 1]
433
+ i += 2
434
+ # Handle --environment flag
435
+ elif arg.startswith("--environment="):
436
+ environment = arg.split("=", 1)[1]
437
+ i += 1
438
+ elif arg.startswith("-e="):
439
+ environment = arg.split("=", 1)[1]
440
+ i += 1
441
+ elif arg in ("-e", "--environment") and i + 1 < len(argv):
442
+ environment = argv[i + 1]
443
+ i += 2
444
+ # Handle --clean flag
445
+ elif arg in ("-c", "--clean"):
446
+ clean = True
447
+ i += 1
448
+ # Handle --verbose flag
449
+ elif arg in ("-v", "--verbose"):
450
+ verbose = True
451
+ i += 1
452
+ else:
453
+ # Unknown flag - warn and skip
454
+ ErrorFormatter.print_error(f"Unknown flag in default action: {arg}", "")
455
+ print("Hint: Use 'fbuild deploy --help' to see available flags")
456
+ sys.exit(1)
457
+
458
+ return DeployArgs(
459
+ project_dir=project_dir,
460
+ environment=environment,
461
+ port=port,
462
+ clean=clean,
463
+ monitor=monitor if monitor is not None else "", # Empty string means monitor with default settings
464
+ verbose=verbose,
465
+ )
466
+
467
+
468
+ def main() -> None:
469
+ """fbuild - Modern embedded build system.
470
+
471
+ Replace PlatformIO with URL-based platform/toolchain management.
472
+ """
473
+ # Handle default action: fbuild <project_dir> [flags] → deploy with monitor
474
+ # 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"]:
476
+ # User provided a path without a subcommand - use default action
477
+ deploy_args = parse_default_action_args(sys.argv)
478
+ deploy_command(deploy_args)
479
+ return
480
+
481
+ parser = argparse.ArgumentParser(
482
+ prog="fbuild",
483
+ description="fbuild - Modern embedded build system",
484
+ )
485
+ parser.add_argument(
486
+ "--version",
487
+ action="version",
488
+ version=f"fbuild {__version__}",
489
+ )
490
+
491
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
492
+
493
+ # Build command
494
+ build_parser = subparsers.add_parser(
495
+ "build",
496
+ help="Build firmware for embedded target",
497
+ )
498
+ build_parser.add_argument(
499
+ "project_dir",
500
+ nargs="?",
501
+ type=Path,
502
+ default=Path.cwd(),
503
+ help="Project directory (default: current directory)",
504
+ )
505
+ build_parser.add_argument(
506
+ "-e",
507
+ "--environment",
508
+ default=None,
509
+ help="Build environment (default: auto-detect from platformio.ini)",
510
+ )
511
+ build_parser.add_argument(
512
+ "-c",
513
+ "--clean",
514
+ action="store_true",
515
+ help="Clean build artifacts before building",
516
+ )
517
+ build_parser.add_argument(
518
+ "-v",
519
+ "--verbose",
520
+ action="store_true",
521
+ help="Show verbose build output",
522
+ )
523
+
524
+ # Deploy command
525
+ deploy_parser = subparsers.add_parser(
526
+ "deploy",
527
+ help="Deploy firmware to embedded target",
528
+ )
529
+ deploy_parser.add_argument(
530
+ "project_dir",
531
+ nargs="?",
532
+ type=Path,
533
+ default=Path.cwd(),
534
+ help="Project directory (default: current directory)",
535
+ )
536
+ deploy_parser.add_argument(
537
+ "-e",
538
+ "--environment",
539
+ default=None,
540
+ help="Build environment (default: auto-detect from platformio.ini)",
541
+ )
542
+ deploy_parser.add_argument(
543
+ "-p",
544
+ "--port",
545
+ default=None,
546
+ help="Serial port (default: auto-detect)",
547
+ )
548
+ deploy_parser.add_argument(
549
+ "-c",
550
+ "--clean",
551
+ action="store_true",
552
+ help="Clean build artifacts before building",
553
+ )
554
+ deploy_parser.add_argument(
555
+ "--monitor",
556
+ default=None,
557
+ help="Monitor flags to pass after deployment (e.g., '--timeout 60 --halt-on-success \"TEST PASSED\"')",
558
+ )
559
+ deploy_parser.add_argument(
560
+ "--qemu",
561
+ action="store_true",
562
+ help="Deploy to QEMU emulator instead of physical device (requires Docker)",
563
+ )
564
+ deploy_parser.add_argument(
565
+ "--qemu-timeout",
566
+ type=int,
567
+ default=30,
568
+ help="Timeout in seconds for QEMU execution (default: 30)",
569
+ )
570
+ deploy_parser.add_argument(
571
+ "-v",
572
+ "--verbose",
573
+ action="store_true",
574
+ help="Show verbose output",
575
+ )
576
+
577
+ # Monitor command
578
+ monitor_parser = subparsers.add_parser(
579
+ "monitor",
580
+ help="Monitor serial output from embedded target",
581
+ )
582
+ monitor_parser.add_argument(
583
+ "project_dir",
584
+ nargs="?",
585
+ type=Path,
586
+ default=Path.cwd(),
587
+ help="Project directory (default: current directory)",
588
+ )
589
+ monitor_parser.add_argument(
590
+ "-e",
591
+ "--environment",
592
+ default=None,
593
+ help="Build environment (default: auto-detect from platformio.ini)",
594
+ )
595
+ monitor_parser.add_argument(
596
+ "-p",
597
+ "--port",
598
+ default=None,
599
+ help="Serial port (default: auto-detect)",
600
+ )
601
+ monitor_parser.add_argument(
602
+ "-b",
603
+ "--baud",
604
+ default=115200,
605
+ type=int,
606
+ help="Baud rate (default: 115200)",
607
+ )
608
+ monitor_parser.add_argument(
609
+ "-t",
610
+ "--timeout",
611
+ default=None,
612
+ type=int,
613
+ help="Timeout in seconds (default: no timeout)",
614
+ )
615
+ monitor_parser.add_argument(
616
+ "--halt-on-error",
617
+ default=None,
618
+ help="Pattern that triggers error exit (regex)",
619
+ )
620
+ monitor_parser.add_argument(
621
+ "--halt-on-success",
622
+ default=None,
623
+ help="Pattern that triggers success exit (regex)",
624
+ )
625
+ monitor_parser.add_argument(
626
+ "--expect",
627
+ default=None,
628
+ help="Expected pattern - checked at timeout/success, exit 0 if found, 1 if not (regex)",
629
+ )
630
+ monitor_parser.add_argument(
631
+ "--no-timestamp",
632
+ action="store_true",
633
+ dest="no_timestamp",
634
+ help="Disable timestamp prefix on each output line (timestamps enabled by default)",
635
+ )
636
+ monitor_parser.add_argument(
637
+ "-v",
638
+ "--verbose",
639
+ action="store_true",
640
+ help="Show verbose output",
641
+ )
642
+
643
+ # Daemon command
644
+ daemon_parser = subparsers.add_parser(
645
+ "daemon",
646
+ help="Manage the fbuild daemon",
647
+ )
648
+ daemon_parser.add_argument(
649
+ "action",
650
+ choices=["status", "stop", "restart", "list", "locks", "clear-locks", "kill", "kill-all"],
651
+ help="Daemon action to perform",
652
+ )
653
+ daemon_parser.add_argument(
654
+ "--pid",
655
+ type=int,
656
+ default=None,
657
+ help="PID of daemon to kill (required for 'kill' action)",
658
+ )
659
+ daemon_parser.add_argument(
660
+ "--force",
661
+ action="store_true",
662
+ help="Force kill without graceful shutdown (for 'kill' and 'kill-all' actions)",
663
+ )
664
+
665
+ # Parse arguments
666
+ parsed_args = parser.parse_args()
667
+
668
+ # If no command specified, show help
669
+ if not parsed_args.command:
670
+ parser.print_help()
671
+ sys.exit(0)
672
+
673
+ # Validate project directory exists
674
+ if hasattr(parsed_args, "project_dir"):
675
+ PathValidator.validate_project_dir(parsed_args.project_dir)
676
+
677
+ # Execute command
678
+ if parsed_args.command == "build":
679
+ build_args = BuildArgs(
680
+ project_dir=parsed_args.project_dir,
681
+ environment=parsed_args.environment,
682
+ clean=parsed_args.clean,
683
+ verbose=parsed_args.verbose,
684
+ )
685
+ build_command(build_args)
686
+ elif parsed_args.command == "deploy":
687
+ deploy_args = DeployArgs(
688
+ project_dir=parsed_args.project_dir,
689
+ environment=parsed_args.environment,
690
+ port=parsed_args.port,
691
+ clean=parsed_args.clean,
692
+ monitor=parsed_args.monitor,
693
+ verbose=parsed_args.verbose,
694
+ qemu=parsed_args.qemu,
695
+ qemu_timeout=parsed_args.qemu_timeout,
696
+ )
697
+ deploy_command(deploy_args)
698
+ elif parsed_args.command == "monitor":
699
+ monitor_args = MonitorArgs(
700
+ project_dir=parsed_args.project_dir,
701
+ environment=parsed_args.environment,
702
+ port=parsed_args.port,
703
+ baud=parsed_args.baud,
704
+ timeout=parsed_args.timeout,
705
+ halt_on_error=parsed_args.halt_on_error,
706
+ halt_on_success=parsed_args.halt_on_success,
707
+ expect=parsed_args.expect,
708
+ verbose=parsed_args.verbose,
709
+ timestamp=not parsed_args.no_timestamp,
710
+ )
711
+ monitor_command(monitor_args)
712
+ elif parsed_args.command == "daemon":
713
+ daemon_command(parsed_args.action, pid=parsed_args.pid, force=parsed_args.force)
714
+
715
+
716
+ if __name__ == "__main__":
717
+ main()