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
@@ -0,0 +1,719 @@
1
+ """
2
+ RP2040/RP2350-specific build orchestration for Fbuild projects.
3
+
4
+ This module handles Raspberry Pi Pico platform builds separately from other platforms,
5
+ providing cleaner separation of concerns and better maintainability.
6
+ """
7
+
8
+ import _thread
9
+ import logging
10
+ import struct
11
+ import time
12
+ from pathlib import Path
13
+ from typing import List, Optional
14
+ from dataclasses import dataclass
15
+
16
+ from ..packages import Cache
17
+ from ..packages.platform_rp2040 import PlatformRP2040
18
+ from ..packages.toolchain_rp2040 import ToolchainRP2040
19
+ from ..packages.library_manager import LibraryManager, LibraryError
20
+ from ..config.board_config import BoardConfig
21
+ from ..cli_utils import BannerFormatter
22
+ from .configurable_compiler import ConfigurableCompiler
23
+ from .configurable_linker import ConfigurableLinker
24
+ from .linker import SizeInfo
25
+ from .orchestrator import IBuildOrchestrator, BuildResult
26
+ from .build_utils import safe_rmtree
27
+ from .build_state import BuildStateTracker
28
+ from .build_info_generator import BuildInfoGenerator
29
+
30
+ # Module-level logger
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ @dataclass
35
+ class BuildResultRP2040:
36
+ """Result of an RP2040/RP2350 build operation (internal use)."""
37
+
38
+ success: bool
39
+ firmware_uf2: Optional[Path]
40
+ firmware_bin: Optional[Path]
41
+ firmware_elf: Optional[Path]
42
+ size_info: Optional[SizeInfo]
43
+ build_time: float
44
+ message: str
45
+
46
+
47
+ class OrchestratorRP2040(IBuildOrchestrator):
48
+ """
49
+ Orchestrates RP2040/RP2350-specific build process.
50
+
51
+ Handles platform initialization, toolchain setup, framework preparation,
52
+ and firmware generation for Raspberry Pi Pico targets.
53
+ """
54
+
55
+ # UF2 magic numbers and constants
56
+ UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
57
+ UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
58
+ UF2_MAGIC_END = 0x0AB16F30 # Final magic
59
+ UF2_FLAG_FAMILY_ID_PRESENT = 0x00002000
60
+ RP2040_FAMILY_ID = 0xE48BFF56
61
+ RP2350_FAMILY_ID = 0xE48BFF59 # Different family ID for RP2350
62
+
63
+ def __init__(self, cache: Cache, verbose: bool = False):
64
+ """
65
+ Initialize RP2040/RP2350 orchestrator.
66
+
67
+ Args:
68
+ cache: Cache instance for package management
69
+ verbose: Enable verbose output
70
+ """
71
+ self.cache = cache
72
+ self.verbose = verbose
73
+
74
+ def build(
75
+ self,
76
+ project_dir: Path,
77
+ env_name: Optional[str] = None,
78
+ clean: bool = False,
79
+ verbose: Optional[bool] = None
80
+ ) -> BuildResult:
81
+ """Execute complete build process (IBuildOrchestrator interface).
82
+
83
+ Args:
84
+ project_dir: Project root directory containing platformio.ini
85
+ env_name: Environment name to build (defaults to first/default env)
86
+ clean: Clean build (remove all artifacts before building)
87
+ verbose: Override verbose setting
88
+
89
+ Returns:
90
+ BuildResult with build status and output paths
91
+
92
+ Raises:
93
+ BuildOrchestratorError: If build fails at any phase
94
+ """
95
+ from ..config import PlatformIOConfig
96
+
97
+ verbose_mode = verbose if verbose is not None else self.verbose
98
+
99
+ # Parse platformio.ini to get environment configuration
100
+ ini_path = project_dir / "platformio.ini"
101
+ if not ini_path.exists():
102
+ return BuildResult(
103
+ success=False,
104
+ hex_path=None,
105
+ elf_path=None,
106
+ size_info=None,
107
+ build_time=0.0,
108
+ message=f"platformio.ini not found in {project_dir}"
109
+ )
110
+
111
+ try:
112
+ config = PlatformIOConfig(ini_path)
113
+
114
+ # Determine environment to build
115
+ if env_name is None:
116
+ env_name = config.get_default_environment()
117
+ if env_name is None:
118
+ return BuildResult(
119
+ success=False,
120
+ hex_path=None,
121
+ elf_path=None,
122
+ size_info=None,
123
+ build_time=0.0,
124
+ message="No environment specified and no default found in platformio.ini"
125
+ )
126
+
127
+ env_config = config.get_env_config(env_name)
128
+ board_id = env_config.get("board", "rpipico")
129
+ build_flags = config.get_build_flags(env_name)
130
+ lib_deps = config.get_lib_deps(env_name)
131
+
132
+ # Call internal build method
133
+ rp2040_result = self._build_rp2040(
134
+ project_dir, env_name, board_id, env_config, build_flags, lib_deps, clean, verbose_mode
135
+ )
136
+
137
+ # Convert BuildResultRP2040 to BuildResult
138
+ # Note: hex_path maps to uf2_path for RP2040/RP2350
139
+ return BuildResult(
140
+ success=rp2040_result.success,
141
+ hex_path=rp2040_result.firmware_uf2, # UF2 is the primary firmware format
142
+ elf_path=rp2040_result.firmware_elf,
143
+ size_info=rp2040_result.size_info,
144
+ build_time=rp2040_result.build_time,
145
+ message=rp2040_result.message
146
+ )
147
+
148
+ except KeyboardInterrupt:
149
+ _thread.interrupt_main()
150
+ raise
151
+ except Exception as e:
152
+ return BuildResult(
153
+ success=False,
154
+ hex_path=None,
155
+ elf_path=None,
156
+ size_info=None,
157
+ build_time=0.0,
158
+ message=f"Failed to parse configuration: {e}"
159
+ )
160
+
161
+ def _build_rp2040(
162
+ self,
163
+ project_dir: Path,
164
+ env_name: str,
165
+ board_id: str,
166
+ env_config: dict,
167
+ build_flags: List[str],
168
+ lib_deps: List[str],
169
+ clean: bool = False,
170
+ verbose: bool = False
171
+ ) -> BuildResultRP2040:
172
+ """
173
+ Execute complete RP2040/RP2350 build process (internal implementation).
174
+
175
+ Args:
176
+ project_dir: Project directory
177
+ env_name: Environment name
178
+ board_id: Board ID (e.g., rpipico, rpipico2)
179
+ env_config: Environment configuration dict
180
+ build_flags: User build flags from platformio.ini
181
+ lib_deps: Library dependencies from platformio.ini
182
+ clean: Whether to clean before build
183
+ verbose: Verbose output mode
184
+
185
+ Returns:
186
+ BuildResultRP2040 with build status and output paths
187
+ """
188
+ start_time = time.time()
189
+
190
+ try:
191
+ # Get board configuration
192
+ from ..config.board_config import BoardConfig
193
+
194
+ if verbose:
195
+ logger.info("[2/7] Loading board configuration...")
196
+ else:
197
+ logger.info("Loading board configuration...")
198
+
199
+ board_config = BoardConfig.from_board_id(board_id)
200
+
201
+ # Initialize platform
202
+ if verbose:
203
+ logger.info("[3/7] Initializing RP2040/RP2350 platform...")
204
+ else:
205
+ logger.info("Initializing RP2040/RP2350 platform...")
206
+
207
+ platform = PlatformRP2040(
208
+ self.cache,
209
+ board_config.mcu,
210
+ show_progress=True
211
+ )
212
+ platform.ensure_package()
213
+
214
+ if verbose:
215
+ logger.info(f" Board: {board_id}")
216
+ logger.info(f" MCU: {board_config.mcu}")
217
+ logger.info(f" CPU Frequency: {board_config.f_cpu}")
218
+
219
+ # Setup build directory
220
+ build_dir = self._setup_build_directory(env_name, clean, verbose)
221
+
222
+ # Check build state and invalidate cache if needed
223
+ if verbose:
224
+ logger.info("[3.5/7] Checking build configuration state...")
225
+
226
+ state_tracker = BuildStateTracker(build_dir)
227
+ needs_rebuild, reasons, current_state = state_tracker.check_invalidation(
228
+ platformio_ini_path=project_dir / "platformio.ini",
229
+ platform="raspberrypi",
230
+ board=board_id,
231
+ framework=env_config.get('framework', 'arduino'),
232
+ toolchain_version=platform.toolchain.version,
233
+ framework_version=platform.framework.version,
234
+ platform_version=f"rp2040-{platform.framework.version}",
235
+ build_flags=build_flags,
236
+ lib_deps=lib_deps,
237
+ )
238
+
239
+ if needs_rebuild:
240
+ if verbose:
241
+ logger.info(" Build cache invalidated:")
242
+ for reason in reasons:
243
+ logger.info(f" - {reason}")
244
+ logger.info(" Cleaning build artifacts...")
245
+ # Clean build artifacts to force rebuild
246
+ if build_dir.exists():
247
+ safe_rmtree(build_dir)
248
+ # Recreate build directory
249
+ build_dir.mkdir(parents=True, exist_ok=True)
250
+ else:
251
+ if verbose:
252
+ logger.info(" Build configuration unchanged, using cached artifacts")
253
+
254
+ # Load platform configuration JSON for MCU-specific settings
255
+ import json
256
+ platform_config_path = Path(__file__).parent.parent / "platform_configs" / f"{board_config.mcu}.json"
257
+ platform_config = None
258
+ if platform_config_path.exists():
259
+ with open(platform_config_path, 'r') as f:
260
+ platform_config = json.load(f)
261
+
262
+ # Initialize compiler
263
+ if verbose:
264
+ logger.info("[4/7] Compiling Arduino core...")
265
+ else:
266
+ logger.info("Compiling Arduino core...")
267
+
268
+ compiler = ConfigurableCompiler(
269
+ platform,
270
+ platform.toolchain,
271
+ platform.framework,
272
+ board_id,
273
+ build_dir,
274
+ platform_config=platform_config,
275
+ show_progress=verbose,
276
+ user_build_flags=build_flags
277
+ )
278
+
279
+ # Compile Arduino core with progress bar
280
+ if verbose:
281
+ core_obj_files = compiler.compile_core()
282
+ else:
283
+ # Use tqdm progress bar for non-verbose mode
284
+ from tqdm import tqdm
285
+
286
+ # Get number of core source files for progress tracking
287
+ core_sources = platform.framework.get_core_sources("rp2040")
288
+ total_files = len(core_sources)
289
+
290
+ # Create progress bar
291
+ with tqdm(
292
+ total=total_files,
293
+ desc='Compiling Arduino core',
294
+ unit='file',
295
+ ncols=80,
296
+ leave=False
297
+ ) as pbar:
298
+ core_obj_files = compiler.compile_core(progress_bar=pbar)
299
+
300
+ # Print completion message
301
+ logger.info(f"Compiled {len(core_obj_files)} core files")
302
+
303
+ core_archive = compiler.create_core_archive(core_obj_files)
304
+
305
+ if verbose:
306
+ logger.info(f" Compiled {len(core_obj_files)} core source files")
307
+
308
+ # Handle library dependencies (if any)
309
+ library_archives, library_include_paths = self._process_libraries(
310
+ env_config, build_dir, compiler, platform.toolchain, board_config, verbose, project_dir=project_dir
311
+ )
312
+
313
+ # Add library include paths to compiler
314
+ if library_include_paths:
315
+ compiler.add_library_includes(library_include_paths)
316
+
317
+ # Get src_dir override from platformio.ini
318
+ from ..config import PlatformIOConfig
319
+ config_for_src_dir = PlatformIOConfig(project_dir / "platformio.ini")
320
+ src_dir_override = config_for_src_dir.get_src_dir()
321
+
322
+ # Find and compile sketch
323
+ sketch_obj_files = self._compile_sketch(project_dir, compiler, start_time, verbose, src_dir_override)
324
+ if sketch_obj_files is None:
325
+ search_dir = project_dir / src_dir_override if src_dir_override else project_dir
326
+ return self._error_result(
327
+ start_time,
328
+ f"No .ino sketch file found in {search_dir}"
329
+ )
330
+
331
+ # Initialize linker
332
+ if verbose:
333
+ logger.info("[6/7] Linking firmware...")
334
+ else:
335
+ logger.info("Linking firmware...")
336
+
337
+ linker = ConfigurableLinker(
338
+ platform,
339
+ platform.toolchain,
340
+ platform.framework,
341
+ board_id,
342
+ build_dir,
343
+ platform_config=platform_config,
344
+ show_progress=verbose
345
+ )
346
+
347
+ # Link firmware
348
+ firmware_elf = linker.link(sketch_obj_files, core_archive, library_archives=library_archives)
349
+
350
+ # Generate bin file (intermediate)
351
+ if verbose:
352
+ logger.info("[7/7] Generating firmware...")
353
+ else:
354
+ logger.info("Generating firmware...")
355
+
356
+ firmware_bin = linker.generate_bin(firmware_elf)
357
+
358
+ # Generate UF2 file (final format for RP2040/RP2350)
359
+ firmware_uf2 = self._generate_uf2(firmware_bin, board_config.mcu, verbose)
360
+
361
+ # Get size info
362
+ size_info = linker.get_size_info(firmware_elf)
363
+
364
+ build_time = time.time() - start_time
365
+
366
+ if verbose:
367
+ self._print_success(
368
+ build_time, firmware_elf, firmware_uf2, size_info
369
+ )
370
+
371
+ # Save build state for future cache validation
372
+ if verbose:
373
+ logger.info("[7.5/7] Saving build state...")
374
+ state_tracker.save_state(current_state)
375
+
376
+ # Generate build_info.json
377
+ build_info_generator = BuildInfoGenerator(build_dir)
378
+ # Parse f_cpu from string (e.g., "133000000L") to int
379
+ f_cpu_int = int(board_config.f_cpu.rstrip("L"))
380
+ # Build toolchain_paths dict, filtering out None values
381
+ toolchain_paths_raw = {
382
+ "gcc": platform.toolchain.get_gcc_path(),
383
+ "gxx": platform.toolchain.get_gxx_path(),
384
+ "ar": platform.toolchain.get_ar_path(),
385
+ "objcopy": platform.toolchain.get_objcopy_path(),
386
+ "size": platform.toolchain.get_size_path(),
387
+ }
388
+ toolchain_paths = {k: v for k, v in toolchain_paths_raw.items() if v is not None}
389
+ build_info = build_info_generator.generate_generic(
390
+ env_name=env_name,
391
+ board_id=board_id,
392
+ board_name=board_config.name,
393
+ mcu=board_config.mcu,
394
+ platform="raspberrypi",
395
+ f_cpu=f_cpu_int,
396
+ build_time=build_time,
397
+ elf_path=firmware_elf,
398
+ bin_path=firmware_bin,
399
+ size_info=size_info,
400
+ build_flags=build_flags,
401
+ lib_deps=lib_deps,
402
+ toolchain_version=platform.toolchain.version,
403
+ toolchain_paths=toolchain_paths,
404
+ framework_name="arduino",
405
+ framework_version=platform.framework.version,
406
+ core_path=platform.framework.get_cores_dir(),
407
+ )
408
+ build_info_generator.save(build_info)
409
+ if verbose:
410
+ logger.info(f" Build info saved to {build_info_generator.build_info_path}")
411
+
412
+ return BuildResultRP2040(
413
+ success=True,
414
+ firmware_uf2=firmware_uf2,
415
+ firmware_bin=firmware_bin,
416
+ firmware_elf=firmware_elf,
417
+ size_info=size_info,
418
+ build_time=build_time,
419
+ message="Build successful (native RP2040/RP2350 build)"
420
+ )
421
+
422
+ except KeyboardInterrupt as ke:
423
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
424
+ handle_keyboard_interrupt_properly(ke)
425
+ raise # Never reached, but satisfies type checker
426
+ except Exception as e:
427
+ build_time = time.time() - start_time
428
+ import traceback
429
+ error_trace = traceback.format_exc()
430
+ return BuildResultRP2040(
431
+ success=False,
432
+ firmware_uf2=None,
433
+ firmware_bin=None,
434
+ firmware_elf=None,
435
+ size_info=None,
436
+ build_time=build_time,
437
+ message=f"RP2040/RP2350 native build failed: {e}\n\n{error_trace}"
438
+ )
439
+
440
+ def _generate_uf2(self, bin_path: Path, mcu: str, verbose: bool = False) -> Path:
441
+ """Generate UF2 file from BIN file.
442
+
443
+ UF2 (USB Flashing Format) is used by RP2040/RP2350 bootloaders.
444
+
445
+ Args:
446
+ bin_path: Path to input BIN file
447
+ mcu: MCU type ("rp2040" or "rp2350")
448
+ verbose: Verbose output mode
449
+
450
+ Returns:
451
+ Path to generated UF2 file
452
+
453
+ Raises:
454
+ Exception: If UF2 generation fails
455
+ """
456
+ uf2_path = bin_path.parent / f"{bin_path.stem}.uf2"
457
+
458
+ if verbose:
459
+ logger.info(f" Generating UF2 file: {uf2_path.name}")
460
+
461
+ # Select family ID based on MCU
462
+ family_id = self.RP2350_FAMILY_ID if mcu.lower() == "rp2350" else self.RP2040_FAMILY_ID
463
+
464
+ # Read binary data
465
+ with open(bin_path, 'rb') as f:
466
+ bin_data = f.read()
467
+
468
+ # UF2 block size is 256 bytes of data per block
469
+ block_size = 256
470
+ num_blocks = (len(bin_data) + block_size - 1) // block_size
471
+
472
+ # RP2040/RP2350 flash starts at 0x10000000
473
+ base_address = 0x10000000
474
+
475
+ with open(uf2_path, 'wb') as f:
476
+ for block_num in range(num_blocks):
477
+ # Get data for this block (pad if needed)
478
+ offset = block_num * block_size
479
+ block_data = bin_data[offset:offset + block_size]
480
+ if len(block_data) < block_size:
481
+ block_data += b'\x00' * (block_size - len(block_data))
482
+
483
+ # Calculate target address
484
+ target_addr = base_address + offset
485
+
486
+ # Build UF2 block (512 bytes total)
487
+ uf2_block = struct.pack('<I', self.UF2_MAGIC_START0) # Magic start 0
488
+ uf2_block += struct.pack('<I', self.UF2_MAGIC_START1) # Magic start 1
489
+ uf2_block += struct.pack('<I', self.UF2_FLAG_FAMILY_ID_PRESENT) # Flags
490
+ uf2_block += struct.pack('<I', target_addr) # Target address
491
+ uf2_block += struct.pack('<I', block_size) # Payload size
492
+ uf2_block += struct.pack('<I', block_num) # Block number
493
+ uf2_block += struct.pack('<I', num_blocks) # Total blocks
494
+ uf2_block += struct.pack('<I', family_id) # Family ID
495
+ uf2_block += block_data # Data (256 bytes)
496
+ uf2_block += struct.pack('<I', self.UF2_MAGIC_END) # Magic end
497
+
498
+ # Pad to 512 bytes (476 bytes already written, need 36 more)
499
+ uf2_block += b'\x00' * (512 - len(uf2_block))
500
+
501
+ f.write(uf2_block)
502
+
503
+ if verbose:
504
+ logger.info(f" UF2 file generated: {num_blocks} blocks, {len(bin_data)} bytes")
505
+
506
+ return uf2_path
507
+
508
+ def _setup_build_directory(self, env_name: str, clean: bool, verbose: bool) -> Path:
509
+ """
510
+ Setup build directory with optional cleaning.
511
+
512
+ Args:
513
+ env_name: Environment name
514
+ clean: Whether to clean before build
515
+ verbose: Verbose output mode
516
+
517
+ Returns:
518
+ Build directory path
519
+ """
520
+ build_dir = self.cache.get_build_dir(env_name)
521
+
522
+ if clean and build_dir.exists():
523
+ if verbose:
524
+ logger.info("[1/7] Cleaning build directory...")
525
+ else:
526
+ logger.info("Cleaning build directory...")
527
+ safe_rmtree(build_dir)
528
+
529
+ build_dir.mkdir(parents=True, exist_ok=True)
530
+ return build_dir
531
+
532
+ def _process_libraries(
533
+ self,
534
+ env_config: dict,
535
+ build_dir: Path,
536
+ compiler: ConfigurableCompiler,
537
+ toolchain: ToolchainRP2040,
538
+ board_config: BoardConfig,
539
+ verbose: bool,
540
+ project_dir: Optional[Path] = None
541
+ ) -> tuple[List[Path], List[Path]]:
542
+ """
543
+ Process and compile library dependencies.
544
+
545
+ Args:
546
+ env_config: Environment configuration
547
+ build_dir: Build directory
548
+ compiler: Configured compiler instance
549
+ toolchain: RP2040 toolchain instance
550
+ board_config: Board configuration instance
551
+ verbose: Verbose output mode
552
+ project_dir: Optional project directory for resolving relative library paths
553
+
554
+ Returns:
555
+ Tuple of (library_archives, library_include_paths)
556
+ """
557
+ lib_deps = env_config.get('lib_deps', '')
558
+ library_archives = []
559
+ library_include_paths = []
560
+
561
+ if not lib_deps:
562
+ return library_archives, library_include_paths
563
+
564
+ if verbose:
565
+ logger.info("[4.5/7] Processing library dependencies...")
566
+
567
+ # Parse lib_deps (can be string or list)
568
+ if isinstance(lib_deps, str):
569
+ lib_specs = [dep.strip() for dep in lib_deps.split('\n') if dep.strip()]
570
+ else:
571
+ lib_specs = lib_deps
572
+
573
+ if not lib_specs:
574
+ return library_archives, library_include_paths
575
+
576
+ try:
577
+ # Initialize library manager
578
+ library_manager = LibraryManager(build_dir, mode="release")
579
+
580
+ # Prepare compilation parameters
581
+ lib_defines = []
582
+ defines_dict = board_config.get_defines()
583
+ for key, value in defines_dict.items():
584
+ if value:
585
+ lib_defines.append(f"{key}={value}")
586
+ else:
587
+ lib_defines.append(key)
588
+
589
+ # Get include paths from compiler configuration
590
+ lib_includes = compiler.get_include_paths()
591
+
592
+ # Get compiler path from toolchain (use C++ compiler for libraries)
593
+ compiler_path = toolchain.get_gxx_path()
594
+ if compiler_path is None:
595
+ raise LibraryError("C++ compiler not found in toolchain")
596
+
597
+ if verbose:
598
+ logger.info(f" Found {len(lib_specs)} library dependencies")
599
+
600
+ # Ensure all libraries are downloaded and compiled
601
+ libraries = library_manager.ensure_libraries(
602
+ lib_deps=lib_specs,
603
+ compiler_path=compiler_path,
604
+ mcu=board_config.mcu,
605
+ f_cpu=board_config.f_cpu,
606
+ defines=lib_defines,
607
+ include_paths=lib_includes,
608
+ extra_flags=[],
609
+ show_progress=verbose
610
+ )
611
+
612
+ # Get library artifacts
613
+ library_include_paths = library_manager.get_library_include_paths()
614
+ library_archives = library_manager.get_library_objects()
615
+
616
+ if verbose:
617
+ logger.info(f" Compiled {len(libraries)} libraries")
618
+
619
+ except LibraryError as e:
620
+ logger.warning(f" Error processing libraries: {e}")
621
+ # Continue build without libraries
622
+ library_archives = []
623
+ library_include_paths = []
624
+
625
+ return library_archives, library_include_paths
626
+
627
+ def _compile_sketch(
628
+ self,
629
+ project_dir: Path,
630
+ compiler: ConfigurableCompiler,
631
+ start_time: float,
632
+ verbose: bool,
633
+ src_dir_override: Optional[str] = None
634
+ ) -> Optional[List[Path]]:
635
+ """
636
+ Find and compile sketch files.
637
+
638
+ Args:
639
+ project_dir: Project directory
640
+ compiler: Configured compiler instance
641
+ start_time: Build start time for error reporting
642
+ verbose: Verbose output mode
643
+ src_dir_override: Optional source directory override (relative to project_dir)
644
+
645
+ Returns:
646
+ List of compiled object files or None if no sketch found
647
+ """
648
+ if verbose:
649
+ logger.info("[5/7] Compiling sketch...")
650
+
651
+ # Determine source directory
652
+ if src_dir_override:
653
+ src_dir = project_dir / src_dir_override
654
+ if verbose:
655
+ logger.info(f" Using source directory override: {src_dir_override}")
656
+ else:
657
+ src_dir = project_dir
658
+
659
+ # Look for .ino files in the source directory
660
+ sketch_files = list(src_dir.glob("*.ino"))
661
+ if not sketch_files:
662
+ # Also check src/ directory
663
+ alt_src_dir = project_dir / "src"
664
+ if alt_src_dir.exists() and not src_dir_override:
665
+ sketch_files = list(alt_src_dir.glob("*.ino"))
666
+
667
+ if not sketch_files:
668
+ return None
669
+
670
+ sketch_path = sketch_files[0]
671
+ sketch_obj_files = compiler.compile_sketch(sketch_path)
672
+
673
+ if verbose:
674
+ logger.info(f" Compiled {len(sketch_obj_files)} sketch file(s)")
675
+
676
+ return sketch_obj_files
677
+
678
+ def _error_result(self, start_time: float, message: str) -> BuildResultRP2040:
679
+ """Create error result."""
680
+ return BuildResultRP2040(
681
+ success=False,
682
+ firmware_uf2=None,
683
+ firmware_bin=None,
684
+ firmware_elf=None,
685
+ size_info=None,
686
+ build_time=time.time() - start_time,
687
+ message=message
688
+ )
689
+
690
+ def _print_success(
691
+ self,
692
+ build_time: float,
693
+ firmware_elf: Path,
694
+ firmware_uf2: Path,
695
+ size_info: Optional[SizeInfo]
696
+ ) -> None:
697
+ """
698
+ Print build success message.
699
+
700
+ Args:
701
+ build_time: Total build time
702
+ firmware_elf: Path to firmware ELF
703
+ firmware_uf2: Path to firmware UF2
704
+ size_info: Size information
705
+ """
706
+ # Build success message
707
+ message_lines = ["BUILD SUCCESSFUL!"]
708
+ message_lines.append(f"Build time: {build_time:.2f}s")
709
+ message_lines.append(f"Firmware ELF: {firmware_elf}")
710
+ message_lines.append(f"Firmware UF2: {firmware_uf2}")
711
+
712
+ BannerFormatter.print_banner("\n".join(message_lines), width=60, center=False)
713
+
714
+ # Print size information if available
715
+ if size_info:
716
+ print()
717
+ from .build_utils import SizeInfoPrinter
718
+ SizeInfoPrinter.print_size_info(size_info)
719
+ print()