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.
- fbuild/__init__.py +390 -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_info_generator.py +624 -0
- fbuild/build/build_state.py +325 -0
- fbuild/build/build_utils.py +93 -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 +664 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +214 -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 +651 -0
- fbuild/build/orchestrator_esp32.py +878 -0
- fbuild/build/orchestrator_rp2040.py +719 -0
- fbuild/build/orchestrator_stm32.py +696 -0
- fbuild/build/orchestrator_teensy.py +580 -0
- fbuild/build/source_compilation_orchestrator.py +218 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +717 -0
- fbuild/cli_utils.py +314 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +542 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +369 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +42 -0
- fbuild/daemon/async_client.py +531 -0
- fbuild/daemon/client.py +1505 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/configuration_lock.py +865 -0
- fbuild/daemon/daemon.py +585 -0
- fbuild/daemon/daemon_context.py +293 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/firmware_ledger.py +546 -0
- fbuild/daemon/lock_manager.py +508 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +957 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/port_state_manager.py +249 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +18 -0
- fbuild/daemon/processors/build_processor.py +248 -0
- fbuild/daemon/processors/deploy_processor.py +664 -0
- fbuild/daemon/processors/install_deps_processor.py +431 -0
- fbuild/daemon/processors/locking_processor.py +777 -0
- fbuild/daemon/processors/monitor_processor.py +285 -0
- fbuild/daemon/request_processor.py +457 -0
- fbuild/daemon/shared_serial.py +819 -0
- fbuild/daemon/status_manager.py +238 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +21 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +310 -0
- fbuild/deploy/docker_utils.py +315 -0
- fbuild/deploy/monitor.py +519 -0
- fbuild/deploy/qemu_runner.py +603 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/ledger/__init__.py +52 -0
- fbuild/ledger/board_ledger.py +560 -0
- fbuild/output.py +352 -0
- fbuild/packages/__init__.py +66 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +256 -0
- fbuild/packages/concurrent_manager.py +510 -0
- fbuild/packages/downloader.py +518 -0
- fbuild/packages/fingerprint.py +423 -0
- fbuild/packages/framework_esp32.py +538 -0
- fbuild/packages/framework_rp2040.py +349 -0
- fbuild/packages/framework_stm32.py +459 -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 +725 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_rp2040.py +400 -0
- fbuild/packages/platform_stm32.py +581 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +369 -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 +489 -0
- fbuild/packages/toolchain_metadata.py +185 -0
- fbuild/packages/toolchain_rp2040.py +436 -0
- fbuild/packages/toolchain_stm32.py +417 -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/platform_configs/rp2040.json +70 -0
- fbuild/platform_configs/rp2350.json +76 -0
- fbuild/platform_configs/stm32f1.json +59 -0
- fbuild/platform_configs/stm32f4.json +63 -0
- fbuild/py.typed +0 -0
- fbuild-1.2.8.dist-info/METADATA +468 -0
- fbuild-1.2.8.dist-info/RECORD +121 -0
- fbuild-1.2.8.dist-info/WHEEL +5 -0
- fbuild-1.2.8.dist-info/entry_points.txt +5 -0
- fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
- fbuild-1.2.8.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
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()
|