fbuild 1.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. fbuild/__init__.py +390 -0
  2. fbuild/assets/example.txt +1 -0
  3. fbuild/build/__init__.py +117 -0
  4. fbuild/build/archive_creator.py +186 -0
  5. fbuild/build/binary_generator.py +444 -0
  6. fbuild/build/build_component_factory.py +131 -0
  7. fbuild/build/build_info_generator.py +624 -0
  8. fbuild/build/build_state.py +325 -0
  9. fbuild/build/build_utils.py +93 -0
  10. fbuild/build/compilation_executor.py +422 -0
  11. fbuild/build/compiler.py +165 -0
  12. fbuild/build/compiler_avr.py +574 -0
  13. fbuild/build/configurable_compiler.py +664 -0
  14. fbuild/build/configurable_linker.py +637 -0
  15. fbuild/build/flag_builder.py +214 -0
  16. fbuild/build/library_dependency_processor.py +185 -0
  17. fbuild/build/linker.py +708 -0
  18. fbuild/build/orchestrator.py +67 -0
  19. fbuild/build/orchestrator_avr.py +651 -0
  20. fbuild/build/orchestrator_esp32.py +878 -0
  21. fbuild/build/orchestrator_rp2040.py +719 -0
  22. fbuild/build/orchestrator_stm32.py +696 -0
  23. fbuild/build/orchestrator_teensy.py +580 -0
  24. fbuild/build/source_compilation_orchestrator.py +218 -0
  25. fbuild/build/source_scanner.py +516 -0
  26. fbuild/cli.py +717 -0
  27. fbuild/cli_utils.py +314 -0
  28. fbuild/config/__init__.py +16 -0
  29. fbuild/config/board_config.py +542 -0
  30. fbuild/config/board_loader.py +92 -0
  31. fbuild/config/ini_parser.py +369 -0
  32. fbuild/config/mcu_specs.py +88 -0
  33. fbuild/daemon/__init__.py +42 -0
  34. fbuild/daemon/async_client.py +531 -0
  35. fbuild/daemon/client.py +1505 -0
  36. fbuild/daemon/compilation_queue.py +293 -0
  37. fbuild/daemon/configuration_lock.py +865 -0
  38. fbuild/daemon/daemon.py +585 -0
  39. fbuild/daemon/daemon_context.py +293 -0
  40. fbuild/daemon/error_collector.py +263 -0
  41. fbuild/daemon/file_cache.py +332 -0
  42. fbuild/daemon/firmware_ledger.py +546 -0
  43. fbuild/daemon/lock_manager.py +508 -0
  44. fbuild/daemon/logging_utils.py +149 -0
  45. fbuild/daemon/messages.py +957 -0
  46. fbuild/daemon/operation_registry.py +288 -0
  47. fbuild/daemon/port_state_manager.py +249 -0
  48. fbuild/daemon/process_tracker.py +366 -0
  49. fbuild/daemon/processors/__init__.py +18 -0
  50. fbuild/daemon/processors/build_processor.py +248 -0
  51. fbuild/daemon/processors/deploy_processor.py +664 -0
  52. fbuild/daemon/processors/install_deps_processor.py +431 -0
  53. fbuild/daemon/processors/locking_processor.py +777 -0
  54. fbuild/daemon/processors/monitor_processor.py +285 -0
  55. fbuild/daemon/request_processor.py +457 -0
  56. fbuild/daemon/shared_serial.py +819 -0
  57. fbuild/daemon/status_manager.py +238 -0
  58. fbuild/daemon/subprocess_manager.py +316 -0
  59. fbuild/deploy/__init__.py +21 -0
  60. fbuild/deploy/deployer.py +67 -0
  61. fbuild/deploy/deployer_esp32.py +310 -0
  62. fbuild/deploy/docker_utils.py +315 -0
  63. fbuild/deploy/monitor.py +519 -0
  64. fbuild/deploy/qemu_runner.py +603 -0
  65. fbuild/interrupt_utils.py +34 -0
  66. fbuild/ledger/__init__.py +52 -0
  67. fbuild/ledger/board_ledger.py +560 -0
  68. fbuild/output.py +352 -0
  69. fbuild/packages/__init__.py +66 -0
  70. fbuild/packages/archive_utils.py +1098 -0
  71. fbuild/packages/arduino_core.py +412 -0
  72. fbuild/packages/cache.py +256 -0
  73. fbuild/packages/concurrent_manager.py +510 -0
  74. fbuild/packages/downloader.py +518 -0
  75. fbuild/packages/fingerprint.py +423 -0
  76. fbuild/packages/framework_esp32.py +538 -0
  77. fbuild/packages/framework_rp2040.py +349 -0
  78. fbuild/packages/framework_stm32.py +459 -0
  79. fbuild/packages/framework_teensy.py +346 -0
  80. fbuild/packages/github_utils.py +96 -0
  81. fbuild/packages/header_trampoline_cache.py +394 -0
  82. fbuild/packages/library_compiler.py +203 -0
  83. fbuild/packages/library_manager.py +549 -0
  84. fbuild/packages/library_manager_esp32.py +725 -0
  85. fbuild/packages/package.py +163 -0
  86. fbuild/packages/platform_esp32.py +383 -0
  87. fbuild/packages/platform_rp2040.py +400 -0
  88. fbuild/packages/platform_stm32.py +581 -0
  89. fbuild/packages/platform_teensy.py +312 -0
  90. fbuild/packages/platform_utils.py +131 -0
  91. fbuild/packages/platformio_registry.py +369 -0
  92. fbuild/packages/sdk_utils.py +231 -0
  93. fbuild/packages/toolchain.py +436 -0
  94. fbuild/packages/toolchain_binaries.py +196 -0
  95. fbuild/packages/toolchain_esp32.py +489 -0
  96. fbuild/packages/toolchain_metadata.py +185 -0
  97. fbuild/packages/toolchain_rp2040.py +436 -0
  98. fbuild/packages/toolchain_stm32.py +417 -0
  99. fbuild/packages/toolchain_teensy.py +404 -0
  100. fbuild/platform_configs/esp32.json +150 -0
  101. fbuild/platform_configs/esp32c2.json +144 -0
  102. fbuild/platform_configs/esp32c3.json +143 -0
  103. fbuild/platform_configs/esp32c5.json +151 -0
  104. fbuild/platform_configs/esp32c6.json +151 -0
  105. fbuild/platform_configs/esp32p4.json +149 -0
  106. fbuild/platform_configs/esp32s3.json +151 -0
  107. fbuild/platform_configs/imxrt1062.json +56 -0
  108. fbuild/platform_configs/rp2040.json +70 -0
  109. fbuild/platform_configs/rp2350.json +76 -0
  110. fbuild/platform_configs/stm32f1.json +59 -0
  111. fbuild/platform_configs/stm32f4.json +63 -0
  112. fbuild/py.typed +0 -0
  113. fbuild-1.2.8.dist-info/METADATA +468 -0
  114. fbuild-1.2.8.dist-info/RECORD +121 -0
  115. fbuild-1.2.8.dist-info/WHEEL +5 -0
  116. fbuild-1.2.8.dist-info/entry_points.txt +5 -0
  117. fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
  118. fbuild-1.2.8.dist-info/top_level.txt +2 -0
  119. fbuild_lint/__init__.py +0 -0
  120. fbuild_lint/ruff_plugins/__init__.py +0 -0
  121. fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
