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,422 @@
1
+ """Compilation Executor.
2
+
3
+ This module handles executing compilation commands via subprocess with support
4
+ for response files and proper error handling.
5
+
6
+ Design:
7
+ - Wraps subprocess.run for compilation commands
8
+ - Generates response files for include paths (avoids command line length limits)
9
+ - Provides clear error messages for compilation failures
10
+ - Supports both C and C++ compilation
11
+ - Integrates sccache for compilation caching
12
+ - Uses header trampoline cache to avoid Windows command-line length limits
13
+ """
14
+
15
+ import _thread
16
+ import subprocess
17
+ import shutil
18
+ import platform
19
+ import time
20
+ from pathlib import Path
21
+ from typing import List, Optional, TYPE_CHECKING
22
+
23
+ from ..packages.header_trampoline_cache import HeaderTrampolineCache
24
+
25
+ if TYPE_CHECKING:
26
+ from ..daemon.compilation_queue import CompilationJobQueue
27
+
28
+
29
+ class CompilationError(Exception):
30
+ """Raised when compilation operations fail."""
31
+ pass
32
+
33
+
34
+ class CompilationExecutor:
35
+ """Executes compilation commands with response file support.
36
+
37
+ This class handles:
38
+ - Running compiler subprocess commands
39
+ - Generating response files for include paths
40
+ - Handling compilation errors with clear messages
41
+ - Supporting progress display
42
+ """
43
+
44
+ def __init__(self, build_dir: Path, show_progress: bool = True, use_sccache: bool = True, use_trampolines: bool = True):
45
+ """Initialize compilation executor.
46
+
47
+ Args:
48
+ build_dir: Build directory for response files
49
+ show_progress: Whether to show compilation progress
50
+ use_sccache: Whether to use sccache for caching (default: True)
51
+ use_trampolines: Whether to use header trampolines on Windows (default: True)
52
+ """
53
+ self.build_dir = build_dir
54
+ self.show_progress = show_progress
55
+
56
+ # Disable sccache on Windows due to file locking issues
57
+ # See: https://github.com/anthropics/claude-code/issues/...
58
+ if platform.system() == 'Windows':
59
+ if use_sccache and show_progress:
60
+ print("[sccache] Disabled on Windows due to file locking issues")
61
+ self.use_sccache = False
62
+ else:
63
+ self.use_sccache = use_sccache
64
+
65
+ self.use_trampolines = use_trampolines
66
+ self.sccache_path: Optional[Path] = None
67
+ self.trampoline_cache: Optional[HeaderTrampolineCache] = None
68
+
69
+ # Check if sccache is available
70
+ if self.use_sccache:
71
+ sccache_exe = shutil.which("sccache")
72
+ if sccache_exe:
73
+ self.sccache_path = Path(sccache_exe)
74
+ # Always print sccache status for visibility
75
+ print(f"[sccache] Enabled: {self.sccache_path}")
76
+ else:
77
+ # Try common Windows locations (Git Bash uses /c/ paths)
78
+ common_locations = [
79
+ Path("/c/tools/python13/Scripts/sccache.exe"),
80
+ Path("C:/tools/python13/Scripts/sccache.exe"),
81
+ Path.home() / ".cargo" / "bin" / "sccache.exe",
82
+ ]
83
+ for loc in common_locations:
84
+ if loc.exists():
85
+ self.sccache_path = loc
86
+ print(f"[sccache] Enabled: {self.sccache_path}")
87
+ break
88
+ else:
89
+ # Always warn if sccache not found
90
+ print("[sccache] Warning: not found in PATH, proceeding without cache")
91
+
92
+ # Initialize trampoline cache if enabled and on Windows
93
+ if self.use_trampolines and platform.system() == 'Windows':
94
+ self.trampoline_cache = HeaderTrampolineCache(show_progress=show_progress)
95
+
96
+ def compile_source(
97
+ self,
98
+ compiler_path: Path,
99
+ source_path: Path,
100
+ output_path: Path,
101
+ compile_flags: List[str],
102
+ include_paths: List[Path]
103
+ ) -> Path:
104
+ """Compile a single source file.
105
+
106
+ Args:
107
+ compiler_path: Path to compiler executable (gcc/g++)
108
+ source_path: Path to source file
109
+ output_path: Path for output object file
110
+ compile_flags: Compilation flags
111
+ include_paths: Include directory paths
112
+
113
+ Returns:
114
+ Path to generated object file
115
+
116
+ Raises:
117
+ CompilationError: If compilation fails
118
+ """
119
+ if not compiler_path.exists():
120
+ raise CompilationError(
121
+ f"Compiler not found: {compiler_path}. Ensure toolchain is installed."
122
+ )
123
+
124
+ if not source_path.exists():
125
+ raise CompilationError(f"Source file not found: {source_path}")
126
+
127
+ # Ensure output directory exists
128
+ output_path.parent.mkdir(parents=True, exist_ok=True)
129
+
130
+ # Apply header trampoline cache on Windows when enabled
131
+ # This resolves Windows CreateProcess 32K limit issues with sccache
132
+ effective_include_paths = include_paths
133
+ if self.trampoline_cache is not None and platform.system() == 'Windows':
134
+ # Use trampolines to shorten include paths
135
+ # Exclude ESP-IDF headers that use relative paths that break trampolines
136
+ try:
137
+ exclude_patterns = [
138
+ 'newlib/platform_include', # Uses #include_next which breaks trampolines
139
+ 'newlib\\platform_include', # Windows path variant
140
+ '/bt/', # Bluetooth SDK uses relative paths between bt/include and bt/controller
141
+ '\\bt\\' # Windows path variant
142
+ ]
143
+ effective_include_paths = self.trampoline_cache.generate_trampolines(
144
+ include_paths,
145
+ exclude_patterns=exclude_patterns
146
+ )
147
+ except KeyboardInterrupt:
148
+ _thread.interrupt_main()
149
+ raise
150
+ except Exception as e:
151
+ if self.show_progress:
152
+ print(f"[trampolines] Warning: Failed to generate trampolines, using original paths: {e}")
153
+ effective_include_paths = include_paths
154
+
155
+ # Convert include paths to flags - ensure no quotes for sccache compatibility
156
+ # GCC response files with quotes cause sccache to treat @file literally
157
+ include_flags = [f"-I{str(inc).replace(chr(92), '/')}" for inc in effective_include_paths]
158
+
159
+ # Build compiler command
160
+ cmd = self._build_compile_command(
161
+ compiler_path, source_path, output_path, compile_flags, include_flags
162
+ )
163
+
164
+ # Execute compilation
165
+ if self.show_progress:
166
+ print(f"Compiling {source_path.name}...")
167
+
168
+ try:
169
+ result = subprocess.run(
170
+ cmd,
171
+ capture_output=True,
172
+ text=True,
173
+ timeout=60
174
+ )
175
+
176
+ if result.returncode != 0:
177
+ error_msg = f"Compilation failed for {source_path.name}\n"
178
+ error_msg += f"stderr: {result.stderr}\n"
179
+ error_msg += f"stdout: {result.stdout}"
180
+ raise CompilationError(error_msg)
181
+
182
+ if self.show_progress and result.stderr:
183
+ print(result.stderr)
184
+
185
+ return output_path
186
+
187
+ except subprocess.TimeoutExpired as e:
188
+ raise CompilationError(f"Compilation timeout for {source_path.name}") from e
189
+ except KeyboardInterrupt as ke:
190
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
191
+ handle_keyboard_interrupt_properly(ke)
192
+ raise # Never reached, but satisfies type checker
193
+ except Exception as e:
194
+ if isinstance(e, CompilationError):
195
+ raise
196
+ raise CompilationError(f"Failed to compile {source_path.name}: {e}") from e
197
+
198
+ def _build_compile_command(
199
+ self,
200
+ compiler_path: Path,
201
+ source_path: Path,
202
+ output_path: Path,
203
+ compile_flags: List[str],
204
+ include_paths: List[str]
205
+ ) -> List[str]:
206
+ """Build compilation command with optional sccache wrapper.
207
+
208
+ Args:
209
+ compiler_path: Path to compiler executable
210
+ source_path: Path to source file
211
+ output_path: Path for output object file
212
+ compile_flags: Compilation flags
213
+ include_paths: Include paths (or include flags if already converted)
214
+
215
+ Returns:
216
+ List of command arguments
217
+ """
218
+ # Include paths are already converted to flags (List[str])
219
+ include_flags = include_paths
220
+
221
+ # Write response file for includes
222
+ response_file = self._write_response_file(include_flags)
223
+
224
+ # Build compiler command with optional sccache wrapper
225
+ use_sccache = self.sccache_path is not None
226
+
227
+ cmd = []
228
+ if use_sccache:
229
+ cmd.append(str(self.sccache_path))
230
+ # Use absolute resolved path for sccache
231
+ # On Windows, sccache needs consistent path format (all backslashes)
232
+ resolved_compiler = compiler_path.resolve()
233
+ compiler_str = str(resolved_compiler)
234
+ # Normalize to Windows backslashes on Windows
235
+ if platform.system() == 'Windows':
236
+ compiler_str = compiler_str.replace('/', '\\')
237
+ cmd.append(compiler_str)
238
+ else:
239
+ cmd.append(str(compiler_path))
240
+ cmd.extend(compile_flags)
241
+ cmd.append(f"@{response_file}")
242
+ cmd.extend(['-c', str(source_path)])
243
+ cmd.extend(['-o', str(output_path)])
244
+
245
+ return cmd
246
+
247
+ def _write_response_file(self, include_flags: List[str]) -> Path:
248
+ """Write include paths to response file.
249
+
250
+ Response files avoid command line length limits when there are
251
+ many include paths.
252
+
253
+ Args:
254
+ include_flags: List of -I include flags
255
+
256
+ Returns:
257
+ Path to generated response file
258
+ """
259
+ response_file = self.build_dir / "includes.rsp"
260
+ response_file.parent.mkdir(parents=True, exist_ok=True)
261
+
262
+ with open(response_file, 'w') as f:
263
+ f.write('\n'.join(include_flags))
264
+
265
+ return response_file
266
+
267
+ def preprocess_ino(
268
+ self,
269
+ ino_path: Path,
270
+ output_dir: Path
271
+ ) -> Path:
272
+ """Preprocess .ino file to .cpp file.
273
+
274
+ Simple preprocessing: adds Arduino.h include and renames to .cpp.
275
+
276
+ Args:
277
+ ino_path: Path to .ino file
278
+ output_dir: Directory for generated .cpp file
279
+
280
+ Returns:
281
+ Path to generated .cpp file
282
+
283
+ Raises:
284
+ CompilationError: If preprocessing fails
285
+ """
286
+ if not ino_path.exists():
287
+ raise CompilationError(f"Sketch file not found: {ino_path}")
288
+
289
+ # Read .ino content
290
+ try:
291
+ with open(ino_path, 'r', encoding='utf-8') as f:
292
+ ino_content = f.read()
293
+ except KeyboardInterrupt as ke:
294
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
295
+ handle_keyboard_interrupt_properly(ke)
296
+ raise # Never reached, but satisfies type checker
297
+ except Exception as e:
298
+ raise CompilationError(f"Failed to read {ino_path}: {e}") from e
299
+
300
+ # Generate .cpp file path
301
+ cpp_path = output_dir / "sketch" / f"{ino_path.stem}.ino.cpp"
302
+ cpp_path.parent.mkdir(parents=True, exist_ok=True)
303
+
304
+ # Simple preprocessing: add Arduino.h and content
305
+ cpp_content = '#include <Arduino.h>\n\n' + ino_content
306
+
307
+ # Write .cpp file
308
+ try:
309
+ with open(cpp_path, 'w', encoding='utf-8') as f:
310
+ f.write(cpp_content)
311
+ except KeyboardInterrupt as ke:
312
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
313
+ handle_keyboard_interrupt_properly(ke)
314
+ raise # Never reached, but satisfies type checker
315
+ except Exception as e:
316
+ raise CompilationError(f"Failed to write {cpp_path}: {e}") from e
317
+
318
+ if self.show_progress:
319
+ print(f"Preprocessed {ino_path.name} -> {cpp_path.name}")
320
+
321
+ return cpp_path
322
+
323
+ def compile_source_async(
324
+ self,
325
+ compiler_path: Path,
326
+ source_path: Path,
327
+ output_path: Path,
328
+ compile_flags: List[str],
329
+ include_paths: List[Path],
330
+ job_queue: 'CompilationJobQueue'
331
+ ) -> str:
332
+ """Compile a single source file asynchronously via daemon queue.
333
+
334
+ This method submits a compilation job to the daemon's CompilationJobQueue
335
+ for parallel execution instead of blocking on subprocess.run().
336
+
337
+ Args:
338
+ compiler_path: Path to compiler executable (gcc/g++)
339
+ source_path: Path to source file
340
+ output_path: Path for output object file
341
+ compile_flags: Compilation flags
342
+ include_paths: Include directory paths
343
+ job_queue: CompilationJobQueue from daemon
344
+
345
+ Returns:
346
+ Job ID string for tracking the compilation job
347
+
348
+ Raises:
349
+ CompilationError: If job submission fails
350
+ """
351
+ from ..daemon.compilation_queue import CompilationJob
352
+
353
+ if not compiler_path.exists():
354
+ raise CompilationError(
355
+ f"Compiler not found: {compiler_path}. Ensure toolchain is installed."
356
+ )
357
+
358
+ if not source_path.exists():
359
+ raise CompilationError(f"Source file not found: {source_path}")
360
+
361
+ # Ensure output directory exists
362
+ output_path.parent.mkdir(parents=True, exist_ok=True)
363
+
364
+ # Apply header trampoline cache on Windows when enabled
365
+ effective_include_paths = include_paths
366
+ if self.trampoline_cache is not None and platform.system() == 'Windows':
367
+ try:
368
+ exclude_patterns = [
369
+ 'newlib/platform_include',
370
+ 'newlib\\platform_include',
371
+ '/bt/',
372
+ '\\bt\\'
373
+ ]
374
+ effective_include_paths = self.trampoline_cache.generate_trampolines(
375
+ include_paths,
376
+ exclude_patterns=exclude_patterns
377
+ )
378
+ except KeyboardInterrupt:
379
+ _thread.interrupt_main()
380
+ raise
381
+ except Exception as e:
382
+ if self.show_progress:
383
+ print(f"[trampolines] Warning: Failed to generate trampolines, using original paths: {e}")
384
+ effective_include_paths = include_paths
385
+
386
+ # Convert include paths to flags
387
+ include_flags = [f"-I{str(inc).replace(chr(92), '/')}" for inc in effective_include_paths]
388
+ response_file = self._write_response_file(include_flags)
389
+
390
+ # Build compiler command with optional sccache wrapper
391
+ use_sccache = self.sccache_path is not None
392
+
393
+ cmd = []
394
+ if use_sccache:
395
+ cmd.append(str(self.sccache_path))
396
+ resolved_compiler = compiler_path.resolve()
397
+ compiler_str = str(resolved_compiler)
398
+ if platform.system() == 'Windows':
399
+ compiler_str = compiler_str.replace('/', '\\')
400
+ cmd.append(compiler_str)
401
+ else:
402
+ cmd.append(str(compiler_path))
403
+ cmd.extend(compile_flags)
404
+ cmd.append(f"@{response_file}")
405
+ cmd.extend(['-c', str(source_path)])
406
+ cmd.extend(['-o', str(output_path)])
407
+
408
+ # Create and submit compilation job
409
+ job_id = f"compile_{source_path.stem}_{int(time.time()*1000000)}"
410
+
411
+ job = CompilationJob(
412
+ job_id=job_id,
413
+ source_path=source_path,
414
+ output_path=output_path,
415
+ compiler_cmd=cmd,
416
+ response_file=response_file
417
+ )
418
+
419
+ # Submit to queue
420
+ job_queue.submit_job(job)
421
+
422
+ return job_id
@@ -0,0 +1,165 @@
1
+ """Abstract base classes for compilation components.
2
+
3
+ This module defines the interface for platform-specific compilers and linkers
4
+ to ensure consistent behavior across different platforms (AVR, ESP32, etc.).
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from pathlib import Path
9
+ from typing import List, Dict, Optional
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass
14
+ class CompileResult:
15
+ """Result of a compilation operation."""
16
+ success: bool
17
+ object_file: Optional[Path]
18
+ stdout: str
19
+ stderr: str
20
+ returncode: int
21
+
22
+
23
+ class CompilerError(Exception):
24
+ """Base exception for compilation errors."""
25
+ pass
26
+
27
+
28
+ class ICompiler(ABC):
29
+ """Interface for source code compilers.
30
+
31
+ This interface defines the common contract for all compiler implementations:
32
+ - AVR Compiler (avr-gcc/avr-g++)
33
+ - ESP32 Compiler (riscv32-esp-elf-gcc, xtensa-esp32-elf-gcc)
34
+ - Configurable Compiler (platform-agnostic)
35
+ """
36
+
37
+ @abstractmethod
38
+ def compile_source(
39
+ self,
40
+ source_path: Path,
41
+ output_path: Optional[Path] = None
42
+ ) -> Path:
43
+ """Compile a single source file to object file.
44
+
45
+ Args:
46
+ source_path: Path to .c or .cpp source file
47
+ output_path: Optional path for output .o file
48
+
49
+ Returns:
50
+ Path to generated .o file
51
+
52
+ Raises:
53
+ CompilerError: If compilation fails
54
+ """
55
+ pass
56
+
57
+ @abstractmethod
58
+ def get_include_paths(self) -> List[Path]:
59
+ """Get all include paths needed for compilation.
60
+
61
+ Returns:
62
+ List of include directory paths
63
+ """
64
+ pass
65
+
66
+ @abstractmethod
67
+ def get_compile_flags(self) -> Dict[str, List[str]]:
68
+ """Get compilation flags.
69
+
70
+ Returns:
71
+ Dictionary with 'cflags', 'cxxflags', and 'common' keys
72
+ """
73
+ pass
74
+
75
+ @abstractmethod
76
+ def needs_rebuild(self, source: Path, object_file: Path) -> bool:
77
+ """Check if source file needs to be recompiled.
78
+
79
+ Args:
80
+ source: Source file path
81
+ object_file: Object file path
82
+
83
+ Returns:
84
+ True if source is newer than object file or object doesn't exist
85
+ """
86
+ pass
87
+
88
+ @abstractmethod
89
+ def compile(
90
+ self,
91
+ source: Path,
92
+ output: Path,
93
+ extra_flags: Optional[List[str]] = None
94
+ ) -> CompileResult:
95
+ """Compile source file (auto-detects C vs C++).
96
+
97
+ Args:
98
+ source: Path to source file
99
+ output: Path to output .o object file
100
+ extra_flags: Additional compiler flags
101
+
102
+ Returns:
103
+ CompileResult with compilation status
104
+
105
+ Raises:
106
+ CompilerError: If compilation fails
107
+ """
108
+ pass
109
+
110
+
111
+ class ILinker(ABC):
112
+ """Interface for linkers.
113
+
114
+ This interface defines the common contract for all linker implementations:
115
+ - AVR Linker (avr-gcc linker)
116
+ - ESP32 Linker (riscv32/xtensa linker)
117
+ - Configurable Linker (platform-agnostic)
118
+ """
119
+
120
+ @abstractmethod
121
+ def link(
122
+ self,
123
+ sketch_objects: List[Path],
124
+ core_archive: Path,
125
+ output_elf: Optional[Path] = None,
126
+ library_archives: Optional[List[Path]] = None
127
+ ) -> Path:
128
+ """Link object files into firmware ELF.
129
+
130
+ Args:
131
+ sketch_objects: List of sketch object files
132
+ core_archive: Core archive file (core.a)
133
+ output_elf: Optional path for output .elf file
134
+ library_archives: Optional list of library archives
135
+
136
+ Returns:
137
+ Path to generated .elf file
138
+
139
+ Raises:
140
+ LinkerError: If linking fails
141
+ """
142
+ pass
143
+
144
+ @abstractmethod
145
+ def generate_bin(self, elf_path: Path) -> Path:
146
+ """Generate binary from ELF file.
147
+
148
+ For AVR: Generates .hex (Intel HEX format)
149
+ For ESP32: Generates .bin (raw binary)
150
+
151
+ Args:
152
+ elf_path: Path to firmware.elf file
153
+
154
+ Returns:
155
+ Path to generated binary file
156
+
157
+ Raises:
158
+ LinkerError: If binary generation fails
159
+ """
160
+ pass
161
+
162
+
163
+ class LinkerError(Exception):
164
+ """Base exception for linking errors."""
165
+ pass