fbuild 1.2.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fbuild/__init__.py +390 -0
- fbuild/assets/example.txt +1 -0
- fbuild/build/__init__.py +117 -0
- fbuild/build/archive_creator.py +186 -0
- fbuild/build/binary_generator.py +444 -0
- fbuild/build/build_component_factory.py +131 -0
- fbuild/build/build_info_generator.py +624 -0
- fbuild/build/build_state.py +325 -0
- fbuild/build/build_utils.py +93 -0
- fbuild/build/compilation_executor.py +422 -0
- fbuild/build/compiler.py +165 -0
- fbuild/build/compiler_avr.py +574 -0
- fbuild/build/configurable_compiler.py +664 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +214 -0
- fbuild/build/library_dependency_processor.py +185 -0
- fbuild/build/linker.py +708 -0
- fbuild/build/orchestrator.py +67 -0
- fbuild/build/orchestrator_avr.py +651 -0
- fbuild/build/orchestrator_esp32.py +878 -0
- fbuild/build/orchestrator_rp2040.py +719 -0
- fbuild/build/orchestrator_stm32.py +696 -0
- fbuild/build/orchestrator_teensy.py +580 -0
- fbuild/build/source_compilation_orchestrator.py +218 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +717 -0
- fbuild/cli_utils.py +314 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +542 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +369 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +42 -0
- fbuild/daemon/async_client.py +531 -0
- fbuild/daemon/client.py +1505 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/configuration_lock.py +865 -0
- fbuild/daemon/daemon.py +585 -0
- fbuild/daemon/daemon_context.py +293 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/firmware_ledger.py +546 -0
- fbuild/daemon/lock_manager.py +508 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +957 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/port_state_manager.py +249 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +18 -0
- fbuild/daemon/processors/build_processor.py +248 -0
- fbuild/daemon/processors/deploy_processor.py +664 -0
- fbuild/daemon/processors/install_deps_processor.py +431 -0
- fbuild/daemon/processors/locking_processor.py +777 -0
- fbuild/daemon/processors/monitor_processor.py +285 -0
- fbuild/daemon/request_processor.py +457 -0
- fbuild/daemon/shared_serial.py +819 -0
- fbuild/daemon/status_manager.py +238 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +21 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +310 -0
- fbuild/deploy/docker_utils.py +315 -0
- fbuild/deploy/monitor.py +519 -0
- fbuild/deploy/qemu_runner.py +603 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/ledger/__init__.py +52 -0
- fbuild/ledger/board_ledger.py +560 -0
- fbuild/output.py +352 -0
- fbuild/packages/__init__.py +66 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +256 -0
- fbuild/packages/concurrent_manager.py +510 -0
- fbuild/packages/downloader.py +518 -0
- fbuild/packages/fingerprint.py +423 -0
- fbuild/packages/framework_esp32.py +538 -0
- fbuild/packages/framework_rp2040.py +349 -0
- fbuild/packages/framework_stm32.py +459 -0
- fbuild/packages/framework_teensy.py +346 -0
- fbuild/packages/github_utils.py +96 -0
- fbuild/packages/header_trampoline_cache.py +394 -0
- fbuild/packages/library_compiler.py +203 -0
- fbuild/packages/library_manager.py +549 -0
- fbuild/packages/library_manager_esp32.py +725 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_rp2040.py +400 -0
- fbuild/packages/platform_stm32.py +581 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +369 -0
- fbuild/packages/sdk_utils.py +231 -0
- fbuild/packages/toolchain.py +436 -0
- fbuild/packages/toolchain_binaries.py +196 -0
- fbuild/packages/toolchain_esp32.py +489 -0
- fbuild/packages/toolchain_metadata.py +185 -0
- fbuild/packages/toolchain_rp2040.py +436 -0
- fbuild/packages/toolchain_stm32.py +417 -0
- fbuild/packages/toolchain_teensy.py +404 -0
- fbuild/platform_configs/esp32.json +150 -0
- fbuild/platform_configs/esp32c2.json +144 -0
- fbuild/platform_configs/esp32c3.json +143 -0
- fbuild/platform_configs/esp32c5.json +151 -0
- fbuild/platform_configs/esp32c6.json +151 -0
- fbuild/platform_configs/esp32p4.json +149 -0
- fbuild/platform_configs/esp32s3.json +151 -0
- fbuild/platform_configs/imxrt1062.json +56 -0
- fbuild/platform_configs/rp2040.json +70 -0
- fbuild/platform_configs/rp2350.json +76 -0
- fbuild/platform_configs/stm32f1.json +59 -0
- fbuild/platform_configs/stm32f4.json +63 -0
- fbuild/py.typed +0 -0
- fbuild-1.2.8.dist-info/METADATA +468 -0
- fbuild-1.2.8.dist-info/RECORD +121 -0
- fbuild-1.2.8.dist-info/WHEEL +5 -0
- fbuild-1.2.8.dist-info/entry_points.txt +5 -0
- fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
- fbuild-1.2.8.dist-info/top_level.txt +2 -0
- fbuild_lint/__init__.py +0 -0
- fbuild_lint/ruff_plugins/__init__.py +0 -0
- fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
|
@@ -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
|
+
)
|