fbuild 1.1.0__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.

Potentially problematic release.


This version of fbuild might be problematic. Click here for more details.

Files changed (93) hide show
  1. fbuild/__init__.py +0 -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_state.py +325 -0
  8. fbuild/build/build_utils.py +98 -0
  9. fbuild/build/compilation_executor.py +422 -0
  10. fbuild/build/compiler.py +165 -0
  11. fbuild/build/compiler_avr.py +574 -0
  12. fbuild/build/configurable_compiler.py +612 -0
  13. fbuild/build/configurable_linker.py +637 -0
  14. fbuild/build/flag_builder.py +186 -0
  15. fbuild/build/library_dependency_processor.py +185 -0
  16. fbuild/build/linker.py +708 -0
  17. fbuild/build/orchestrator.py +67 -0
  18. fbuild/build/orchestrator_avr.py +656 -0
  19. fbuild/build/orchestrator_esp32.py +797 -0
  20. fbuild/build/orchestrator_teensy.py +543 -0
  21. fbuild/build/source_compilation_orchestrator.py +220 -0
  22. fbuild/build/source_scanner.py +516 -0
  23. fbuild/cli.py +566 -0
  24. fbuild/cli_utils.py +312 -0
  25. fbuild/config/__init__.py +16 -0
  26. fbuild/config/board_config.py +457 -0
  27. fbuild/config/board_loader.py +92 -0
  28. fbuild/config/ini_parser.py +209 -0
  29. fbuild/config/mcu_specs.py +88 -0
  30. fbuild/daemon/__init__.py +34 -0
  31. fbuild/daemon/client.py +929 -0
  32. fbuild/daemon/compilation_queue.py +293 -0
  33. fbuild/daemon/daemon.py +474 -0
  34. fbuild/daemon/daemon_context.py +196 -0
  35. fbuild/daemon/error_collector.py +263 -0
  36. fbuild/daemon/file_cache.py +332 -0
  37. fbuild/daemon/lock_manager.py +270 -0
  38. fbuild/daemon/logging_utils.py +149 -0
  39. fbuild/daemon/messages.py +301 -0
  40. fbuild/daemon/operation_registry.py +288 -0
  41. fbuild/daemon/process_tracker.py +366 -0
  42. fbuild/daemon/processors/__init__.py +12 -0
  43. fbuild/daemon/processors/build_processor.py +157 -0
  44. fbuild/daemon/processors/deploy_processor.py +327 -0
  45. fbuild/daemon/processors/monitor_processor.py +146 -0
  46. fbuild/daemon/request_processor.py +401 -0
  47. fbuild/daemon/status_manager.py +216 -0
  48. fbuild/daemon/subprocess_manager.py +316 -0
  49. fbuild/deploy/__init__.py +17 -0
  50. fbuild/deploy/deployer.py +67 -0
  51. fbuild/deploy/deployer_esp32.py +314 -0
  52. fbuild/deploy/monitor.py +495 -0
  53. fbuild/interrupt_utils.py +34 -0
  54. fbuild/packages/__init__.py +53 -0
  55. fbuild/packages/archive_utils.py +1098 -0
  56. fbuild/packages/arduino_core.py +412 -0
  57. fbuild/packages/cache.py +249 -0
  58. fbuild/packages/downloader.py +366 -0
  59. fbuild/packages/framework_esp32.py +538 -0
  60. fbuild/packages/framework_teensy.py +346 -0
  61. fbuild/packages/github_utils.py +96 -0
  62. fbuild/packages/header_trampoline_cache.py +394 -0
  63. fbuild/packages/library_compiler.py +203 -0
  64. fbuild/packages/library_manager.py +549 -0
  65. fbuild/packages/library_manager_esp32.py +413 -0
  66. fbuild/packages/package.py +163 -0
  67. fbuild/packages/platform_esp32.py +383 -0
  68. fbuild/packages/platform_teensy.py +312 -0
  69. fbuild/packages/platform_utils.py +131 -0
  70. fbuild/packages/platformio_registry.py +325 -0
  71. fbuild/packages/sdk_utils.py +231 -0
  72. fbuild/packages/toolchain.py +436 -0
  73. fbuild/packages/toolchain_binaries.py +196 -0
  74. fbuild/packages/toolchain_esp32.py +484 -0
  75. fbuild/packages/toolchain_metadata.py +185 -0
  76. fbuild/packages/toolchain_teensy.py +404 -0
  77. fbuild/platform_configs/esp32.json +150 -0
  78. fbuild/platform_configs/esp32c2.json +144 -0
  79. fbuild/platform_configs/esp32c3.json +143 -0
  80. fbuild/platform_configs/esp32c5.json +151 -0
  81. fbuild/platform_configs/esp32c6.json +151 -0
  82. fbuild/platform_configs/esp32p4.json +149 -0
  83. fbuild/platform_configs/esp32s3.json +151 -0
  84. fbuild/platform_configs/imxrt1062.json +56 -0
  85. fbuild-1.1.0.dist-info/METADATA +447 -0
  86. fbuild-1.1.0.dist-info/RECORD +93 -0
  87. fbuild-1.1.0.dist-info/WHEEL +5 -0
  88. fbuild-1.1.0.dist-info/entry_points.txt +5 -0
  89. fbuild-1.1.0.dist-info/licenses/LICENSE +21 -0
  90. fbuild-1.1.0.dist-info/top_level.txt +2 -0
  91. fbuild_lint/__init__.py +0 -0
  92. fbuild_lint/ruff_plugins/__init__.py +0 -0
  93. fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
