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