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,878 @@
1
+ """
2
+ ESP32-specific build orchestration for Fbuild projects.
3
+
4
+ This module handles ESP32 platform builds separately from AVR builds,
5
+ providing cleaner separation of concerns and better maintainability.
6
+ """
7
+
8
+ import _thread
9
+ import logging
10
+ import time
11
+ from pathlib import Path
12
+ from typing import List, Optional
13
+ from dataclasses import dataclass
14
+
15
+ from ..packages import Cache
16
+ from ..packages.platform_esp32 import PlatformESP32
17
+ from ..packages.toolchain_esp32 import ToolchainESP32
18
+ from ..packages.framework_esp32 import FrameworkESP32
19
+ from ..packages.library_manager_esp32 import LibraryManagerESP32
20
+ from ..cli_utils import BannerFormatter
21
+ from .configurable_compiler import ConfigurableCompiler
22
+ from .configurable_linker import ConfigurableLinker
23
+ from .linker import SizeInfo
24
+ from .orchestrator import IBuildOrchestrator, BuildResult
25
+ from .build_utils import safe_rmtree
26
+ from .build_state import BuildStateTracker
27
+ from .build_info_generator import BuildInfoGenerator
28
+ from ..output import log_phase, log_detail, log_warning
29
+
30
+ # Module-level logger
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ @dataclass
35
+ class BuildResultESP32:
36
+ """Result of an ESP32 build operation (internal use)."""
37
+
38
+ success: bool
39
+ firmware_bin: Optional[Path]
40
+ firmware_elf: Optional[Path]
41
+ bootloader_bin: Optional[Path]
42
+ partitions_bin: Optional[Path]
43
+ size_info: Optional[SizeInfo]
44
+ build_time: float
45
+ message: str
46
+
47
+
48
+ class OrchestratorESP32(IBuildOrchestrator):
49
+ """
50
+ Orchestrates ESP32-specific build process.
51
+
52
+ Handles platform initialization, toolchain setup, framework preparation,
53
+ library compilation, and firmware generation for ESP32 targets.
54
+ """
55
+
56
+ def __init__(self, cache: Cache, verbose: bool = False):
57
+ """
58
+ Initialize ESP32 orchestrator.
59
+
60
+ Args:
61
+ cache: Cache instance for package management
62
+ verbose: Enable verbose output
63
+ """
64
+ self.cache = cache
65
+ self.verbose = verbose
66
+
67
+ def build(
68
+ self,
69
+ project_dir: Path,
70
+ env_name: Optional[str] = None,
71
+ clean: bool = False,
72
+ verbose: Optional[bool] = None
73
+ ) -> BuildResult:
74
+ """Execute complete build process (BaseBuildOrchestrator interface).
75
+
76
+ Args:
77
+ project_dir: Project root directory containing platformio.ini
78
+ env_name: Environment name to build (defaults to first/default env)
79
+ clean: Clean build (remove all artifacts before building)
80
+ verbose: Override verbose setting
81
+
82
+ Returns:
83
+ BuildResult with build status and output paths
84
+
85
+ Raises:
86
+ BuildOrchestratorError: If build fails at any phase
87
+ """
88
+ from ..config import PlatformIOConfig
89
+
90
+ verbose_mode = verbose if verbose is not None else self.verbose
91
+
92
+ # Parse platformio.ini to get environment configuration
93
+ ini_path = project_dir / "platformio.ini"
94
+ if not ini_path.exists():
95
+ return BuildResult(
96
+ success=False,
97
+ hex_path=None,
98
+ elf_path=None,
99
+ size_info=None,
100
+ build_time=0.0,
101
+ message=f"platformio.ini not found in {project_dir}"
102
+ )
103
+
104
+ try:
105
+ config = PlatformIOConfig(ini_path)
106
+
107
+ # Determine environment to build
108
+ if env_name is None:
109
+ env_name = config.get_default_environment()
110
+ if env_name is None:
111
+ return BuildResult(
112
+ success=False,
113
+ hex_path=None,
114
+ elf_path=None,
115
+ size_info=None,
116
+ build_time=0.0,
117
+ message="No environment specified and no default found in platformio.ini"
118
+ )
119
+
120
+ env_config = config.get_env_config(env_name)
121
+ board_id = env_config.get("board", "")
122
+ build_flags = config.get_build_flags(env_name)
123
+
124
+ # Add debug logging for lib_deps
125
+ logger.debug(f"[ORCHESTRATOR] About to call config.get_lib_deps('{env_name}')")
126
+ lib_deps = config.get_lib_deps(env_name)
127
+ logger.debug(f"[ORCHESTRATOR] get_lib_deps returned: {lib_deps}")
128
+
129
+ # Call internal build method
130
+ esp32_result = self._build_esp32(
131
+ project_dir, env_name, board_id, env_config, build_flags, lib_deps, clean, verbose_mode
132
+ )
133
+
134
+ # Convert BuildResultESP32 to BuildResult
135
+ return BuildResult(
136
+ success=esp32_result.success,
137
+ hex_path=esp32_result.firmware_bin,
138
+ elf_path=esp32_result.firmware_elf,
139
+ size_info=esp32_result.size_info,
140
+ build_time=esp32_result.build_time,
141
+ message=esp32_result.message
142
+ )
143
+
144
+ except KeyboardInterrupt:
145
+ _thread.interrupt_main()
146
+ raise
147
+ except Exception as e:
148
+ return BuildResult(
149
+ success=False,
150
+ hex_path=None,
151
+ elf_path=None,
152
+ size_info=None,
153
+ build_time=0.0,
154
+ message=f"Failed to parse configuration: {e}"
155
+ )
156
+
157
+ def _build_esp32(
158
+ self,
159
+ project_dir: Path,
160
+ env_name: str,
161
+ board_id: str,
162
+ env_config: dict,
163
+ build_flags: List[str],
164
+ lib_deps: List[str],
165
+ clean: bool = False,
166
+ verbose: bool = False
167
+ ) -> BuildResultESP32:
168
+ """
169
+ Execute complete ESP32 build process (internal implementation).
170
+
171
+ Args:
172
+ project_dir: Project directory
173
+ env_name: Environment name
174
+ board_id: Board ID (e.g., esp32-c6-devkitm-1)
175
+ env_config: Environment configuration dict
176
+ build_flags: User build flags from platformio.ini
177
+ lib_deps: Library dependencies from platformio.ini
178
+ clean: Whether to clean before build
179
+ verbose: Verbose output mode
180
+
181
+ Returns:
182
+ BuildResultESP32 with build status and output paths
183
+ """
184
+ start_time = time.time()
185
+
186
+ try:
187
+ # Get platform URL from env_config
188
+ platform_url = env_config.get('platform')
189
+ if not platform_url:
190
+ return self._error_result(
191
+ start_time,
192
+ "No platform URL specified in platformio.ini"
193
+ )
194
+
195
+ # Resolve platform shorthand to actual download URL
196
+ # PlatformIO supports formats like "platformio/espressif32" which need
197
+ # to be converted to a real download URL
198
+ platform_url = self._resolve_platform_url(platform_url)
199
+
200
+ # Initialize platform
201
+ log_phase(3, 12, "Initializing ESP32 platform...")
202
+
203
+ platform = PlatformESP32(self.cache, platform_url, show_progress=True)
204
+ platform.ensure_platform()
205
+
206
+ # Get board configuration
207
+ board_json = platform.get_board_json(board_id)
208
+ mcu = board_json.get("build", {}).get("mcu", "esp32c6")
209
+
210
+ log_detail(f"Board: {board_id}", verbose_only=True)
211
+ log_detail(f"MCU: {mcu}", verbose_only=True)
212
+
213
+ # Get required packages
214
+ packages = platform.get_required_packages(mcu)
215
+
216
+ # Initialize toolchain
217
+ toolchain = self._setup_toolchain(packages, start_time, verbose)
218
+ if toolchain is None:
219
+ return self._error_result(
220
+ start_time,
221
+ "Failed to initialize toolchain"
222
+ )
223
+
224
+ # Initialize framework
225
+ framework = self._setup_framework(packages, start_time, verbose)
226
+ if framework is None:
227
+ return self._error_result(
228
+ start_time,
229
+ "Failed to initialize framework"
230
+ )
231
+
232
+ # Setup build directory
233
+ build_dir = self._setup_build_directory(env_name, clean, verbose)
234
+
235
+ # Check build state and invalidate cache if needed
236
+ log_detail("Checking build configuration state...", verbose_only=True)
237
+
238
+ state_tracker = BuildStateTracker(build_dir)
239
+ needs_rebuild, reasons, current_state = state_tracker.check_invalidation(
240
+ platformio_ini_path=project_dir / "platformio.ini",
241
+ platform="esp32",
242
+ board=board_id,
243
+ framework=env_config.get('framework', 'arduino'),
244
+ toolchain_version=toolchain.version,
245
+ framework_version=framework.version,
246
+ platform_version=platform.version,
247
+ build_flags=build_flags,
248
+ lib_deps=lib_deps,
249
+ )
250
+
251
+ if needs_rebuild:
252
+ log_detail("Build cache invalidated:", verbose_only=True)
253
+ for reason in reasons:
254
+ log_detail(f" - {reason}", indent=8, verbose_only=True)
255
+ log_detail("Cleaning build artifacts...", verbose_only=True)
256
+ # Clean build artifacts to force rebuild
257
+ from .build_utils import safe_rmtree
258
+ if build_dir.exists():
259
+ safe_rmtree(build_dir)
260
+ # Recreate build directory
261
+ build_dir.mkdir(parents=True, exist_ok=True)
262
+ else:
263
+ log_detail("Build configuration unchanged, using cached artifacts", verbose_only=True)
264
+
265
+ # Initialize compilation executor early to show sccache status
266
+ from .compilation_executor import CompilationExecutor
267
+ compilation_executor = CompilationExecutor(
268
+ build_dir=build_dir,
269
+ show_progress=verbose
270
+ )
271
+
272
+ # Try to get compilation queue from daemon for async compilation
273
+ # TODO: Implement get_compilation_queue() in daemon module
274
+ compilation_queue = None
275
+ # try:
276
+ # from ..daemon import daemon
277
+ # compilation_queue = daemon.get_compilation_queue()
278
+ # if compilation_queue and verbose:
279
+ # num_workers = getattr(compilation_queue, 'num_workers', 'unknown')
280
+ # print(f"[Async Mode] Using daemon compilation queue with {num_workers} workers")
281
+ # except (ImportError, AttributeError):
282
+ # # Daemon not available or not running - use synchronous compilation
283
+ # if verbose:
284
+ # print("[Sync Mode] Daemon not available, using synchronous compilation")
285
+
286
+ # Initialize compiler
287
+ log_phase(7, 12, "Compiling Arduino core...")
288
+
289
+ compiler = ConfigurableCompiler(
290
+ platform,
291
+ toolchain,
292
+ framework,
293
+ board_id,
294
+ build_dir,
295
+ platform_config=None,
296
+ show_progress=verbose,
297
+ user_build_flags=build_flags,
298
+ compilation_executor=compilation_executor,
299
+ compilation_queue=compilation_queue
300
+ )
301
+
302
+ # Compile Arduino core with progress bar
303
+ if verbose:
304
+ core_obj_files = compiler.compile_core()
305
+ else:
306
+ # Use tqdm progress bar for non-verbose mode
307
+ from tqdm import tqdm
308
+
309
+ # Get number of core source files for progress tracking
310
+ core_sources = framework.get_core_sources(compiler.core)
311
+ total_files = len(core_sources)
312
+
313
+ # Create progress bar
314
+ with tqdm(
315
+ total=total_files,
316
+ desc='Compiling Arduino core',
317
+ unit='file',
318
+ ncols=80,
319
+ leave=False
320
+ ) as pbar:
321
+ core_obj_files = compiler.compile_core(progress_bar=pbar)
322
+
323
+ # Print completion message
324
+ log_detail(f"Compiled {len(core_obj_files)} core files")
325
+
326
+ # Add Bluetooth stub for non-ESP32 targets (ESP32-C6, ESP32-S3, etc.)
327
+ # where esp32-hal-bt.c fails to compile but btInUse() is still referenced
328
+ bt_stub_obj = self._create_bt_stub(build_dir, compiler, verbose)
329
+ if bt_stub_obj:
330
+ core_obj_files.append(bt_stub_obj)
331
+
332
+ core_archive = compiler.create_core_archive(core_obj_files)
333
+
334
+ log_detail(f"Compiled {len(core_obj_files)} core source files", verbose_only=True)
335
+
336
+ # Handle library dependencies
337
+ library_archives, library_include_paths = self._process_libraries(
338
+ env_config, build_dir, compiler, toolchain, verbose, project_dir=project_dir
339
+ )
340
+
341
+ # Add library include paths to compiler
342
+ if library_include_paths:
343
+ compiler.add_library_includes(library_include_paths)
344
+
345
+ # Get src_dir override from platformio.ini
346
+ from ..config import PlatformIOConfig
347
+ config_for_src_dir = PlatformIOConfig(project_dir / "platformio.ini")
348
+ src_dir_override = config_for_src_dir.get_src_dir()
349
+
350
+ # Find and compile sketch
351
+ sketch_obj_files = self._compile_sketch(project_dir, compiler, start_time, verbose, src_dir_override)
352
+ if sketch_obj_files is None:
353
+ search_dir = project_dir / src_dir_override if src_dir_override else project_dir
354
+ return self._error_result(
355
+ start_time,
356
+ f"No .ino sketch file found in {search_dir}"
357
+ )
358
+
359
+ # Initialize linker
360
+ log_phase(9, 12, "Linking firmware...")
361
+
362
+ linker = ConfigurableLinker(
363
+ platform,
364
+ toolchain,
365
+ framework,
366
+ board_id,
367
+ build_dir,
368
+ platform_config=None,
369
+ show_progress=verbose
370
+ )
371
+
372
+ # Link firmware
373
+ firmware_elf = linker.link(sketch_obj_files, core_archive, library_archives=library_archives)
374
+
375
+ # Generate binary
376
+ log_phase(10, 12, "Generating firmware binary...")
377
+
378
+ firmware_bin = linker.generate_bin(firmware_elf)
379
+
380
+ # Generate bootloader and partition table
381
+ bootloader_bin, partitions_bin = self._generate_boot_components(
382
+ linker, mcu, verbose
383
+ )
384
+
385
+ # Get size information from ELF file
386
+ size_info = linker.get_size_info(firmware_elf)
387
+
388
+ build_time = time.time() - start_time
389
+
390
+ if verbose:
391
+ self._print_success(
392
+ build_time, firmware_elf, firmware_bin,
393
+ bootloader_bin, partitions_bin, size_info
394
+ )
395
+
396
+ # Save build state for future cache validation
397
+ log_detail("Saving build state...", verbose_only=True)
398
+ state_tracker.save_state(current_state)
399
+
400
+ # Generate build_info.json
401
+ build_info_generator = BuildInfoGenerator(build_dir)
402
+ board_name = board_json.get("name", board_id)
403
+ # Parse f_cpu from string (e.g., "160000000L" or "160000000") to int
404
+ f_cpu_raw = board_json.get("build", {}).get("f_cpu", "0")
405
+ f_cpu_int = int(str(f_cpu_raw).rstrip("L")) if f_cpu_raw else 0
406
+ # Build toolchain_paths dict, filtering out None values
407
+ toolchain_paths_raw = {
408
+ "gcc": toolchain.get_gcc_path(),
409
+ "gxx": toolchain.get_gxx_path(),
410
+ "ar": toolchain.get_ar_path(),
411
+ "objcopy": toolchain.get_objcopy_path(),
412
+ "size": toolchain.get_size_path(),
413
+ }
414
+ toolchain_paths = {k: v for k, v in toolchain_paths_raw.items() if v is not None}
415
+ build_info = build_info_generator.generate_esp32(
416
+ env_name=env_name,
417
+ board_id=board_id,
418
+ board_name=board_name,
419
+ mcu=mcu,
420
+ f_cpu=f_cpu_int,
421
+ build_time=build_time,
422
+ elf_path=firmware_elf,
423
+ bin_path=firmware_bin,
424
+ size_info=size_info,
425
+ build_flags=build_flags,
426
+ lib_deps=lib_deps,
427
+ toolchain_version=toolchain.version,
428
+ toolchain_paths=toolchain_paths,
429
+ framework_version=framework.version,
430
+ core_path=framework.get_cores_dir(),
431
+ bootloader_path=bootloader_bin,
432
+ partitions_path=partitions_bin,
433
+ application_offset=board_json.get("build", {}).get("app_offset", "0x10000"),
434
+ flash_mode=env_config.get("board_build.flash_mode"),
435
+ flash_size=env_config.get("board_build.flash_size"),
436
+ )
437
+ build_info_generator.save(build_info)
438
+ log_detail(f"Build info saved to {build_info_generator.build_info_path}", verbose_only=True)
439
+
440
+ return BuildResultESP32(
441
+ success=True,
442
+ firmware_bin=firmware_bin,
443
+ firmware_elf=firmware_elf,
444
+ bootloader_bin=bootloader_bin,
445
+ partitions_bin=partitions_bin,
446
+ size_info=size_info,
447
+ build_time=build_time,
448
+ message="Build successful (native ESP32 build)"
449
+ )
450
+
451
+ except KeyboardInterrupt as ke:
452
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
453
+ handle_keyboard_interrupt_properly(ke)
454
+ raise # Never reached, but satisfies type checker
455
+ except Exception as e:
456
+ build_time = time.time() - start_time
457
+ import traceback
458
+ error_trace = traceback.format_exc()
459
+ return BuildResultESP32(
460
+ success=False,
461
+ firmware_bin=None,
462
+ firmware_elf=None,
463
+ bootloader_bin=None,
464
+ partitions_bin=None,
465
+ size_info=None,
466
+ build_time=build_time,
467
+ message=f"ESP32 native build failed: {e}\n\n{error_trace}"
468
+ )
469
+
470
+ def _setup_toolchain(
471
+ self,
472
+ packages: dict,
473
+ start_time: float,
474
+ verbose: bool
475
+ ) -> Optional['ToolchainESP32']:
476
+ """
477
+ Initialize ESP32 toolchain.
478
+
479
+ Args:
480
+ packages: Package URLs dictionary
481
+ start_time: Build start time for error reporting
482
+ verbose: Verbose output mode
483
+
484
+ Returns:
485
+ ToolchainESP32 instance or None on failure
486
+ """
487
+ log_phase(4, 12, "Initializing ESP32 toolchain...")
488
+
489
+ toolchain_url = packages.get("toolchain-riscv32-esp") or packages.get("toolchain-xtensa-esp-elf")
490
+ if not toolchain_url:
491
+ return None
492
+
493
+ # Determine toolchain type
494
+ toolchain_type = "riscv32-esp" if "riscv32" in toolchain_url else "xtensa-esp-elf"
495
+ toolchain = ToolchainESP32(
496
+ self.cache,
497
+ toolchain_url,
498
+ toolchain_type,
499
+ show_progress=True
500
+ )
501
+ toolchain.ensure_toolchain()
502
+ return toolchain
503
+
504
+ def _setup_framework(
505
+ self,
506
+ packages: dict,
507
+ start_time: float,
508
+ verbose: bool
509
+ ) -> Optional[FrameworkESP32]:
510
+ """
511
+ Initialize ESP32 framework.
512
+
513
+ Args:
514
+ packages: Package URLs dictionary
515
+ start_time: Build start time for error reporting
516
+ verbose: Verbose output mode
517
+
518
+ Returns:
519
+ FrameworkESP32 instance or None on failure
520
+ """
521
+ log_phase(5, 12, "Initializing ESP32 framework...")
522
+
523
+ framework_url = packages.get("framework-arduinoespressif32")
524
+ libs_url = packages.get("framework-arduinoespressif32-libs", "")
525
+
526
+ if not framework_url:
527
+ return None
528
+
529
+ # Find skeleton library if present (e.g., framework-arduino-esp32c2-skeleton-lib)
530
+ skeleton_lib_url = None
531
+ for package_name, package_url in packages.items():
532
+ if package_name.startswith("framework-arduino-") and package_name.endswith("-skeleton-lib"):
533
+ skeleton_lib_url = package_url
534
+ break
535
+
536
+ framework = FrameworkESP32(
537
+ self.cache,
538
+ framework_url,
539
+ libs_url,
540
+ skeleton_lib_url=skeleton_lib_url,
541
+ show_progress=True
542
+ )
543
+ framework.ensure_framework()
544
+ return framework
545
+
546
+ def _setup_build_directory(self, env_name: str, clean: bool, verbose: bool) -> Path:
547
+ """
548
+ Setup build directory with optional cleaning.
549
+
550
+ Args:
551
+ env_name: Environment name
552
+ clean: Whether to clean before build
553
+ verbose: Verbose output mode
554
+
555
+ Returns:
556
+ Build directory path
557
+ """
558
+ build_dir = self.cache.get_build_dir(env_name)
559
+
560
+ if clean and build_dir.exists():
561
+ log_phase(6, 12, "Cleaning build directory...")
562
+ safe_rmtree(build_dir)
563
+
564
+ build_dir.mkdir(parents=True, exist_ok=True)
565
+ return build_dir
566
+
567
+ def _process_libraries(
568
+ self,
569
+ env_config: dict,
570
+ build_dir: Path,
571
+ compiler: ConfigurableCompiler,
572
+ toolchain: ToolchainESP32,
573
+ verbose: bool,
574
+ project_dir: Optional[Path] = None
575
+ ) -> tuple[List[Path], List[Path]]:
576
+ """
577
+ Process and compile library dependencies.
578
+
579
+ Args:
580
+ env_config: Environment configuration
581
+ build_dir: Build directory
582
+ compiler: Configured compiler instance
583
+ toolchain: ESP32 toolchain instance
584
+ verbose: Verbose output mode
585
+ project_dir: Optional project directory for resolving relative library paths
586
+
587
+ Returns:
588
+ Tuple of (library_archives, library_include_paths)
589
+ """
590
+ lib_deps = env_config.get('lib_deps', '')
591
+ library_archives = []
592
+ library_include_paths = []
593
+
594
+ if not lib_deps:
595
+ return library_archives, library_include_paths
596
+
597
+ log_phase(8, 12, "Processing library dependencies...", verbose_only=True)
598
+
599
+ # Parse lib_deps (can be string or list)
600
+ if isinstance(lib_deps, str):
601
+ lib_specs = [dep.strip() for dep in lib_deps.split('\n') if dep.strip()]
602
+ else:
603
+ lib_specs = lib_deps
604
+
605
+ if not lib_specs:
606
+ return library_archives, library_include_paths
607
+
608
+ # Initialize library manager with project directory for resolving local paths
609
+ lib_manager = LibraryManagerESP32(build_dir, project_dir=project_dir)
610
+
611
+ # Get compiler flags for library compilation
612
+ lib_compiler_flags = compiler.get_base_flags()
613
+
614
+ # Get include paths for library compilation
615
+ lib_include_paths = compiler.get_include_paths()
616
+
617
+ # Get toolchain bin path
618
+ toolchain_bin_path = toolchain.get_bin_path()
619
+ if toolchain_bin_path is None:
620
+ log_warning("Toolchain bin directory not found, skipping libraries")
621
+ return library_archives, library_include_paths
622
+
623
+ # Ensure libraries are downloaded and compiled
624
+ logger.debug(f"[ORCHESTRATOR] Calling lib_manager.ensure_libraries with {len(lib_specs)} specs: {lib_specs}")
625
+ libraries = lib_manager.ensure_libraries(
626
+ lib_specs,
627
+ toolchain_bin_path,
628
+ lib_compiler_flags,
629
+ lib_include_paths,
630
+ show_progress=verbose
631
+ )
632
+ logger.debug(f"[ORCHESTRATOR] ensure_libraries returned {len(libraries)} libraries")
633
+
634
+ # Get library archives and include paths
635
+ library_archives = [lib.archive_file for lib in libraries if lib.is_compiled]
636
+ library_include_paths = lib_manager.get_library_include_paths()
637
+
638
+ log_detail(f"Compiled {len(libraries)} library dependencies", verbose_only=True)
639
+
640
+ return library_archives, library_include_paths
641
+
642
+ def _compile_sketch(
643
+ self,
644
+ project_dir: Path,
645
+ compiler: ConfigurableCompiler,
646
+ start_time: float,
647
+ verbose: bool,
648
+ src_dir_override: Optional[str] = None
649
+ ) -> Optional[List[Path]]:
650
+ """
651
+ Find and compile sketch files.
652
+
653
+ Args:
654
+ project_dir: Project directory
655
+ compiler: Configured compiler instance
656
+ start_time: Build start time for error reporting
657
+ verbose: Verbose output mode
658
+ src_dir_override: Optional source directory override (relative to project_dir)
659
+
660
+ Returns:
661
+ List of compiled object files or None if no sketch found
662
+ """
663
+ log_phase(8, 12, "Compiling sketch...", verbose_only=True)
664
+
665
+ # Determine source directory
666
+ if src_dir_override:
667
+ src_dir = project_dir / src_dir_override
668
+ log_detail(f"Using source directory override: {src_dir_override}", verbose_only=True)
669
+ else:
670
+ src_dir = project_dir
671
+
672
+ # Look for .ino files in the source directory
673
+ sketch_files = list(src_dir.glob("*.ino"))
674
+ if not sketch_files:
675
+ return None
676
+
677
+ sketch_path = sketch_files[0]
678
+ sketch_obj_files = compiler.compile_sketch(sketch_path)
679
+
680
+ log_detail(f"Compiled {len(sketch_obj_files)} sketch file(s)", verbose_only=True)
681
+
682
+ return sketch_obj_files
683
+
684
+ def _create_bt_stub(
685
+ self,
686
+ build_dir: Path,
687
+ compiler: ConfigurableCompiler,
688
+ verbose: bool
689
+ ) -> Optional[Path]:
690
+ """
691
+ Create a Bluetooth stub for ESP32 targets where esp32-hal-bt.c fails to compile.
692
+
693
+ On non-ESP32 targets (ESP32-C6, ESP32-S3, etc.), the esp32-hal-bt.c file may
694
+ fail to compile due to SDK incompatibilities, but initArduino() still references
695
+ btInUse(). This creates a stub implementation that returns false.
696
+
697
+ Args:
698
+ build_dir: Build directory
699
+ compiler: Configured compiler instance
700
+ verbose: Whether to print verbose output
701
+
702
+ Returns:
703
+ Path to compiled stub object file, or None on error
704
+ """
705
+ try:
706
+ # Create stub source file
707
+ stub_dir = build_dir / "stubs"
708
+ stub_dir.mkdir(parents=True, exist_ok=True)
709
+ stub_file = stub_dir / "bt_stub.c"
710
+
711
+ # Write minimal btInUse() implementation
712
+ stub_content = """// Bluetooth stub for ESP32 targets where esp32-hal-bt.c fails to compile
713
+ // This provides a fallback implementation of btInUse() that always returns false
714
+
715
+ #include <stdbool.h>
716
+
717
+ // Weak attribute allows this to be overridden if the real implementation links
718
+ __attribute__((weak)) bool btInUse(void) {
719
+ return false;
720
+ }
721
+ """
722
+ stub_file.write_text(stub_content)
723
+
724
+ # Compile the stub
725
+ stub_obj = stub_dir / "bt_stub.o"
726
+ compiled_obj = compiler.compile_source(stub_file, stub_obj)
727
+
728
+ log_detail(f"Created Bluetooth stub: {compiled_obj.name}", verbose_only=True)
729
+
730
+ return compiled_obj
731
+
732
+ except KeyboardInterrupt as ke:
733
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
734
+
735
+ handle_keyboard_interrupt_properly(ke)
736
+ raise # Never reached, but satisfies type checker
737
+ except Exception as e:
738
+ log_warning(f"Failed to create Bluetooth stub: {e}")
739
+ return None
740
+
741
+ def _generate_boot_components(
742
+ self,
743
+ linker: ConfigurableLinker,
744
+ mcu: str,
745
+ verbose: bool
746
+ ) -> tuple[Optional[Path], Optional[Path]]:
747
+ """
748
+ Generate bootloader and partition table for ESP32.
749
+
750
+ Args:
751
+ linker: Configured linker instance
752
+ mcu: MCU identifier
753
+ verbose: Verbose output mode
754
+
755
+ Returns:
756
+ Tuple of (bootloader_bin, partitions_bin)
757
+ """
758
+ bootloader_bin = None
759
+ partitions_bin = None
760
+
761
+ if not mcu.startswith("esp32"):
762
+ return bootloader_bin, partitions_bin
763
+
764
+ log_phase(11, 12, "Generating bootloader...", verbose_only=True)
765
+ try:
766
+ bootloader_bin = linker.generate_bootloader()
767
+ except KeyboardInterrupt as ke:
768
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
769
+ handle_keyboard_interrupt_properly(ke)
770
+ raise # Never reached, but satisfies type checker
771
+ except Exception as e:
772
+ log_warning(f"Could not generate bootloader: {e}")
773
+
774
+ log_phase(12, 12, "Generating partition table...", verbose_only=True)
775
+ try:
776
+ partitions_bin = linker.generate_partition_table()
777
+ except KeyboardInterrupt as ke:
778
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
779
+ handle_keyboard_interrupt_properly(ke)
780
+ raise # Never reached, but satisfies type checker
781
+ except Exception as e:
782
+ log_warning(f"Could not generate partition table: {e}")
783
+
784
+ return bootloader_bin, partitions_bin
785
+
786
+ def _print_success(
787
+ self,
788
+ build_time: float,
789
+ firmware_elf: Path,
790
+ firmware_bin: Path,
791
+ bootloader_bin: Optional[Path],
792
+ partitions_bin: Optional[Path],
793
+ size_info: Optional[SizeInfo] = None
794
+ ) -> None:
795
+ """
796
+ Print build success message.
797
+
798
+ Args:
799
+ build_time: Total build time
800
+ firmware_elf: Path to firmware ELF
801
+ firmware_bin: Path to firmware binary
802
+ bootloader_bin: Optional path to bootloader
803
+ partitions_bin: Optional path to partition table
804
+ size_info: Optional size information to display
805
+ """
806
+ # Build success message
807
+ message_lines = ["BUILD SUCCESSFUL!"]
808
+ message_lines.append(f"Build time: {build_time:.2f}s")
809
+ message_lines.append(f"Firmware ELF: {firmware_elf}")
810
+ message_lines.append(f"Firmware BIN: {firmware_bin}")
811
+ if bootloader_bin:
812
+ message_lines.append(f"Bootloader: {bootloader_bin}")
813
+ if partitions_bin:
814
+ message_lines.append(f"Partitions: {partitions_bin}")
815
+
816
+ BannerFormatter.print_banner("\n".join(message_lines), width=60, center=False)
817
+
818
+ # Print size information if available
819
+ if size_info:
820
+ print()
821
+ from .build_utils import SizeInfoPrinter
822
+ SizeInfoPrinter.print_size_info(size_info)
823
+ print()
824
+
825
+ def _error_result(self, start_time: float, message: str) -> BuildResultESP32:
826
+ """
827
+ Create an error result.
828
+
829
+ Args:
830
+ start_time: Build start time
831
+ message: Error message
832
+
833
+ Returns:
834
+ BuildResultESP32 indicating failure
835
+ """
836
+ return BuildResultESP32(
837
+ success=False,
838
+ firmware_bin=None,
839
+ firmware_elf=None,
840
+ bootloader_bin=None,
841
+ partitions_bin=None,
842
+ size_info=None,
843
+ build_time=time.time() - start_time,
844
+ message=message
845
+ )
846
+
847
+ @staticmethod
848
+ def _resolve_platform_url(platform_spec: str) -> str:
849
+ """
850
+ Resolve platform specification to actual download URL.
851
+
852
+ PlatformIO supports several formats for specifying platforms:
853
+ - Full URL: "https://github.com/.../platform-espressif32.zip" -> used as-is
854
+ - Shorthand: "platformio/espressif32" -> resolved to pioarduino stable release
855
+ - Name only: "espressif32" -> resolved to pioarduino stable release
856
+
857
+ Args:
858
+ platform_spec: Platform specification from platformio.ini
859
+
860
+ Returns:
861
+ Actual download URL for the platform
862
+ """
863
+ # Default stable release URL for espressif32 (pioarduino fork)
864
+ # This is the recommended platform for ESP32 Arduino development
865
+ DEFAULT_ESP32_URL = "https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip"
866
+
867
+ # If it's already a proper URL, use it as-is
868
+ if platform_spec.startswith("http://") or platform_spec.startswith("https://"):
869
+ return platform_spec
870
+
871
+ # Handle PlatformIO shorthand formats
872
+ if platform_spec in ("platformio/espressif32", "espressif32"):
873
+ log_detail(f"Resolving platform shorthand '{platform_spec}' to pioarduino stable release")
874
+ return DEFAULT_ESP32_URL
875
+
876
+ # For unknown formats, return as-is and let the download fail with a clear error
877
+ log_warning(f"Unknown platform format: {platform_spec}, attempting to use as URL")
878
+ return platform_spec