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.
- fbuild/__init__.py +0 -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_state.py +325 -0
- fbuild/build/build_utils.py +98 -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 +612 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +186 -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 +656 -0
- fbuild/build/orchestrator_esp32.py +797 -0
- fbuild/build/orchestrator_teensy.py +543 -0
- fbuild/build/source_compilation_orchestrator.py +220 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +566 -0
- fbuild/cli_utils.py +312 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +457 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +209 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +34 -0
- fbuild/daemon/client.py +929 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/daemon.py +474 -0
- fbuild/daemon/daemon_context.py +196 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/lock_manager.py +270 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +301 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +12 -0
- fbuild/daemon/processors/build_processor.py +157 -0
- fbuild/daemon/processors/deploy_processor.py +327 -0
- fbuild/daemon/processors/monitor_processor.py +146 -0
- fbuild/daemon/request_processor.py +401 -0
- fbuild/daemon/status_manager.py +216 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +17 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +314 -0
- fbuild/deploy/monitor.py +495 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/packages/__init__.py +53 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +249 -0
- fbuild/packages/downloader.py +366 -0
- fbuild/packages/framework_esp32.py +538 -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 +413 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +325 -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 +484 -0
- fbuild/packages/toolchain_metadata.py +185 -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-1.1.0.dist-info/METADATA +447 -0
- fbuild-1.1.0.dist-info/RECORD +93 -0
- fbuild-1.1.0.dist-info/WHEEL +5 -0
- fbuild-1.1.0.dist-info/entry_points.txt +5 -0
- fbuild-1.1.0.dist-info/licenses/LICENSE +21 -0
- fbuild-1.1.0.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,637 @@
|
|
|
1
|
+
"""Configurable Linker.
|
|
2
|
+
|
|
3
|
+
This module provides a generic, configuration-driven linker that can link
|
|
4
|
+
for any platform (ESP32, AVR, etc.) based on platform configuration files.
|
|
5
|
+
|
|
6
|
+
Design:
|
|
7
|
+
- Loads linker flags, scripts, libraries from JSON/Python config
|
|
8
|
+
- Generic implementation replaces platform-specific linker classes
|
|
9
|
+
- Same interface as ESP32Linker for drop-in replacement
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import gc
|
|
13
|
+
import json
|
|
14
|
+
import platform
|
|
15
|
+
import subprocess
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import List, Dict, Any, Optional, Union
|
|
19
|
+
|
|
20
|
+
from ..packages.package import IPackage, IToolchain, IFramework
|
|
21
|
+
from .binary_generator import BinaryGenerator
|
|
22
|
+
from .compiler import ILinker, LinkerError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConfigurableLinkerError(LinkerError):
|
|
26
|
+
"""Raised when configurable linking operations fail."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConfigurableLinker(ILinker):
|
|
31
|
+
"""Generic linker driven by platform configuration.
|
|
32
|
+
|
|
33
|
+
This class handles:
|
|
34
|
+
- Loading platform-specific config from JSON
|
|
35
|
+
- Linker script management
|
|
36
|
+
- Library collection
|
|
37
|
+
- Linking object files into firmware.elf
|
|
38
|
+
- Converting .elf to .bin
|
|
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
|
+
):
|
|
51
|
+
"""Initialize configurable linker.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
platform: Platform instance
|
|
55
|
+
toolchain: Toolchain instance
|
|
56
|
+
framework: Framework instance
|
|
57
|
+
board_id: Board identifier (e.g., "esp32-c6-devkitm-1")
|
|
58
|
+
build_dir: Directory for build artifacts
|
|
59
|
+
platform_config: Platform config dict or path to config JSON file
|
|
60
|
+
show_progress: Whether to show linking progress
|
|
61
|
+
"""
|
|
62
|
+
self.platform = platform
|
|
63
|
+
self.toolchain = toolchain
|
|
64
|
+
self.framework = framework
|
|
65
|
+
self.board_id = board_id
|
|
66
|
+
self.build_dir = build_dir
|
|
67
|
+
self.show_progress = show_progress
|
|
68
|
+
|
|
69
|
+
# Load board configuration
|
|
70
|
+
self.board_config = platform.get_board_json(board_id) # type: ignore[attr-defined]
|
|
71
|
+
|
|
72
|
+
# Get MCU type from board config
|
|
73
|
+
self.mcu = self.board_config.get("build", {}).get("mcu", "").lower()
|
|
74
|
+
|
|
75
|
+
# Load platform configuration
|
|
76
|
+
if platform_config is None:
|
|
77
|
+
# Try to load from default location
|
|
78
|
+
config_path = Path(__file__).parent.parent / "platform_configs" / f"{self.mcu}.json"
|
|
79
|
+
if config_path.exists():
|
|
80
|
+
with open(config_path, 'r') as f:
|
|
81
|
+
self.config = json.load(f)
|
|
82
|
+
else:
|
|
83
|
+
raise ConfigurableLinkerError(
|
|
84
|
+
f"No platform configuration found for {self.mcu}. " +
|
|
85
|
+
f"Expected: {config_path}"
|
|
86
|
+
)
|
|
87
|
+
elif isinstance(platform_config, dict):
|
|
88
|
+
self.config = platform_config
|
|
89
|
+
else:
|
|
90
|
+
# Assume it's a path
|
|
91
|
+
with open(platform_config, 'r') as f:
|
|
92
|
+
self.config = json.load(f)
|
|
93
|
+
|
|
94
|
+
# Cache for linker paths
|
|
95
|
+
self._linker_scripts_cache: Optional[List[Path]] = None
|
|
96
|
+
self._sdk_libs_cache: Optional[List[Path]] = None
|
|
97
|
+
|
|
98
|
+
# Initialize binary generator
|
|
99
|
+
self.binary_generator = BinaryGenerator(
|
|
100
|
+
mcu=self.mcu,
|
|
101
|
+
board_config=self.board_config,
|
|
102
|
+
build_dir=build_dir,
|
|
103
|
+
toolchain=toolchain,
|
|
104
|
+
framework=framework,
|
|
105
|
+
show_progress=show_progress
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def get_linker_scripts(self) -> List[Path]:
|
|
109
|
+
"""Get list of linker script paths for the MCU.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of .ld file paths in linking order
|
|
113
|
+
"""
|
|
114
|
+
if self._linker_scripts_cache is not None:
|
|
115
|
+
return self._linker_scripts_cache
|
|
116
|
+
|
|
117
|
+
scripts = []
|
|
118
|
+
|
|
119
|
+
# Check if framework has a get_linker_script method (Teensy-style)
|
|
120
|
+
if hasattr(self.framework, 'get_linker_script'):
|
|
121
|
+
linker_script = self.framework.get_linker_script(self.board_id) # type: ignore[attr-defined]
|
|
122
|
+
if linker_script and linker_script.exists():
|
|
123
|
+
scripts.append(linker_script)
|
|
124
|
+
|
|
125
|
+
# Otherwise use ESP32-style SDK directory approach
|
|
126
|
+
elif hasattr(self.framework, 'get_sdk_dir'):
|
|
127
|
+
# Apply SDK fallback for MCUs not fully supported in the platform
|
|
128
|
+
# (e.g., esp32c2 can use esp32c3 SDK)
|
|
129
|
+
from ..packages.sdk_utils import SDKPathResolver
|
|
130
|
+
sdk_dir = self.framework.get_sdk_dir() # type: ignore[attr-defined]
|
|
131
|
+
resolver = SDKPathResolver(sdk_dir, show_progress=False)
|
|
132
|
+
resolved_mcu = resolver._resolve_mcu(self.mcu)
|
|
133
|
+
|
|
134
|
+
# Get linker script directory
|
|
135
|
+
sdk_ld_dir = sdk_dir / resolved_mcu / "ld"
|
|
136
|
+
|
|
137
|
+
if not sdk_ld_dir.exists():
|
|
138
|
+
raise ConfigurableLinkerError(f"Linker script directory not found: {sdk_ld_dir}")
|
|
139
|
+
|
|
140
|
+
# Get linker scripts from config
|
|
141
|
+
config_scripts = self.config.get('linker_scripts', [])
|
|
142
|
+
|
|
143
|
+
for script_name in config_scripts:
|
|
144
|
+
script_path = sdk_ld_dir / script_name
|
|
145
|
+
if script_path.exists():
|
|
146
|
+
scripts.append(script_path)
|
|
147
|
+
# For ESP32-S3, sections.ld may be in flash mode subdirectories
|
|
148
|
+
elif self.mcu == "esp32s3" and script_name == "sections.ld":
|
|
149
|
+
flash_mode = self.board_config.get("build", {}).get("flash_mode", "qio")
|
|
150
|
+
psram_mode = self.board_config.get("build", {}).get("psram_mode", "qspi")
|
|
151
|
+
flash_dir = sdk_ld_dir.parent / f"{flash_mode}_{psram_mode}"
|
|
152
|
+
alt_script_path = flash_dir / script_name
|
|
153
|
+
if alt_script_path.exists():
|
|
154
|
+
scripts.append(alt_script_path)
|
|
155
|
+
|
|
156
|
+
if not scripts:
|
|
157
|
+
raise ConfigurableLinkerError(
|
|
158
|
+
f"No linker scripts found for {self.mcu}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
self._linker_scripts_cache = scripts
|
|
162
|
+
return scripts
|
|
163
|
+
|
|
164
|
+
def get_sdk_libraries(self) -> List[Path]:
|
|
165
|
+
"""Get list of SDK precompiled libraries.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of .a library file paths
|
|
169
|
+
"""
|
|
170
|
+
if self._sdk_libs_cache is not None:
|
|
171
|
+
return self._sdk_libs_cache
|
|
172
|
+
|
|
173
|
+
# Only ESP32 frameworks have SDK libraries
|
|
174
|
+
if hasattr(self.framework, 'get_sdk_libs'):
|
|
175
|
+
# Get flash mode from board configuration
|
|
176
|
+
flash_mode = self.board_config.get("build", {}).get("flash_mode", "qio")
|
|
177
|
+
|
|
178
|
+
# Get SDK libraries
|
|
179
|
+
self._sdk_libs_cache = self.framework.get_sdk_libs(self.mcu, flash_mode) # type: ignore[attr-defined]
|
|
180
|
+
else:
|
|
181
|
+
# No SDK libraries for this framework (e.g., Teensy)
|
|
182
|
+
self._sdk_libs_cache = []
|
|
183
|
+
|
|
184
|
+
return self._sdk_libs_cache or []
|
|
185
|
+
|
|
186
|
+
def get_linker_flags(self) -> List[str]:
|
|
187
|
+
"""Get linker flags from configuration.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
List of linker flags
|
|
191
|
+
"""
|
|
192
|
+
flags = []
|
|
193
|
+
|
|
194
|
+
# Get flags from config
|
|
195
|
+
config_flags = self.config.get('linker_flags', [])
|
|
196
|
+
flags.extend(config_flags)
|
|
197
|
+
|
|
198
|
+
# Add map file flag with forward slashes for GCC compatibility
|
|
199
|
+
# Use "firmware.map" instead of board_id to avoid special characters
|
|
200
|
+
map_file = self.build_dir / "firmware.map"
|
|
201
|
+
map_file_str = str(map_file).replace('\\', '/')
|
|
202
|
+
flags.append(f'-Wl,-Map={map_file_str}')
|
|
203
|
+
|
|
204
|
+
return flags
|
|
205
|
+
|
|
206
|
+
def link(
|
|
207
|
+
self,
|
|
208
|
+
object_files: List[Path],
|
|
209
|
+
core_archive: Path,
|
|
210
|
+
output_elf: Optional[Path] = None,
|
|
211
|
+
library_archives: Optional[List[Path]] = None
|
|
212
|
+
) -> Path:
|
|
213
|
+
"""Link object files and libraries into firmware.elf.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
object_files: List of object files to link (sketch, libraries)
|
|
217
|
+
core_archive: Path to core.a archive
|
|
218
|
+
output_elf: Optional path for output .elf file
|
|
219
|
+
library_archives: Optional list of library archives to link
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Path to generated firmware.elf
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ConfigurableLinkerError: If linking fails
|
|
226
|
+
"""
|
|
227
|
+
if not object_files:
|
|
228
|
+
raise ConfigurableLinkerError("No object files provided for linking")
|
|
229
|
+
|
|
230
|
+
if not core_archive.exists():
|
|
231
|
+
raise ConfigurableLinkerError(f"Core archive not found: {core_archive}")
|
|
232
|
+
|
|
233
|
+
# Initialize library archives list
|
|
234
|
+
if library_archives is None:
|
|
235
|
+
library_archives = []
|
|
236
|
+
|
|
237
|
+
# Get linker tool (use g++ for C++ support)
|
|
238
|
+
linker_path = self.toolchain.get_gxx_path()
|
|
239
|
+
if linker_path is None or not linker_path.exists():
|
|
240
|
+
raise ConfigurableLinkerError(
|
|
241
|
+
f"Linker not found: {linker_path}. " +
|
|
242
|
+
"Ensure toolchain is installed."
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Generate output path if not provided
|
|
246
|
+
if output_elf is None:
|
|
247
|
+
output_elf = self.build_dir / "firmware.elf"
|
|
248
|
+
|
|
249
|
+
# Get linker flags
|
|
250
|
+
linker_flags = self.get_linker_flags()
|
|
251
|
+
|
|
252
|
+
# Get linker scripts
|
|
253
|
+
linker_scripts = self.get_linker_scripts()
|
|
254
|
+
|
|
255
|
+
# Get SDK libraries
|
|
256
|
+
sdk_libs = self.get_sdk_libraries()
|
|
257
|
+
|
|
258
|
+
# Build linker command
|
|
259
|
+
cmd = [str(linker_path)]
|
|
260
|
+
cmd.extend(linker_flags)
|
|
261
|
+
|
|
262
|
+
# Add linker script directory to library search path (ESP32-specific)
|
|
263
|
+
if hasattr(self.framework, 'get_sdk_dir'):
|
|
264
|
+
ld_dir = self.framework.get_sdk_dir() / self.mcu / "ld" # type: ignore[attr-defined]
|
|
265
|
+
cmd.append(f"-L{ld_dir}")
|
|
266
|
+
|
|
267
|
+
# For ESP32-S3, also add flash mode directory to search path
|
|
268
|
+
if self.mcu == "esp32s3":
|
|
269
|
+
flash_mode = self.board_config.get("build", {}).get("flash_mode", "qio")
|
|
270
|
+
psram_mode = self.board_config.get("build", {}).get("psram_mode", "qspi")
|
|
271
|
+
flash_dir = self.framework.get_sdk_dir() / self.mcu / f"{flash_mode}_{psram_mode}" # type: ignore[attr-defined]
|
|
272
|
+
if flash_dir.exists():
|
|
273
|
+
cmd.append(f"-L{flash_dir}")
|
|
274
|
+
|
|
275
|
+
# Add linker scripts with ESP32-specific path handling
|
|
276
|
+
for script in linker_scripts:
|
|
277
|
+
if script.parent == ld_dir or (self.mcu == "esp32s3" and script.parent.name.endswith(("_qspi", "_opi"))):
|
|
278
|
+
cmd.append(f"-T{script.name}")
|
|
279
|
+
else:
|
|
280
|
+
cmd.append(f"-T{script}")
|
|
281
|
+
else:
|
|
282
|
+
# For non-ESP32 platforms (e.g., Teensy), use absolute paths
|
|
283
|
+
for script in linker_scripts:
|
|
284
|
+
cmd.append(f"-T{script}")
|
|
285
|
+
|
|
286
|
+
# Add object files
|
|
287
|
+
cmd.extend([str(obj) for obj in object_files])
|
|
288
|
+
|
|
289
|
+
# Add core archive
|
|
290
|
+
cmd.append(str(core_archive))
|
|
291
|
+
|
|
292
|
+
# Add SDK library directory to search path (ESP32-specific)
|
|
293
|
+
if hasattr(self.framework, 'get_sdk_dir'):
|
|
294
|
+
sdk_lib_dir = self.framework.get_sdk_dir() / self.mcu / "lib" # type: ignore[attr-defined]
|
|
295
|
+
if sdk_lib_dir.exists():
|
|
296
|
+
cmd.append(f"-L{sdk_lib_dir}")
|
|
297
|
+
|
|
298
|
+
# Group libraries to resolve circular dependencies
|
|
299
|
+
cmd.append("-Wl,--start-group")
|
|
300
|
+
|
|
301
|
+
# Add user library archives first
|
|
302
|
+
for lib_archive in library_archives:
|
|
303
|
+
if lib_archive.exists():
|
|
304
|
+
cmd.append(str(lib_archive))
|
|
305
|
+
|
|
306
|
+
# Add SDK libraries
|
|
307
|
+
for lib in sdk_libs:
|
|
308
|
+
cmd.append(str(lib))
|
|
309
|
+
|
|
310
|
+
# Add standard libraries
|
|
311
|
+
cmd.extend([
|
|
312
|
+
"-lgcc",
|
|
313
|
+
"-lstdc++",
|
|
314
|
+
"-lm",
|
|
315
|
+
"-lc",
|
|
316
|
+
])
|
|
317
|
+
|
|
318
|
+
cmd.append("-Wl,--end-group")
|
|
319
|
+
|
|
320
|
+
# Add output
|
|
321
|
+
cmd.extend(["-o", str(output_elf)])
|
|
322
|
+
|
|
323
|
+
# Execute linker
|
|
324
|
+
if self.show_progress:
|
|
325
|
+
print("Linking firmware.elf...")
|
|
326
|
+
print(f" Object files: {len(object_files)}")
|
|
327
|
+
print(f" Core archive: {core_archive.name}")
|
|
328
|
+
print(f" SDK libraries: {len(sdk_libs)}")
|
|
329
|
+
print(f" Linker scripts: {len(linker_scripts)}")
|
|
330
|
+
|
|
331
|
+
# Add retry logic for Windows file locking issues
|
|
332
|
+
is_windows = platform.system() == "Windows"
|
|
333
|
+
max_retries = 5 if is_windows else 1
|
|
334
|
+
delay = 0.1
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
for attempt in range(max_retries):
|
|
338
|
+
# On Windows, force garbage collection and add delay before retry
|
|
339
|
+
if is_windows and attempt > 0:
|
|
340
|
+
gc.collect()
|
|
341
|
+
time.sleep(delay)
|
|
342
|
+
if self.show_progress:
|
|
343
|
+
print(f" Retrying linking (attempt {attempt + 1}/{max_retries})...")
|
|
344
|
+
|
|
345
|
+
result = subprocess.run(
|
|
346
|
+
cmd,
|
|
347
|
+
capture_output=True,
|
|
348
|
+
text=True,
|
|
349
|
+
timeout=120
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if result.returncode != 0:
|
|
353
|
+
# Check if error is due to file truncation/locking (Windows-specific)
|
|
354
|
+
# Windows file locking manifests as: "file truncated", "error reading", "No such file", or "no more archived files"
|
|
355
|
+
stderr_lower = result.stderr.lower()
|
|
356
|
+
is_file_locking_error = (
|
|
357
|
+
"file truncated" in stderr_lower or
|
|
358
|
+
"error reading" in stderr_lower or
|
|
359
|
+
"no such file" in stderr_lower or
|
|
360
|
+
"no more archived files" in stderr_lower
|
|
361
|
+
)
|
|
362
|
+
if is_windows and is_file_locking_error:
|
|
363
|
+
if attempt < max_retries - 1:
|
|
364
|
+
if self.show_progress:
|
|
365
|
+
print(" [Windows] Detected file locking error, retrying...")
|
|
366
|
+
delay = min(delay * 2, 1.0) # Exponential backoff, max 1s
|
|
367
|
+
continue
|
|
368
|
+
else:
|
|
369
|
+
# Last attempt failed
|
|
370
|
+
error_msg = f"Linking failed after {max_retries} attempts (file locking)\n"
|
|
371
|
+
error_msg += f"stderr: {result.stderr}\n"
|
|
372
|
+
error_msg += f"stdout: {result.stdout}"
|
|
373
|
+
raise ConfigurableLinkerError(error_msg)
|
|
374
|
+
else:
|
|
375
|
+
# Non-file-locking error, fail immediately
|
|
376
|
+
error_msg = "Linking failed\n"
|
|
377
|
+
error_msg += f"stderr: {result.stderr}\n"
|
|
378
|
+
error_msg += f"stdout: {result.stdout}"
|
|
379
|
+
raise ConfigurableLinkerError(error_msg)
|
|
380
|
+
|
|
381
|
+
# Success - linker returned 0
|
|
382
|
+
break
|
|
383
|
+
|
|
384
|
+
if not output_elf.exists():
|
|
385
|
+
raise ConfigurableLinkerError(f"firmware.elf was not created: {output_elf}")
|
|
386
|
+
|
|
387
|
+
if self.show_progress:
|
|
388
|
+
size = output_elf.stat().st_size
|
|
389
|
+
print(f"✓ Created firmware.elf: {size:,} bytes ({size / 1024 / 1024:.2f} MB)")
|
|
390
|
+
|
|
391
|
+
return output_elf
|
|
392
|
+
|
|
393
|
+
except subprocess.TimeoutExpired:
|
|
394
|
+
raise ConfigurableLinkerError("Linking timeout")
|
|
395
|
+
except KeyboardInterrupt as ke:
|
|
396
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
397
|
+
handle_keyboard_interrupt_properly(ke)
|
|
398
|
+
raise # Never reached, but satisfies type checker
|
|
399
|
+
except Exception as e:
|
|
400
|
+
raise ConfigurableLinkerError(f"Failed to link: {e}")
|
|
401
|
+
|
|
402
|
+
def generate_bin(self, elf_path: Path, output_bin: Optional[Path] = None) -> Path:
|
|
403
|
+
"""Generate firmware.bin from firmware.elf.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
elf_path: Path to firmware.elf
|
|
407
|
+
output_bin: Optional path for output .bin file
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Path to generated firmware.bin
|
|
411
|
+
|
|
412
|
+
Raises:
|
|
413
|
+
ConfigurableLinkerError: If conversion fails
|
|
414
|
+
"""
|
|
415
|
+
try:
|
|
416
|
+
return self.binary_generator.generate_bin(elf_path, output_bin)
|
|
417
|
+
except KeyboardInterrupt as ke:
|
|
418
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
419
|
+
handle_keyboard_interrupt_properly(ke)
|
|
420
|
+
raise # Never reached, but satisfies type checker
|
|
421
|
+
except Exception as e:
|
|
422
|
+
raise ConfigurableLinkerError(f"Binary generation failed: {e}")
|
|
423
|
+
|
|
424
|
+
def generate_hex(self, elf_path: Path, output_hex: Optional[Path] = None) -> Path:
|
|
425
|
+
"""Generate firmware.hex from firmware.elf using objcopy.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
elf_path: Path to firmware.elf
|
|
429
|
+
output_hex: Optional path for output .hex file
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
Path to generated firmware.hex
|
|
433
|
+
|
|
434
|
+
Raises:
|
|
435
|
+
ConfigurableLinkerError: If conversion fails
|
|
436
|
+
"""
|
|
437
|
+
if not elf_path.exists():
|
|
438
|
+
raise ConfigurableLinkerError(f"ELF file not found: {elf_path}")
|
|
439
|
+
|
|
440
|
+
# Generate output path if not provided
|
|
441
|
+
if output_hex is None:
|
|
442
|
+
output_hex = self.build_dir / "firmware.hex"
|
|
443
|
+
|
|
444
|
+
# Get objcopy tool from toolchain
|
|
445
|
+
objcopy_path = self.toolchain.get_objcopy_path()
|
|
446
|
+
if objcopy_path is None or not objcopy_path.exists():
|
|
447
|
+
raise ConfigurableLinkerError(
|
|
448
|
+
f"objcopy not found: {objcopy_path}. " +
|
|
449
|
+
"Ensure toolchain is installed."
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Build objcopy command: convert ELF to Intel HEX format
|
|
453
|
+
cmd = [
|
|
454
|
+
str(objcopy_path),
|
|
455
|
+
"-O", "ihex",
|
|
456
|
+
"-R", ".eeprom",
|
|
457
|
+
str(elf_path),
|
|
458
|
+
str(output_hex)
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
result = subprocess.run(
|
|
463
|
+
cmd,
|
|
464
|
+
capture_output=True,
|
|
465
|
+
text=True,
|
|
466
|
+
timeout=30
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if result.returncode != 0:
|
|
470
|
+
error_msg = "HEX generation failed\n"
|
|
471
|
+
error_msg += f"stderr: {result.stderr}\n"
|
|
472
|
+
error_msg += f"stdout: {result.stdout}"
|
|
473
|
+
raise ConfigurableLinkerError(error_msg)
|
|
474
|
+
|
|
475
|
+
if not output_hex.exists():
|
|
476
|
+
raise ConfigurableLinkerError(f"firmware.hex was not created: {output_hex}")
|
|
477
|
+
|
|
478
|
+
if self.show_progress:
|
|
479
|
+
size = output_hex.stat().st_size
|
|
480
|
+
print(f"✓ Created firmware.hex: {size:,} bytes")
|
|
481
|
+
|
|
482
|
+
return output_hex
|
|
483
|
+
|
|
484
|
+
except subprocess.TimeoutExpired:
|
|
485
|
+
raise ConfigurableLinkerError("HEX generation timeout")
|
|
486
|
+
except KeyboardInterrupt as ke:
|
|
487
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
488
|
+
handle_keyboard_interrupt_properly(ke)
|
|
489
|
+
raise # Never reached, but satisfies type checker
|
|
490
|
+
except Exception as e:
|
|
491
|
+
raise ConfigurableLinkerError(f"Failed to generate HEX: {e}")
|
|
492
|
+
|
|
493
|
+
def get_size_info(self, elf_path: Path):
|
|
494
|
+
"""Get firmware size information from ELF file.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
elf_path: Path to firmware.elf
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
SizeInfo object with size data or None if failed
|
|
501
|
+
|
|
502
|
+
Raises:
|
|
503
|
+
ConfigurableLinkerError: If size calculation fails
|
|
504
|
+
"""
|
|
505
|
+
from .linker import SizeInfo
|
|
506
|
+
|
|
507
|
+
if not elf_path.exists():
|
|
508
|
+
raise ConfigurableLinkerError(f"ELF file not found: {elf_path}")
|
|
509
|
+
|
|
510
|
+
# Get arm-none-eabi-size or appropriate size tool from toolchain
|
|
511
|
+
# Check if toolchain has a get_size_path method
|
|
512
|
+
if hasattr(self.toolchain, 'get_size_path'):
|
|
513
|
+
size_tool = self.toolchain.get_size_path()
|
|
514
|
+
else:
|
|
515
|
+
# Fall back to looking for size tool in toolchain bin directory
|
|
516
|
+
gcc_path = self.toolchain.get_gcc_path()
|
|
517
|
+
if gcc_path is None:
|
|
518
|
+
return None
|
|
519
|
+
toolchain_bin = gcc_path.parent
|
|
520
|
+
size_tool = toolchain_bin / "arm-none-eabi-size"
|
|
521
|
+
if not size_tool.exists():
|
|
522
|
+
size_tool = toolchain_bin / "arm-none-eabi-size.exe"
|
|
523
|
+
|
|
524
|
+
if size_tool and not size_tool.exists():
|
|
525
|
+
# If we can't find the size tool, return None (non-fatal)
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
result = subprocess.run(
|
|
530
|
+
[str(size_tool), str(elf_path)],
|
|
531
|
+
capture_output=True,
|
|
532
|
+
text=True,
|
|
533
|
+
timeout=10
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
if result.returncode == 0:
|
|
537
|
+
# Get max flash and RAM from board config
|
|
538
|
+
max_flash = self.board_config.get("upload", {}).get("maximum_size")
|
|
539
|
+
max_ram = self.board_config.get("upload", {}).get("maximum_ram_size")
|
|
540
|
+
|
|
541
|
+
return SizeInfo.parse(
|
|
542
|
+
result.stdout,
|
|
543
|
+
max_flash=max_flash,
|
|
544
|
+
max_ram=max_ram
|
|
545
|
+
)
|
|
546
|
+
else:
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
except KeyboardInterrupt as ke:
|
|
550
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
551
|
+
handle_keyboard_interrupt_properly(ke)
|
|
552
|
+
raise # Never reached, but satisfies type checker
|
|
553
|
+
except Exception:
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
def generate_bootloader(self, output_bin: Optional[Path] = None) -> Path:
|
|
557
|
+
"""Generate bootloader.bin from bootloader ELF file.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
output_bin: Optional path for output bootloader.bin
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Path to generated bootloader.bin
|
|
564
|
+
|
|
565
|
+
Raises:
|
|
566
|
+
ConfigurableLinkerError: If generation fails
|
|
567
|
+
"""
|
|
568
|
+
try:
|
|
569
|
+
return self.binary_generator.generate_bootloader(output_bin)
|
|
570
|
+
except KeyboardInterrupt as ke:
|
|
571
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
572
|
+
handle_keyboard_interrupt_properly(ke)
|
|
573
|
+
raise # Never reached, but satisfies type checker
|
|
574
|
+
except Exception as e:
|
|
575
|
+
raise ConfigurableLinkerError(f"Bootloader generation failed: {e}")
|
|
576
|
+
|
|
577
|
+
def generate_partition_table(self, output_bin: Optional[Path] = None) -> Path:
|
|
578
|
+
"""Generate partitions.bin from partition CSV file.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
output_bin: Optional path for output partitions.bin
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Path to generated partitions.bin
|
|
585
|
+
|
|
586
|
+
Raises:
|
|
587
|
+
ConfigurableLinkerError: If generation fails
|
|
588
|
+
"""
|
|
589
|
+
try:
|
|
590
|
+
return self.binary_generator.generate_partition_table(output_bin)
|
|
591
|
+
except KeyboardInterrupt as ke:
|
|
592
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
593
|
+
handle_keyboard_interrupt_properly(ke)
|
|
594
|
+
raise # Never reached, but satisfies type checker
|
|
595
|
+
except Exception as e:
|
|
596
|
+
raise ConfigurableLinkerError(f"Partition table generation failed: {e}")
|
|
597
|
+
|
|
598
|
+
def get_linker_info(self) -> Dict[str, Any]:
|
|
599
|
+
"""Get information about the linker configuration.
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Dictionary with linker information
|
|
603
|
+
"""
|
|
604
|
+
info = {
|
|
605
|
+
'board_id': self.board_id,
|
|
606
|
+
'mcu': self.mcu,
|
|
607
|
+
'build_dir': str(self.build_dir),
|
|
608
|
+
'toolchain_type': self.toolchain.toolchain_type, # type: ignore[attr-defined]
|
|
609
|
+
'linker_path': str(self.toolchain.get_gxx_path()),
|
|
610
|
+
'objcopy_path': str(self.toolchain.get_objcopy_path()),
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
# Add linker scripts
|
|
614
|
+
try:
|
|
615
|
+
scripts = self.get_linker_scripts()
|
|
616
|
+
info['linker_scripts'] = [s.name for s in scripts]
|
|
617
|
+
info['linker_script_count'] = len(scripts)
|
|
618
|
+
except KeyboardInterrupt as ke:
|
|
619
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
620
|
+
handle_keyboard_interrupt_properly(ke)
|
|
621
|
+
raise # Never reached, but satisfies type checker
|
|
622
|
+
except Exception as e:
|
|
623
|
+
info['linker_scripts_error'] = str(e)
|
|
624
|
+
|
|
625
|
+
# Add SDK libraries
|
|
626
|
+
try:
|
|
627
|
+
libs = self.get_sdk_libraries()
|
|
628
|
+
info['sdk_library_count'] = len(libs)
|
|
629
|
+
info['sdk_libraries_sample'] = [lib.name for lib in libs[:10]]
|
|
630
|
+
except KeyboardInterrupt as ke:
|
|
631
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
632
|
+
handle_keyboard_interrupt_properly(ke)
|
|
633
|
+
raise # Never reached, but satisfies type checker
|
|
634
|
+
except Exception as e:
|
|
635
|
+
info['sdk_libraries_error'] = str(e)
|
|
636
|
+
|
|
637
|
+
return info
|