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,664 @@
1
+ """Configurable Compiler.
2
+
3
+ This module provides a generic, configuration-driven compiler that can compile
4
+ for any platform (ESP32, AVR, etc.) based on platform configuration files.
5
+
6
+ Design:
7
+ - Loads compilation flags, includes, and settings from JSON/Python config
8
+ - Generic implementation replaces platform-specific compiler classes
9
+ - Same interface as ESP32Compiler for drop-in replacement
10
+ """
11
+
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Any, List, Dict, Optional, Union, TYPE_CHECKING
15
+
16
+ from ..packages.package import IPackage, IToolchain, IFramework
17
+ from .flag_builder import FlagBuilder
18
+ from .compilation_executor import CompilationExecutor
19
+ from .archive_creator import ArchiveCreator
20
+ from .compiler import ICompiler, CompilerError
21
+
22
+ if TYPE_CHECKING:
23
+ from ..daemon.compilation_queue import CompilationJobQueue
24
+
25
+
26
+ class ConfigurableCompilerError(CompilerError):
27
+ """Raised when configurable compilation operations fail."""
28
+ pass
29
+
30
+
31
+ class ConfigurableCompiler(ICompiler):
32
+ """Generic compiler driven by platform configuration.
33
+
34
+ This class handles:
35
+ - Loading platform-specific config from JSON
36
+ - Source file compilation with configured flags
37
+ - Object file generation
38
+ - Core archive creation
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ platform: IPackage,
44
+ toolchain: IToolchain,
45
+ framework: IFramework,
46
+ board_id: str,
47
+ build_dir: Path,
48
+ platform_config: Optional[Union[Dict, Path]] = None,
49
+ show_progress: bool = True,
50
+ user_build_flags: Optional[List[str]] = None,
51
+ compilation_executor: Optional[CompilationExecutor] = None,
52
+ compilation_queue: Optional['CompilationJobQueue'] = None
53
+ ):
54
+ """Initialize configurable compiler.
55
+
56
+ Args:
57
+ platform: Platform instance
58
+ toolchain: Toolchain instance
59
+ framework: Framework instance
60
+ board_id: Board identifier (e.g., "esp32-c6-devkitm-1")
61
+ build_dir: Directory for build artifacts
62
+ platform_config: Platform config dict or path to config JSON file
63
+ show_progress: Whether to show compilation progress
64
+ user_build_flags: Build flags from platformio.ini
65
+ compilation_executor: Optional pre-initialized CompilationExecutor
66
+ compilation_queue: Optional compilation queue for async/parallel compilation
67
+ """
68
+ self.platform = platform
69
+ self.toolchain = toolchain
70
+ self.framework = framework
71
+ self.board_id = board_id
72
+ self.build_dir = build_dir
73
+ self.show_progress = show_progress
74
+ self.user_build_flags = user_build_flags or []
75
+ self.compilation_queue = compilation_queue
76
+ self.pending_jobs: List[str] = [] # Track async job IDs
77
+
78
+ # Load board configuration
79
+ self.board_config = platform.get_board_json(board_id) # type: ignore[attr-defined]
80
+
81
+ # Get MCU type from board config
82
+ self.mcu = self.board_config.get("build", {}).get("mcu", "").lower()
83
+
84
+ # Get variant name
85
+ self.variant = self.board_config.get("build", {}).get("variant", "")
86
+
87
+ # Get core name from board config (defaults to "arduino" if not specified)
88
+ self.core = self.board_config.get("build", {}).get("core", "arduino")
89
+
90
+ # Load platform configuration
91
+ if platform_config is None:
92
+ # Try to load from default location
93
+ config_path = Path(__file__).parent.parent / "platform_configs" / f"{self.mcu}.json"
94
+ if config_path.exists():
95
+ with open(config_path, 'r') as f:
96
+ self.config = json.load(f)
97
+ else:
98
+ raise ConfigurableCompilerError(
99
+ f"No platform configuration found for {self.mcu}. " +
100
+ f"Expected: {config_path}"
101
+ )
102
+ elif isinstance(platform_config, dict):
103
+ self.config = platform_config
104
+ else:
105
+ # Assume it's a path
106
+ with open(platform_config, 'r') as f:
107
+ self.config = json.load(f)
108
+
109
+ # Initialize utility components
110
+ self.flag_builder = FlagBuilder(
111
+ config=self.config,
112
+ board_config=self.board_config,
113
+ board_id=self.board_id,
114
+ variant=self.variant,
115
+ user_build_flags=self.user_build_flags
116
+ )
117
+ # Use provided executor or create a new one
118
+ if compilation_executor is not None:
119
+ self.compilation_executor = compilation_executor
120
+ else:
121
+ self.compilation_executor = CompilationExecutor(
122
+ build_dir=self.build_dir,
123
+ show_progress=self.show_progress
124
+ )
125
+ self.archive_creator = ArchiveCreator(show_progress=self.show_progress)
126
+
127
+ # Cache for include paths
128
+ self._include_paths_cache: Optional[List[Path]] = None
129
+
130
+ def get_compile_flags(self) -> Dict[str, List[str]]:
131
+ """Get compilation flags from configuration.
132
+
133
+ Returns:
134
+ Dictionary with 'cflags', 'cxxflags', and 'common' keys
135
+ """
136
+ return self.flag_builder.build_flags()
137
+
138
+ def get_include_paths(self) -> List[Path]:
139
+ """Get all include paths needed for compilation.
140
+
141
+ Returns:
142
+ List of include directory paths
143
+ """
144
+ if self._include_paths_cache is not None:
145
+ return self._include_paths_cache
146
+
147
+ includes = []
148
+
149
+ # Core include path
150
+ core_dir = self.framework.get_core_dir(self.core) # type: ignore[attr-defined]
151
+ includes.append(core_dir)
152
+
153
+ # Variant include path
154
+ try:
155
+ variant_dir = self.framework.get_variant_dir(self.variant) # type: ignore[attr-defined]
156
+ includes.append(variant_dir)
157
+ except KeyboardInterrupt as ke:
158
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
159
+ handle_keyboard_interrupt_properly(ke)
160
+ raise # Never reached, but satisfies type checker
161
+ except Exception:
162
+ pass
163
+
164
+ # SDK include paths (ESP32-specific)
165
+ if hasattr(self.framework, 'get_sdk_includes'):
166
+ sdk_includes = self.framework.get_sdk_includes(self.mcu) # type: ignore[attr-defined]
167
+ includes.extend(sdk_includes)
168
+
169
+ # STM32-specific system includes (CMSIS, HAL)
170
+ if hasattr(self.framework, 'get_stm32_system_includes'):
171
+ # Determine MCU family from MCU name
172
+ mcu_upper = self.mcu.upper()
173
+ if mcu_upper.startswith("STM32F0"):
174
+ mcu_family = "STM32F0xx"
175
+ elif mcu_upper.startswith("STM32F1"):
176
+ mcu_family = "STM32F1xx"
177
+ elif mcu_upper.startswith("STM32F2"):
178
+ mcu_family = "STM32F2xx"
179
+ elif mcu_upper.startswith("STM32F3"):
180
+ mcu_family = "STM32F3xx"
181
+ elif mcu_upper.startswith("STM32F4"):
182
+ mcu_family = "STM32F4xx"
183
+ elif mcu_upper.startswith("STM32F7"):
184
+ mcu_family = "STM32F7xx"
185
+ elif mcu_upper.startswith("STM32G0"):
186
+ mcu_family = "STM32G0xx"
187
+ elif mcu_upper.startswith("STM32G4"):
188
+ mcu_family = "STM32G4xx"
189
+ elif mcu_upper.startswith("STM32H7"):
190
+ mcu_family = "STM32H7xx"
191
+ elif mcu_upper.startswith("STM32L0"):
192
+ mcu_family = "STM32L0xx"
193
+ elif mcu_upper.startswith("STM32L1"):
194
+ mcu_family = "STM32L1xx"
195
+ elif mcu_upper.startswith("STM32L4"):
196
+ mcu_family = "STM32L4xx"
197
+ elif mcu_upper.startswith("STM32L5"):
198
+ mcu_family = "STM32L5xx"
199
+ elif mcu_upper.startswith("STM32U5"):
200
+ mcu_family = "STM32U5xx"
201
+ elif mcu_upper.startswith("STM32WB"):
202
+ mcu_family = "STM32WBxx"
203
+ elif mcu_upper.startswith("STM32WL"):
204
+ mcu_family = "STM32WLxx"
205
+ else:
206
+ mcu_family = "STM32F4xx" # Default fallback
207
+ system_includes = self.framework.get_stm32_system_includes(mcu_family) # type: ignore[attr-defined]
208
+ includes.extend(system_includes)
209
+
210
+ # Add flash mode specific sdkconfig.h path (ESP32-specific)
211
+ if hasattr(self.framework, 'get_sdk_dir'):
212
+ flash_mode = self.board_config.get("build", {}).get("flash_mode", "qio")
213
+ sdk_dir = self.framework.get_sdk_dir() # type: ignore[attr-defined]
214
+
215
+ # Apply SDK fallback for MCUs not fully supported in the platform
216
+ # (e.g., esp32c2 can use esp32c3 SDK)
217
+ from ..packages.sdk_utils import SDKPathResolver
218
+ resolver = SDKPathResolver(sdk_dir, show_progress=False)
219
+ resolved_mcu = resolver._resolve_mcu(self.mcu)
220
+
221
+ flash_config_dir = sdk_dir / resolved_mcu / f"{flash_mode}_qspi" / "include"
222
+ if flash_config_dir.exists():
223
+ includes.append(flash_config_dir)
224
+
225
+ # Add Arduino built-in libraries (e.g., SPI, Wire, WiFi) for ESP32
226
+ if hasattr(self.framework, 'get_libraries_dir'):
227
+ libs_dir = self.framework.get_libraries_dir()
228
+ if libs_dir.exists():
229
+ # Add src subdirectory of each built-in library
230
+ for lib_entry in libs_dir.iterdir():
231
+ if lib_entry.is_dir() and not lib_entry.name.startswith("."):
232
+ lib_src = lib_entry / "src"
233
+ if lib_src.exists():
234
+ includes.append(lib_src)
235
+
236
+ self._include_paths_cache = includes
237
+ return includes
238
+
239
+ def preprocess_ino(self, ino_path: Path) -> Path:
240
+ """Preprocess .ino file to .cpp file.
241
+
242
+ Args:
243
+ ino_path: Path to .ino file
244
+
245
+ Returns:
246
+ Path to generated .cpp file
247
+
248
+ Raises:
249
+ ConfigurableCompilerError: If preprocessing fails
250
+ """
251
+ try:
252
+ return self.compilation_executor.preprocess_ino(ino_path, self.build_dir)
253
+ except KeyboardInterrupt as ke:
254
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
255
+ handle_keyboard_interrupt_properly(ke)
256
+ raise # Never reached, but satisfies type checker
257
+ except Exception as e:
258
+ raise ConfigurableCompilerError(str(e))
259
+
260
+ def compile_source(
261
+ self,
262
+ source_path: Path,
263
+ output_path: Optional[Path] = None
264
+ ) -> Path:
265
+ """Compile a single source file to object file.
266
+
267
+ Args:
268
+ source_path: Path to .c or .cpp source file
269
+ output_path: Optional path for output .o file
270
+
271
+ Returns:
272
+ Path to generated .o file
273
+
274
+ Raises:
275
+ ConfigurableCompilerError: If compilation fails
276
+ """
277
+ # Determine compiler based on file extension
278
+ is_cpp = source_path.suffix in ['.cpp', '.cxx', '.cc']
279
+ compiler_path = self.toolchain.get_gxx_path() if is_cpp else self.toolchain.get_gcc_path()
280
+
281
+ if compiler_path is None:
282
+ raise ConfigurableCompilerError(
283
+ f"Compiler path not found for {'C++' if is_cpp else 'C'} compilation"
284
+ )
285
+
286
+ # Generate output path if not provided
287
+ if output_path is None:
288
+ obj_dir = self.build_dir / "obj"
289
+ obj_dir.mkdir(parents=True, exist_ok=True)
290
+ output_path = obj_dir / f"{source_path.stem}.o"
291
+
292
+ # Get compilation flags
293
+ flags = self.get_compile_flags()
294
+ compile_flags = flags['common'].copy()
295
+ if is_cpp:
296
+ compile_flags.extend(flags['cxxflags'])
297
+ else:
298
+ compile_flags.extend(flags['cflags'])
299
+
300
+ # Get include paths
301
+ includes = self.get_include_paths()
302
+
303
+ # Async mode: submit to queue and return immediately
304
+ if self.compilation_queue is not None:
305
+ # Convert include paths to flags
306
+ include_flags = [f"-I{str(inc).replace(chr(92), '/')}" for inc in includes]
307
+ # Build command that would be executed
308
+ cmd = self.compilation_executor._build_compile_command(
309
+ compiler_path, source_path, output_path, compile_flags, include_flags
310
+ )
311
+
312
+ # Submit to async compilation queue
313
+ job_id = self._submit_async_compilation(source_path, output_path, cmd)
314
+ self.pending_jobs.append(job_id)
315
+
316
+ # Return output path optimistically (validated in wait_all_jobs())
317
+ return output_path
318
+
319
+ # Sync mode: compile using executor (legacy behavior)
320
+ try:
321
+ return self.compilation_executor.compile_source(
322
+ compiler_path=compiler_path,
323
+ source_path=source_path,
324
+ output_path=output_path,
325
+ compile_flags=compile_flags,
326
+ include_paths=includes
327
+ )
328
+ except KeyboardInterrupt as ke:
329
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
330
+ handle_keyboard_interrupt_properly(ke)
331
+ raise # Never reached, but satisfies type checker
332
+ except Exception as e:
333
+ raise ConfigurableCompilerError(str(e))
334
+
335
+ def compile_sketch(self, sketch_path: Path) -> List[Path]:
336
+ """Compile an Arduino sketch.
337
+
338
+ Args:
339
+ sketch_path: Path to .ino file
340
+
341
+ Returns:
342
+ List of generated object file paths
343
+
344
+ Raises:
345
+ ConfigurableCompilerError: If compilation fails
346
+ """
347
+ object_files = []
348
+
349
+ # Preprocess .ino to .cpp
350
+ cpp_path = self.preprocess_ino(sketch_path)
351
+
352
+ # Determine object file path
353
+ obj_dir = self.build_dir / "obj"
354
+ obj_dir.mkdir(parents=True, exist_ok=True)
355
+ obj_path = obj_dir / f"{cpp_path.stem}.o"
356
+
357
+ # Skip compilation if object file is up-to-date
358
+ if not self.needs_rebuild(cpp_path, obj_path):
359
+ object_files.append(obj_path)
360
+ return object_files
361
+
362
+ # Compile preprocessed .cpp
363
+ compiled_obj = self.compile_source(cpp_path, obj_path)
364
+ object_files.append(compiled_obj)
365
+
366
+ return object_files
367
+
368
+ def compile_core(self, progress_bar: Optional[Any] = None) -> List[Path]:
369
+ """Compile Arduino core sources.
370
+
371
+ Args:
372
+ progress_bar: Optional tqdm progress bar to update during compilation
373
+
374
+ Returns:
375
+ List of generated object file paths
376
+
377
+ Raises:
378
+ ConfigurableCompilerError: If compilation fails
379
+ """
380
+ object_files = []
381
+
382
+ # Get core sources
383
+ core_sources = self.framework.get_core_sources(self.core) # type: ignore[attr-defined]
384
+
385
+ if self.show_progress:
386
+ print(f"Compiling {len(core_sources)} core source files...")
387
+
388
+ # Create core object directory
389
+ core_obj_dir = self.build_dir / "obj" / "core"
390
+ core_obj_dir.mkdir(parents=True, exist_ok=True)
391
+
392
+ # Disable individual file progress messages when using progress bar
393
+ original_show_progress = self.compilation_executor.show_progress
394
+ if progress_bar is not None:
395
+ self.compilation_executor.show_progress = False
396
+
397
+ try:
398
+ # Compile each core source
399
+ for source in core_sources:
400
+ # Update progress bar BEFORE compilation for better UX
401
+ if progress_bar is not None:
402
+ progress_bar.set_description(f'Compiling {source.name[:30]}')
403
+
404
+ try:
405
+ obj_path = core_obj_dir / f"{source.stem}.o"
406
+
407
+ # Skip compilation if object file is up-to-date
408
+ if not self.needs_rebuild(source, obj_path):
409
+ object_files.append(obj_path)
410
+ if progress_bar is not None:
411
+ progress_bar.update(1)
412
+ continue
413
+
414
+ compiled_obj = self.compile_source(source, obj_path)
415
+ object_files.append(compiled_obj)
416
+ if progress_bar is not None:
417
+ progress_bar.update(1)
418
+ except ConfigurableCompilerError as e:
419
+ if self.show_progress:
420
+ print(f"Warning: Failed to compile {source.name}: {e}")
421
+ if progress_bar is not None:
422
+ progress_bar.update(1)
423
+ finally:
424
+ # Restore original show_progress setting
425
+ self.compilation_executor.show_progress = original_show_progress
426
+
427
+ # Wait for all async jobs to complete (if using async mode)
428
+ if hasattr(self, 'wait_all_jobs'):
429
+ try:
430
+ self.wait_all_jobs()
431
+ except ConfigurableCompilerError as e:
432
+ raise ConfigurableCompilerError(f"Core compilation failed: {e}")
433
+
434
+ return object_files
435
+
436
+ def create_core_archive(self, object_files: List[Path]) -> Path:
437
+ """Create core.a archive from compiled object files.
438
+
439
+ Args:
440
+ object_files: List of object file paths to archive
441
+
442
+ Returns:
443
+ Path to generated core.a file
444
+
445
+ Raises:
446
+ ConfigurableCompilerError: If archive creation fails
447
+ """
448
+ # Get archiver tool
449
+ ar_path = self.toolchain.get_ar_path()
450
+
451
+ if ar_path is None:
452
+ raise ConfigurableCompilerError("Archiver (ar) path not found")
453
+
454
+ # Create archive using creator
455
+ try:
456
+ return self.archive_creator.create_core_archive(
457
+ ar_path=ar_path,
458
+ build_dir=self.build_dir,
459
+ object_files=object_files
460
+ )
461
+ except KeyboardInterrupt as ke:
462
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
463
+ handle_keyboard_interrupt_properly(ke)
464
+ raise # Never reached, but satisfies type checker
465
+ except Exception as e:
466
+ raise ConfigurableCompilerError(str(e))
467
+
468
+ def get_compiler_info(self) -> Dict[str, Any]:
469
+ """Get information about the compiler configuration.
470
+
471
+ Returns:
472
+ Dictionary with compiler information
473
+ """
474
+ info = {
475
+ 'board_id': self.board_id,
476
+ 'mcu': self.mcu,
477
+ 'variant': self.variant,
478
+ 'build_dir': str(self.build_dir),
479
+ 'toolchain_type': self.toolchain.toolchain_type, # type: ignore[attr-defined]
480
+ 'gcc_path': str(self.toolchain.get_gcc_path()),
481
+ 'gxx_path': str(self.toolchain.get_gxx_path()),
482
+ }
483
+
484
+ # Add compile flags
485
+ flags = self.get_compile_flags()
486
+ info['compile_flags'] = flags
487
+
488
+ # Add include paths
489
+ includes = self.get_include_paths()
490
+ info['include_paths'] = [str(p) for p in includes]
491
+ info['include_count'] = len(includes)
492
+
493
+ return info
494
+
495
+ def get_base_flags(self) -> List[str]:
496
+ """Get base compiler flags for library compilation.
497
+
498
+ Returns:
499
+ List of compiler flags
500
+ """
501
+ return self.flag_builder.get_base_flags_for_library()
502
+
503
+ def add_library_includes(self, library_includes: List[Path]) -> None:
504
+ """Add library include paths to the compiler.
505
+
506
+ Args:
507
+ library_includes: List of library include directory paths
508
+ """
509
+ if self._include_paths_cache is not None:
510
+ self._include_paths_cache.extend(library_includes)
511
+
512
+ def needs_rebuild(self, source: Path, object_file: Path) -> bool:
513
+ """Check if source file needs to be recompiled.
514
+
515
+ Args:
516
+ source: Source file path
517
+ object_file: Object file path
518
+
519
+ Returns:
520
+ True if source is newer than object file or object doesn't exist
521
+ """
522
+ if not object_file.exists():
523
+ return True
524
+
525
+ source_mtime = source.stat().st_mtime
526
+ object_mtime = object_file.stat().st_mtime
527
+
528
+ return source_mtime > object_mtime
529
+
530
+ def _submit_async_compilation(
531
+ self,
532
+ source: Path,
533
+ output: Path,
534
+ cmd: List[str]
535
+ ) -> str:
536
+ """
537
+ Submit compilation job to async queue.
538
+
539
+ Args:
540
+ source: Source file path
541
+ output: Output object file path
542
+ cmd: Full compiler command
543
+
544
+ Returns:
545
+ Job ID for tracking
546
+ """
547
+ import time
548
+ from ..daemon.compilation_queue import CompilationJob
549
+
550
+ job_id = f"compile_{source.stem}_{int(time.time() * 1000000)}"
551
+
552
+ job = CompilationJob(
553
+ job_id=job_id,
554
+ source_path=source,
555
+ output_path=output,
556
+ compiler_cmd=cmd,
557
+ response_file=None # ConfigurableCompiler doesn't use response files
558
+ )
559
+
560
+ if self.compilation_queue is None:
561
+ raise ConfigurableCompilerError("Compilation queue not initialized")
562
+ self.compilation_queue.submit_job(job)
563
+ return job_id
564
+
565
+ def wait_all_jobs(self) -> None:
566
+ """
567
+ Wait for all pending async compilation jobs to complete.
568
+
569
+ This method must be called after using async compilation mode
570
+ to wait for all submitted jobs and validate their results.
571
+
572
+ Raises:
573
+ ConfigurableCompilerError: If any compilation fails
574
+ """
575
+ if not self.compilation_queue:
576
+ return
577
+
578
+ if not self.pending_jobs:
579
+ return
580
+
581
+ # Wait for all jobs to complete
582
+ self.compilation_queue.wait_for_completion(self.pending_jobs)
583
+
584
+ # Collect failed jobs
585
+ failed_jobs = []
586
+
587
+ for job_id in self.pending_jobs:
588
+ job = self.compilation_queue.get_job_status(job_id)
589
+
590
+ if job is None:
591
+ # This shouldn't happen
592
+ failed_jobs.append(f"Job {job_id} not found")
593
+ continue
594
+
595
+ if job.state.value != "completed":
596
+ failed_jobs.append(f"{job.source_path.name}: {job.stderr[:200]}")
597
+
598
+ # Clear pending jobs
599
+ self.pending_jobs.clear()
600
+
601
+ # Raise error if any jobs failed
602
+ if failed_jobs:
603
+ error_msg = f"Compilation failed for {len(failed_jobs)} file(s):\n"
604
+ error_msg += "\n".join(f" - {err}" for err in failed_jobs[:5])
605
+ if len(failed_jobs) > 5:
606
+ error_msg += f"\n ... and {len(failed_jobs) - 5} more"
607
+ raise ConfigurableCompilerError(error_msg)
608
+
609
+ def get_statistics(self) -> Dict[str, int]:
610
+ """
611
+ Get compilation statistics from the queue.
612
+
613
+ Returns:
614
+ Dictionary with compilation statistics
615
+ """
616
+ if not self.compilation_queue:
617
+ return {
618
+ "total_jobs": 0,
619
+ "pending": 0,
620
+ "running": 0,
621
+ "completed": 0,
622
+ "failed": 0
623
+ }
624
+
625
+ return self.compilation_queue.get_statistics()
626
+
627
+ def compile(
628
+ self,
629
+ source: Path,
630
+ output: Path,
631
+ extra_flags: Optional[List[str]] = None
632
+ ):
633
+ """Compile source file (auto-detects C vs C++).
634
+
635
+ Args:
636
+ source: Path to source file
637
+ output: Path to output .o object file
638
+ extra_flags: Additional compiler flags
639
+
640
+ Returns:
641
+ CompileResult with compilation status
642
+
643
+ Raises:
644
+ ConfigurableCompilerError: If compilation fails
645
+ """
646
+ from .compiler import CompileResult # Import here to avoid circular dependency
647
+
648
+ try:
649
+ obj_path = self.compile_source(source, output)
650
+ return CompileResult(
651
+ success=True,
652
+ object_file=obj_path,
653
+ stdout="",
654
+ stderr="",
655
+ returncode=0
656
+ )
657
+ except ConfigurableCompilerError as e:
658
+ return CompileResult(
659
+ success=False,
660
+ object_file=None,
661
+ stdout="",
662
+ stderr=str(e),
663
+ returncode=1
664
+ )