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,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