fbuild/build/linker.py ADDED
@@ -0,0 +1,708 @@
1
+ """
2
+ AVR linker wrapper for creating firmware binaries.
3
+
4
+ This module provides a wrapper around avr-gcc linker, avr-ar, avr-objcopy,
5
+ and avr-size for linking object files into firmware.
6
+ """
7
+
8
+ import subprocess
9
+ import time
10
+ from pathlib import Path
11
+ from typing import List, Optional
12
+ from dataclasses import dataclass
13
+
14
+ from .compiler import ILinker, LinkerError
15
+
16
+
17
+ @dataclass
18
+ class SizeInfo:
19
+ """Firmware size information."""
20
+
21
+ text: int # Program memory (flash) usage in bytes
22
+ data: int # Initialized data in RAM
23
+ bss: int # Uninitialized data in RAM
24
+ total_flash: int # Total flash used (text + data)
25
+ total_ram: int # Total RAM used (data + bss)
26
+ max_flash: Optional[int] = None # Maximum flash available
27
+ max_ram: Optional[int] = None # Maximum RAM available
28
+
29
+ @property
30
+ def flash_percent(self) -> Optional[float]:
31
+ """Calculate flash usage percentage."""
32
+ if self.max_flash:
33
+ return (self.total_flash / self.max_flash) * 100
34
+ return None
35
+
36
+ @property
37
+ def ram_percent(self) -> Optional[float]:
38
+ """Calculate RAM usage percentage."""
39
+ if self.max_ram:
40
+ return (self.total_ram / self.max_ram) * 100
41
+ return None
42
+
43
+ @staticmethod
44
+ def parse(avr_size_output: str, max_flash: Optional[int] = None,
45
+ max_ram: Optional[int] = None) -> 'SizeInfo':
46
+ """
47
+ Parse avr-size output.
48
+
49
+ Args:
50
+ avr_size_output: Output from `avr-size -A` command
51
+ max_flash: Maximum flash size for board
52
+ max_ram: Maximum RAM size for board
53
+
54
+ Returns:
55
+ SizeInfo object with parsed size data
56
+ """
57
+ text = 0
58
+ data = 0
59
+ bss = 0
60
+
61
+ for line in avr_size_output.split('\n'):
62
+ parts = line.split()
63
+ if len(parts) >= 2:
64
+ section = parts[0]
65
+ try:
66
+ size = int(parts[1])
67
+ if section == '.text':
68
+ text = size
69
+ elif section == '.data':
70
+ data = size
71
+ elif section == '.bss':
72
+ bss = size
73
+ except (ValueError, IndexError):
74
+ continue
75
+
76
+ total_flash = text + data
77
+ total_ram = data + bss
78
+
79
+ return SizeInfo(
80
+ text=text,
81
+ data=data,
82
+ bss=bss,
83
+ total_flash=total_flash,
84
+ total_ram=total_ram,
85
+ max_flash=max_flash,
86
+ max_ram=max_ram
87
+ )
88
+
89
+
90
+ @dataclass
91
+ class LinkResult:
92
+ """Result of linking operation."""
93
+
94
+ success: bool
95
+ elf_path: Optional[Path]
96
+ hex_path: Optional[Path]
97
+ size_info: Optional[SizeInfo]
98
+ stdout: str
99
+ stderr: str
100
+
101
+
102
+ class LinkerAVR(ILinker):
103
+ """
104
+ Wrapper for AVR linker tools.
105
+
106
+ Links object files into firmware binaries using avr-gcc, avr-ar,
107
+ avr-objcopy, and avr-size.
108
+ """
109
+
110
+ def __init__(
111
+ self,
112
+ avr_gcc: Path,
113
+ avr_ar: Path,
114
+ avr_objcopy: Path,
115
+ avr_size: Path,
116
+ mcu: str,
117
+ max_flash: Optional[int] = None,
118
+ max_ram: Optional[int] = None
119
+ ):
120
+ """
121
+ Initialize linker.
122
+
123
+ Args:
124
+ avr_gcc: Path to avr-gcc executable (used for linking)
125
+ avr_ar: Path to avr-ar executable (for creating archives)
126
+ avr_objcopy: Path to avr-objcopy (for ELF to HEX conversion)
127
+ avr_size: Path to avr-size (for size reporting)
128
+ mcu: MCU type (e.g., atmega328p)
129
+ max_flash: Maximum flash size for overflow detection
130
+ max_ram: Maximum RAM size for overflow detection
131
+ """
132
+ self.avr_gcc = Path(avr_gcc)
133
+ self.avr_ar = Path(avr_ar)
134
+ self.avr_objcopy = Path(avr_objcopy)
135
+ self.avr_size = Path(avr_size)
136
+ self.mcu = mcu
137
+ self.max_flash = max_flash
138
+ self.max_ram = max_ram
139
+
140
+ # Verify tools exist
141
+ if not self.avr_gcc.exists():
142
+ raise LinkerError(f"avr-gcc not found: {self.avr_gcc}")
143
+ if not self.avr_ar.exists():
144
+ raise LinkerError(f"avr-ar not found: {self.avr_ar}")
145
+ if not self.avr_objcopy.exists():
146
+ raise LinkerError(f"avr-objcopy not found: {self.avr_objcopy}")
147
+ if not self.avr_size.exists():
148
+ raise LinkerError(f"avr-size not found: {self.avr_size}")
149
+
150
+ def link_legacy(
151
+ self,
152
+ sketch_objects: List[Path],
153
+ core_objects: List[Path],
154
+ output_elf: Path,
155
+ output_hex: Path,
156
+ lib_archives: Optional[List[Path]] = None,
157
+ extra_flags: Optional[List[str]] = None,
158
+ additional_objects: Optional[List[Path]] = None
159
+ ) -> LinkResult:
160
+ """
161
+ Link object files into firmware (LEGACY method - use link() instead).
162
+
163
+ Process:
164
+ 1. Link sketch objects + core objects + additional objects + library archives to create .elf
165
+ 2. Convert .elf to .hex using avr-objcopy
166
+ 3. Get size information using avr-size
167
+
168
+ Note: Core objects are passed directly instead of being archived because
169
+ LTO with -fno-fat-lto-objects produces bytecode-only objects that don't
170
+ work well in archives (the archive won't have a proper symbol index).
171
+
172
+ Args:
173
+ sketch_objects: User sketch object files
174
+ core_objects: Arduino core object files
175
+ output_elf: Output .elf file path
176
+ output_hex: Output .hex file path
177
+ lib_archives: Optional list of library archive (.a) files
178
+ extra_flags: Additional linker flags
179
+ additional_objects: Optional additional object files (e.g., library objects for LTO)
180
+
181
+ Returns:
182
+ LinkResult with linking status and size info
183
+ """
184
+ try:
185
+ # Create build directory if needed
186
+ output_elf.parent.mkdir(parents=True, exist_ok=True)
187
+
188
+ # Link to .elf - pass core objects directly instead of archiving
189
+ link_result = self._link_elf(
190
+ sketch_objects,
191
+ core_objects,
192
+ output_elf,
193
+ lib_archives or [],
194
+ extra_flags or [],
195
+ additional_objects
196
+ )
197
+
198
+ if not link_result or link_result.returncode != 0:
199
+ return LinkResult(
200
+ success=False,
201
+ elf_path=None,
202
+ hex_path=None,
203
+ size_info=None,
204
+ stdout=link_result.stdout if link_result else '',
205
+ stderr=link_result.stderr if link_result else 'Linking failed'
206
+ )
207
+
208
+ # Step 3: Convert to .hex
209
+ if not self._objcopy_hex(output_elf, output_hex):
210
+ return LinkResult(
211
+ success=False,
212
+ elf_path=output_elf if output_elf.exists() else None,
213
+ hex_path=None,
214
+ size_info=None,
215
+ stdout=link_result.stdout,
216
+ stderr='Failed to convert ELF to HEX'
217
+ )
218
+
219
+ # Step 4: Get size info
220
+ size_info = self._get_size(output_elf)
221
+
222
+ # Check for flash overflow
223
+ if size_info and self.max_flash and size_info.total_flash > self.max_flash:
224
+ return LinkResult(
225
+ success=False,
226
+ elf_path=output_elf,
227
+ hex_path=output_hex,
228
+ size_info=size_info,
229
+ stdout=link_result.stdout,
230
+ stderr=f'Sketch too large: {size_info.total_flash} bytes (maximum is {self.max_flash} bytes)'
231
+ )
232
+
233
+ return LinkResult(
234
+ success=True,
235
+ elf_path=output_elf,
236
+ hex_path=output_hex,
237
+ size_info=size_info,
238
+ stdout=link_result.stdout,
239
+ stderr=link_result.stderr
240
+ )
241
+
242
+ except KeyboardInterrupt as ke:
243
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
244
+ handle_keyboard_interrupt_properly(ke)
245
+ raise # Never reached, but satisfies type checker
246
+ except Exception as e:
247
+ return LinkResult(
248
+ success=False,
249
+ elf_path=None,
250
+ hex_path=None,
251
+ size_info=None,
252
+ stdout='',
253
+ stderr=f'Linking exception: {str(e)}'
254
+ )
255
+
256
+ def _create_core_archive(
257
+ self,
258
+ core_objects: List[Path],
259
+ archive_path: Path
260
+ ) -> bool:
261
+ """
262
+ Create core.a archive from core object files.
263
+
264
+ Args:
265
+ core_objects: List of core .o files
266
+ archive_path: Output archive path
267
+
268
+ Returns:
269
+ True if successful, False otherwise
270
+ """
271
+ if not core_objects:
272
+ return True # No core objects, nothing to archive
273
+
274
+ # Remove existing archive to avoid issues on Windows
275
+ # Try multiple times with delays if deletion fails
276
+ if archive_path.exists():
277
+ for _ in range(5):
278
+ try:
279
+ archive_path.unlink()
280
+ break
281
+ except PermissionError:
282
+ # File might be locked, wait a bit
283
+ time.sleep(0.05)
284
+ except KeyboardInterrupt as ke:
285
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
286
+ handle_keyboard_interrupt_properly(ke)
287
+ except Exception:
288
+ # Other error, just continue
289
+ break
290
+
291
+ cmd = [
292
+ str(self.avr_ar),
293
+ 'rcs', # r=replace, c=create, s=index
294
+ str(archive_path)
295
+ ]
296
+ cmd.extend(str(obj) for obj in core_objects)
297
+
298
+ try:
299
+ result = subprocess.run(
300
+ cmd,
301
+ capture_output=True,
302
+ text=True,
303
+ check=False
304
+ )
305
+
306
+ # Check if command succeeded
307
+ if result.returncode != 0:
308
+ return False
309
+
310
+ # On Windows, there might be a slight delay before file appears
311
+ # Try checking existence with a small delay if needed
312
+ for attempt in range(10): # Try up to 10 times
313
+ if archive_path.exists():
314
+ return True
315
+ time.sleep(0.02 * (attempt + 1)) # Exponential backoff: 20ms, 40ms, 60ms...
316
+
317
+ return False
318
+ except KeyboardInterrupt as ke:
319
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
320
+ handle_keyboard_interrupt_properly(ke)
321
+ raise # Never reached, but satisfies type checker
322
+ except Exception:
323
+ return False
324
+
325
+ def _link_elf(
326
+ self,
327
+ sketch_objects: List[Path],
328
+ core_objects: List[Path],
329
+ output_elf: Path,
330
+ lib_archives: List[Path],
331
+ extra_flags: List[str],
332
+ additional_objects: Optional[List[Path]] = None
333
+ ):
334
+ """
335
+ Link objects and archives to create .elf file.
336
+
337
+ Args:
338
+ sketch_objects: Sketch object files
339
+ core_objects: Core object files (passed directly for LTO)
340
+ output_elf: Output .elf file
341
+ lib_archives: Library archives (.a files)
342
+ extra_flags: Additional linker flags
343
+ additional_objects: Additional object files (e.g., library objects for LTO)
344
+
345
+ Returns:
346
+ subprocess.CompletedProcess result
347
+ """
348
+ cmd = [
349
+ str(self.avr_gcc),
350
+ '-w', # Suppress warnings
351
+ '-Os', # Optimize for size
352
+ '-g', # Include debug info
353
+ '-flto', # Link-time optimization
354
+ '-fuse-linker-plugin', # Use LTO plugin
355
+ '-Wl,--gc-sections', # Garbage collect unused sections
356
+ '-Wl,--allow-multiple-definition', # Allow multiple definitions (needed for some libraries like FastLED)
357
+ f'-mmcu={self.mcu}', # Target MCU
358
+ '-o', str(output_elf)
359
+ ]
360
+
361
+ # Add sketch objects
362
+ cmd.extend(str(obj) for obj in sketch_objects)
363
+
364
+ # Add core objects (passed directly for LTO compatibility)
365
+ cmd.extend(str(obj) for obj in core_objects)
366
+
367
+ # Add additional objects (e.g., library objects for LTO)
368
+ if additional_objects:
369
+ cmd.extend(str(obj) for obj in additional_objects)
370
+
371
+ # Start group for circular dependencies
372
+ cmd.append('-Wl,--start-group')
373
+
374
+ # Add library archives
375
+ for lib_archive in lib_archives:
376
+ if lib_archive.exists():
377
+ cmd.append(str(lib_archive))
378
+
379
+ # Add math library
380
+ cmd.append('-lm')
381
+
382
+ # End group
383
+ cmd.append('-Wl,--end-group')
384
+
385
+ # Add library path
386
+ cmd.append(f'-L{output_elf.parent}')
387
+
388
+ # Add extra flags
389
+ cmd.extend(extra_flags)
390
+
391
+ try:
392
+ result = subprocess.run(
393
+ cmd,
394
+ capture_output=True,
395
+ text=True,
396
+ check=False
397
+ )
398
+ return result
399
+ except KeyboardInterrupt as ke:
400
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
401
+ handle_keyboard_interrupt_properly(ke)
402
+ raise # Never reached, but satisfies type checker
403
+ except Exception as e:
404
+ from types import SimpleNamespace
405
+ return SimpleNamespace(
406
+ returncode=-1,
407
+ stdout='',
408
+ stderr=str(e)
409
+ )
410
+
411
+ def _objcopy_hex(self, elf_path: Path, hex_path: Path) -> bool:
412
+ """
413
+ Convert .elf to .hex using avr-objcopy.
414
+
415
+ Args:
416
+ elf_path: Input .elf file
417
+ hex_path: Output .hex file
418
+
419
+ Returns:
420
+ True if successful, False otherwise
421
+ """
422
+ cmd = [
423
+ str(self.avr_objcopy),
424
+ '-O', 'ihex', # Intel HEX format
425
+ '-R', '.eeprom', # Remove EEPROM section
426
+ str(elf_path),
427
+ str(hex_path)
428
+ ]
429
+
430
+ try:
431
+ result = subprocess.run(
432
+ cmd,
433
+ capture_output=True,
434
+ text=True,
435
+ check=False
436
+ )
437
+ return result.returncode == 0 and hex_path.exists()
438
+ except KeyboardInterrupt as ke:
439
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
440
+ handle_keyboard_interrupt_properly(ke)
441
+ raise # Never reached, but satisfies type checker
442
+ except Exception:
443
+ return False
444
+
445
+ def _get_size(self, elf_path: Path) -> Optional[SizeInfo]:
446
+ """
447
+ Get firmware size information.
448
+
449
+ Args:
450
+ elf_path: Path to .elf file
451
+
452
+ Returns:
453
+ SizeInfo object or None if failed
454
+ """
455
+ cmd = [
456
+ str(self.avr_size),
457
+ '-A', # Berkeley format with section details
458
+ str(elf_path)
459
+ ]
460
+
461
+ try:
462
+ result = subprocess.run(
463
+ cmd,
464
+ capture_output=True,
465
+ text=True,
466
+ check=False
467
+ )
468
+
469
+ if result.returncode == 0:
470
+ return SizeInfo.parse(
471
+ result.stdout,
472
+ self.max_flash,
473
+ self.max_ram
474
+ )
475
+ return None
476
+ except KeyboardInterrupt as ke:
477
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
478
+ handle_keyboard_interrupt_properly(ke)
479
+ raise # Never reached, but satisfies type checker
480
+ except Exception:
481
+ return None
482
+
483
+ def create_eep(self, elf_path: Path, eep_path: Path) -> bool:
484
+ """
485
+ Extract EEPROM data to .eep file (optional).
486
+
487
+ Args:
488
+ elf_path: Input .elf file
489
+ eep_path: Output .eep file
490
+
491
+ Returns:
492
+ True if successful, False otherwise
493
+ """
494
+ cmd = [
495
+ str(self.avr_objcopy),
496
+ '-O', 'ihex',
497
+ '-j', '.eeprom',
498
+ '--set-section-flags=.eeprom=alloc,load',
499
+ '--no-change-warnings',
500
+ '--change-section-lma', '.eeprom=0',
501
+ str(elf_path),
502
+ str(eep_path)
503
+ ]
504
+
505
+ try:
506
+ result = subprocess.run(
507
+ cmd,
508
+ capture_output=True,
509
+ text=True,
510
+ check=False
511
+ )
512
+ return result.returncode == 0
513
+ except KeyboardInterrupt as ke:
514
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
515
+ handle_keyboard_interrupt_properly(ke)
516
+ raise # Never reached, but satisfies type checker
517
+ except Exception:
518
+ return False
519
+
520
+ # BaseLinker interface implementation
521
+ def link(
522
+ self,
523
+ sketch_objects: List[Path],
524
+ core_archive: Path,
525
+ output_elf: Optional[Path] = None,
526
+ library_archives: Optional[List[Path]] = None
527
+ ) -> Path:
528
+ """Link object files into firmware ELF (BaseLinker interface).
529
+
530
+ This is the BaseLinker interface method. For the legacy method with more
531
+ options, see link_with_options().
532
+
533
+ Args:
534
+ sketch_objects: List of sketch object files
535
+ core_archive: Core archive file (core.a)
536
+ output_elf: Optional path for output .elf file
537
+ library_archives: Optional list of library archives
538
+
539
+ Returns:
540
+ Path to generated .elf file
541
+
542
+ Raises:
543
+ LinkerError: If linking fails
544
+ """
545
+ # Generate default paths
546
+ if output_elf is None:
547
+ output_elf = Path.cwd() / "build" / "firmware.elf"
548
+
549
+ output_hex = output_elf.with_suffix('.hex')
550
+
551
+ # For AVR with LTO, we don't use core.a directly.
552
+ # Instead, extract object files from the archive if it exists
553
+ core_objects = []
554
+ if core_archive.exists() and core_archive.suffix == '.a':
555
+ # For now, we'll assume core objects are in the same directory
556
+ # This is a simplification; in practice, the orchestrator should
557
+ # pass core objects directly
558
+ core_obj_dir = core_archive.parent / "core"
559
+ if core_obj_dir.exists():
560
+ core_objects = list(core_obj_dir.glob("*.o"))
561
+
562
+ # Use the legacy link method
563
+ result = self.link_with_options(
564
+ sketch_objects=sketch_objects,
565
+ core_objects=core_objects,
566
+ output_elf=output_elf,
567
+ output_hex=output_hex,
568
+ lib_archives=library_archives,
569
+ extra_flags=None,
570
+ additional_objects=None
571
+ )
572
+
573
+ if not result.success:
574
+ raise LinkerError(f"Linking failed: {result.stderr}")
575
+
576
+ if result.elf_path is None:
577
+ raise LinkerError("Linking succeeded but no ELF file was generated")
578
+
579
+ return result.elf_path
580
+
581
+ def generate_bin(self, elf_path: Path) -> Path:
582
+ """Generate binary from ELF file (BaseLinker interface).
583
+
584
+ For AVR: Generates .hex (Intel HEX format)
585
+
586
+ Args:
587
+ elf_path: Path to firmware.elf file
588
+
589
+ Returns:
590
+ Path to generated binary file (.hex for AVR)
591
+
592
+ Raises:
593
+ LinkerError: If binary generation fails
594
+ """
595
+ hex_path = elf_path.with_suffix('.hex')
596
+
597
+ if not self._objcopy_hex(elf_path, hex_path):
598
+ raise LinkerError(f"Failed to generate HEX from {elf_path}")
599
+
600
+ return hex_path
601
+
602
+ def link_with_options(
603
+ self,
604
+ sketch_objects: List[Path],
605
+ core_objects: List[Path],
606
+ output_elf: Path,
607
+ output_hex: Path,
608
+ lib_archives: Optional[List[Path]] = None,
609
+ extra_flags: Optional[List[str]] = None,
610
+ additional_objects: Optional[List[Path]] = None
611
+ ) -> LinkResult:
612
+ """Link object files into firmware (legacy method with full options).
613
+
614
+ This is the original link method with all the AVR-specific options.
615
+ Process:
616
+ 1. Link sketch objects + core objects + additional objects + library archives to create .elf
617
+ 2. Convert .elf to .hex using avr-objcopy
618
+ 3. Get size information using avr-size
619
+
620
+ Note: Core objects are passed directly instead of being archived because
621
+ LTO with -fno-fat-lto-objects produces bytecode-only objects that don't
622
+ work well in archives (the archive won't have a proper symbol index).
623
+
624
+ Args:
625
+ sketch_objects: User sketch object files
626
+ core_objects: Arduino core object files
627
+ output_elf: Output .elf file path
628
+ output_hex: Output .hex file path
629
+ lib_archives: Optional list of library archive (.a) files
630
+ extra_flags: Additional linker flags
631
+ additional_objects: Optional additional object files (e.g., library objects for LTO)
632
+
633
+ Returns:
634
+ LinkResult with linking status and size info
635
+ """
636
+ try:
637
+ # Create build directory if needed
638
+ output_elf.parent.mkdir(parents=True, exist_ok=True)
639
+
640
+ # Link to .elf - pass core objects directly instead of archiving
641
+ link_result = self._link_elf(
642
+ sketch_objects,
643
+ core_objects,
644
+ output_elf,
645
+ lib_archives or [],
646
+ extra_flags or [],
647
+ additional_objects
648
+ )
649
+
650
+ if not link_result or link_result.returncode != 0:
651
+ return LinkResult(
652
+ success=False,
653
+ elf_path=None,
654
+ hex_path=None,
655
+ size_info=None,
656
+ stdout=link_result.stdout if link_result else '',
657
+ stderr=link_result.stderr if link_result else 'Linking failed'
658
+ )
659
+
660
+ # Step 3: Convert to .hex
661
+ if not self._objcopy_hex(output_elf, output_hex):
662
+ return LinkResult(
663
+ success=False,
664
+ elf_path=output_elf if output_elf.exists() else None,
665
+ hex_path=None,
666
+ size_info=None,
667
+ stdout=link_result.stdout,
668
+ stderr='Failed to convert ELF to HEX'
669
+ )
670
+
671
+ # Step 4: Get size info
672
+ size_info = self._get_size(output_elf)
673
+
674
+ # Check for flash overflow
675
+ if size_info and self.max_flash and size_info.total_flash > self.max_flash:
676
+ return LinkResult(
677
+ success=False,
678
+ elf_path=output_elf,
679
+ hex_path=output_hex,
680
+ size_info=size_info,
681
+ stdout=link_result.stdout,
682
+ stderr=f'Sketch too large: {size_info.total_flash} bytes (maximum is {self.max_flash} bytes)'
683
+ )
684
+
685
+ return LinkResult(
686
+ success=True,
687
+ elf_path=output_elf,
688
+ hex_path=output_hex,
689
+ size_info=size_info,
690
+ stdout=link_result.stdout,
691
+ stderr=link_result.stderr
692
+ )
693
+
694
+ except KeyboardInterrupt as ke:
695
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
696
+ handle_keyboard_interrupt_properly(ke)
697
+ raise # Never reached, but satisfies type checker
698
+ except Exception as e:
699
+ return LinkResult(
700
+ success=False,
701
+ elf_path=None,
702
+ hex_path=None,
703
+ size_info=None,
704
+ stdout='',
705
+ stderr=f'Linking exception: {str(e)}'
706
+ )
707
+
708
+