@@ -0,0 +1,612 @@
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
+ # Add flash mode specific sdkconfig.h path (ESP32-specific)
170
+ if hasattr(self.framework, 'get_sdk_dir'):
171
+ flash_mode = self.board_config.get("build", {}).get("flash_mode", "qio")
172
+ sdk_dir = self.framework.get_sdk_dir() # type: ignore[attr-defined]
173
+
174
+ # Apply SDK fallback for MCUs not fully supported in the platform
175
+ # (e.g., esp32c2 can use esp32c3 SDK)
176
+ from ..packages.sdk_utils import SDKPathResolver
177
+ resolver = SDKPathResolver(sdk_dir, show_progress=False)
178
+ resolved_mcu = resolver._resolve_mcu(self.mcu)
179
+
180
+ flash_config_dir = sdk_dir / resolved_mcu / f"{flash_mode}_qspi" / "include"
181
+ if flash_config_dir.exists():
182
+ includes.append(flash_config_dir)
183
+
184
+ self._include_paths_cache = includes
185
+ return includes
186
+
187
+ def preprocess_ino(self, ino_path: Path) -> Path:
188
+ """Preprocess .ino file to .cpp file.
189
+
190
+ Args:
191
+ ino_path: Path to .ino file
192
+
193
+ Returns:
194
+ Path to generated .cpp file
195
+
196
+ Raises:
197
+ ConfigurableCompilerError: If preprocessing fails
198
+ """
199
+ try:
200
+ return self.compilation_executor.preprocess_ino(ino_path, self.build_dir)
201
+ except KeyboardInterrupt as ke:
202
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
203
+ handle_keyboard_interrupt_properly(ke)
204
+ raise # Never reached, but satisfies type checker
205
+ except Exception as e:
206
+ raise ConfigurableCompilerError(str(e))
207
+
208
+ def compile_source(
209
+ self,
210
+ source_path: Path,
211
+ output_path: Optional[Path] = None
212
+ ) -> Path:
213
+ """Compile a single source file to object file.
214
+
215
+ Args:
216
+ source_path: Path to .c or .cpp source file
217
+ output_path: Optional path for output .o file
218
+
219
+ Returns:
220
+ Path to generated .o file
221
+
222
+ Raises:
223
+ ConfigurableCompilerError: If compilation fails
224
+ """
225
+ # Determine compiler based on file extension
226
+ is_cpp = source_path.suffix in ['.cpp', '.cxx', '.cc']
227
+ compiler_path = self.toolchain.get_gxx_path() if is_cpp else self.toolchain.get_gcc_path()
228
+
229
+ if compiler_path is None:
230
+ raise ConfigurableCompilerError(
231
+ f"Compiler path not found for {'C++' if is_cpp else 'C'} compilation"
232
+ )
233
+
234
+ # Generate output path if not provided
235
+ if output_path is None:
236
+ obj_dir = self.build_dir / "obj"
237
+ obj_dir.mkdir(parents=True, exist_ok=True)
238
+ output_path = obj_dir / f"{source_path.stem}.o"
239
+
240
+ # Get compilation flags
241
+ flags = self.get_compile_flags()
242
+ compile_flags = flags['common'].copy()
243
+ if is_cpp:
244
+ compile_flags.extend(flags['cxxflags'])
245
+ else:
246
+ compile_flags.extend(flags['cflags'])
247
+
248
+ # Get include paths
249
+ includes = self.get_include_paths()
250
+
251
+ # Async mode: submit to queue and return immediately
252
+ if self.compilation_queue is not None:
253
+ # Convert include paths to flags
254
+ include_flags = [f"-I{str(inc).replace(chr(92), '/')}" for inc in includes]
255
+ # Build command that would be executed
256
+ cmd = self.compilation_executor._build_compile_command(
257
+ compiler_path, source_path, output_path, compile_flags, include_flags
258
+ )
259
+
260
+ # Submit to async compilation queue
261
+ job_id = self._submit_async_compilation(source_path, output_path, cmd)
262
+ self.pending_jobs.append(job_id)
263
+
264
+ # Return output path optimistically (validated in wait_all_jobs())
265
+ return output_path
266
+
267
+ # Sync mode: compile using executor (legacy behavior)
268
+ try:
269
+ return self.compilation_executor.compile_source(
270
+ compiler_path=compiler_path,
271
+ source_path=source_path,
272
+ output_path=output_path,
273
+ compile_flags=compile_flags,
274
+ include_paths=includes
275
+ )
276
+ except KeyboardInterrupt as ke:
277
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
278
+ handle_keyboard_interrupt_properly(ke)
279
+ raise # Never reached, but satisfies type checker
280
+ except Exception as e:
281
+ raise ConfigurableCompilerError(str(e))
282
+
283
+ def compile_sketch(self, sketch_path: Path) -> List[Path]:
284
+ """Compile an Arduino sketch.
285
+
286
+ Args:
287
+ sketch_path: Path to .ino file
288
+
289
+ Returns:
290
+ List of generated object file paths
291
+
292
+ Raises:
293
+ ConfigurableCompilerError: If compilation fails
294
+ """
295
+ object_files = []
296
+
297
+ # Preprocess .ino to .cpp
298
+ cpp_path = self.preprocess_ino(sketch_path)
299
+
300
+ # Determine object file path
301
+ obj_dir = self.build_dir / "obj"
302
+ obj_dir.mkdir(parents=True, exist_ok=True)
303
+ obj_path = obj_dir / f"{cpp_path.stem}.o"
304
+
305
+ # Skip compilation if object file is up-to-date
306
+ if not self.needs_rebuild(cpp_path, obj_path):
307
+ object_files.append(obj_path)
308
+ return object_files
309
+
310
+ # Compile preprocessed .cpp
311
+ compiled_obj = self.compile_source(cpp_path, obj_path)
312
+ object_files.append(compiled_obj)
313
+
314
+ return object_files
315
+
316
+ def compile_core(self, progress_bar: Optional[Any] = None) -> List[Path]:
317
+ """Compile Arduino core sources.
318
+
319
+ Args:
320
+ progress_bar: Optional tqdm progress bar to update during compilation
321
+
322
+ Returns:
323
+ List of generated object file paths
324
+
325
+ Raises:
326
+ ConfigurableCompilerError: If compilation fails
327
+ """
328
+ object_files = []
329
+
330
+ # Get core sources
331
+ core_sources = self.framework.get_core_sources(self.core) # type: ignore[attr-defined]
332
+
333
+ if self.show_progress:
334
+ print(f"Compiling {len(core_sources)} core source files...")
335
+
336
+ # Create core object directory
337
+ core_obj_dir = self.build_dir / "obj" / "core"
338
+ core_obj_dir.mkdir(parents=True, exist_ok=True)
339
+
340
+ # Disable individual file progress messages when using progress bar
341
+ original_show_progress = self.compilation_executor.show_progress
342
+ if progress_bar is not None:
343
+ self.compilation_executor.show_progress = False
344
+
345
+ try:
346
+ # Compile each core source
347
+ for source in core_sources:
348
+ # Update progress bar BEFORE compilation for better UX
349
+ if progress_bar is not None:
350
+ progress_bar.set_description(f'Compiling {source.name[:30]}')
351
+
352
+ try:
353
+ obj_path = core_obj_dir / f"{source.stem}.o"
354
+
355
+ # Skip compilation if object file is up-to-date
356
+ if not self.needs_rebuild(source, obj_path):
357
+ object_files.append(obj_path)
358
+ if progress_bar is not None:
359
+ progress_bar.update(1)
360
+ continue
361
+
362
+ compiled_obj = self.compile_source(source, obj_path)
363
+ object_files.append(compiled_obj)
364
+ if progress_bar is not None:
365
+ progress_bar.update(1)
366
+ except ConfigurableCompilerError as e:
367
+ if self.show_progress:
368
+ print(f"Warning: Failed to compile {source.name}: {e}")
369
+ if progress_bar is not None:
370
+ progress_bar.update(1)
371
+ finally:
372
+ # Restore original show_progress setting
373
+ self.compilation_executor.show_progress = original_show_progress
374
+
375
+ # Wait for all async jobs to complete (if using async mode)
376
+ if hasattr(self, 'wait_all_jobs'):
377
+ try:
378
+ self.wait_all_jobs()
379
+ except ConfigurableCompilerError as e:
380
+ raise ConfigurableCompilerError(f"Core compilation failed: {e}")
381
+
382
+ return object_files
383
+
384
+ def create_core_archive(self, object_files: List[Path]) -> Path:
385
+ """Create core.a archive from compiled object files.
386
+
387
+ Args:
388
+ object_files: List of object file paths to archive
389
+
390
+ Returns:
391
+ Path to generated core.a file
392
+
393
+ Raises:
394
+ ConfigurableCompilerError: If archive creation fails
395
+ """
396
+ # Get archiver tool
397
+ ar_path = self.toolchain.get_ar_path()
398
+
399
+ if ar_path is None:
400
+ raise ConfigurableCompilerError("Archiver (ar) path not found")
401
+
402
+ # Create archive using creator
403
+ try:
404
+ return self.archive_creator.create_core_archive(
405
+ ar_path=ar_path,
406
+ build_dir=self.build_dir,
407
+ object_files=object_files
408
+ )
409
+ except KeyboardInterrupt as ke:
410
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
411
+ handle_keyboard_interrupt_properly(ke)
412
+ raise # Never reached, but satisfies type checker
413
+ except Exception as e:
414
+ raise ConfigurableCompilerError(str(e))
415
+
416
+ def get_compiler_info(self) -> Dict[str, Any]:
417
+ """Get information about the compiler configuration.
418
+
419
+ Returns:
420
+ Dictionary with compiler information
421
+ """
422
+ info = {
423
+ 'board_id': self.board_id,
424
+ 'mcu': self.mcu,
425
+ 'variant': self.variant,
426
+ 'build_dir': str(self.build_dir),
427
+ 'toolchain_type': self.toolchain.toolchain_type, # type: ignore[attr-defined]
428
+ 'gcc_path': str(self.toolchain.get_gcc_path()),
429
+ 'gxx_path': str(self.toolchain.get_gxx_path()),
430
+ }
431
+
432
+ # Add compile flags
433
+ flags = self.get_compile_flags()
434
+ info['compile_flags'] = flags
435
+
436
+ # Add include paths
437
+ includes = self.get_include_paths()
438
+ info['include_paths'] = [str(p) for p in includes]
439
+ info['include_count'] = len(includes)
440
+
441
+ return info
442
+
443
+ def get_base_flags(self) -> List[str]:
444
+ """Get base compiler flags for library compilation.
445
+
446
+ Returns:
447
+ List of compiler flags
448
+ """
449
+ return self.flag_builder.get_base_flags_for_library()
450
+
451
+ def add_library_includes(self, library_includes: List[Path]) -> None:
452
+ """Add library include paths to the compiler.
453
+
454
+ Args:
455
+ library_includes: List of library include directory paths
456
+ """
457
+ if self._include_paths_cache is not None:
458
+ self._include_paths_cache.extend(library_includes)
459
+
460
+ def needs_rebuild(self, source: Path, object_file: Path) -> bool:
461
+ """Check if source file needs to be recompiled.
462
+
463
+ Args:
464
+ source: Source file path
465
+ object_file: Object file path
466
+
467
+ Returns:
468
+ True if source is newer than object file or object doesn't exist
469
+ """
470
+ if not object_file.exists():
471
+ return True
472
+
473
+ source_mtime = source.stat().st_mtime
474
+ object_mtime = object_file.stat().st_mtime
475
+
476
+ return source_mtime > object_mtime
477
+
478
+ def _submit_async_compilation(
479
+ self,
480
+ source: Path,
481
+ output: Path,
482
+ cmd: List[str]
483
+ ) -> str:
484
+ """
485
+ Submit compilation job to async queue.
486
+
487
+ Args:
488
+ source: Source file path
489
+ output: Output object file path
490
+ cmd: Full compiler command
491
+
492
+ Returns:
493
+ Job ID for tracking
494
+ """
495
+ import time
496
+ from ..daemon.compilation_queue import CompilationJob
497
+
498
+ job_id = f"compile_{source.stem}_{int(time.time() * 1000000)}"
499
+
500
+ job = CompilationJob(
501
+ job_id=job_id,
502
+ source_path=source,
503
+ output_path=output,
504
+ compiler_cmd=cmd,
505
+ response_file=None # ConfigurableCompiler doesn't use response files
506
+ )
507
+
508
+ if self.compilation_queue is None:
509
+ raise ConfigurableCompilerError("Compilation queue not initialized")
510
+ self.compilation_queue.submit_job(job)
511
+ return job_id
512
+
513
+ def wait_all_jobs(self) -> None:
514
+ """
515
+ Wait for all pending async compilation jobs to complete.
516
+
517
+ This method must be called after using async compilation mode
518
+ to wait for all submitted jobs and validate their results.
519
+
520
+ Raises:
521
+ ConfigurableCompilerError: If any compilation fails
522
+ """
523
+ if not self.compilation_queue:
524
+ return
525
+
526
+ if not self.pending_jobs:
527
+ return
528
+
529
+ # Wait for all jobs to complete
530
+ self.compilation_queue.wait_for_completion(self.pending_jobs)
531
+
532
+ # Collect failed jobs
533
+ failed_jobs = []
534
+
535
+ for job_id in self.pending_jobs:
536
+ job = self.compilation_queue.get_job_status(job_id)
537
+
538
+ if job is None:
539
+ # This shouldn't happen
540
+ failed_jobs.append(f"Job {job_id} not found")
541
+ continue
542
+
543
+ if job.state.value != "completed":
544
+ failed_jobs.append(f"{job.source_path.name}: {job.stderr[:200]}")
545
+
546
+ # Clear pending jobs
547
+ self.pending_jobs.clear()
548
+
549
+ # Raise error if any jobs failed
550
+ if failed_jobs:
551
+ error_msg = f"Compilation failed for {len(failed_jobs)} file(s):\n"
552
+ error_msg += "\n".join(f" - {err}" for err in failed_jobs[:5])
553
+ if len(failed_jobs) > 5:
554
+ error_msg += f"\n ... and {len(failed_jobs) - 5} more"
555
+ raise ConfigurableCompilerError(error_msg)
556
+
557
+ def get_statistics(self) -> Dict[str, int]:
558
+ """
559
+ Get compilation statistics from the queue.
560
+
561
+ Returns:
562
+ Dictionary with compilation statistics
563
+ """
564
+ if not self.compilation_queue:
565
+ return {
566
+ "total_jobs": 0,
567
+ "pending": 0,
568
+ "running": 0,
569
+ "completed": 0,
570
+ "failed": 0
571
+ }
572
+
573
+ return self.compilation_queue.get_statistics()
574
+
575
+ def compile(
576
+ self,
577
+ source: Path,
578
+ output: Path,
579
+ extra_flags: Optional[List[str]] = None
580
+ ):
581
+ """Compile source file (auto-detects C vs C++).
582
+
583
+ Args:
584
+ source: Path to source file
585
+ output: Path to output .o object file
586
+ extra_flags: Additional compiler flags
587
+
588
+ Returns:
589
+ CompileResult with compilation status
590
+
591
+ Raises:
592
+ ConfigurableCompilerError: If compilation fails
593
+ """
594
+ from .compiler import CompileResult # Import here to avoid circular dependency
595
+
596
+ try:
597
+ obj_path = self.compile_source(source, output)
598
+ return CompileResult(
599
+ success=True,
600
+ object_file=obj_path,
601
+ stdout="",
602
+ stderr="",
603
+ returncode=0
604
+ )
605
+ except ConfigurableCompilerError as e:
606
+ return CompileResult(
607
+ success=False,
608
+ object_file=None,
609
+ stdout="",
610
+ stderr=str(e),
611
+ returncode=1
612
+ )