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,436 @@
1
+ """Toolchain management for AVR-GCC.
2
+
3
+ This module handles downloading, extracting, and managing the AVR-GCC
4
+ toolchain required for building Arduino sketches.
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any, Dict, Optional
10
+
11
+ from .cache import Cache
12
+ from .downloader import PackageDownloader
13
+ from .package import IToolchain, PackageError
14
+ from .platform_utils import PlatformDetector, PlatformError
15
+
16
+
17
+ class ToolchainError(PackageError):
18
+ """Raised when toolchain operations fail."""
19
+
20
+ pass
21
+
22
+
23
+ class ToolchainAVR(IToolchain):
24
+ """Manages AVR-GCC toolchain."""
25
+
26
+ # AVR-GCC version used by Arduino
27
+ VERSION = "7.3.0-atmel3.6.1-arduino7"
28
+
29
+ # Base URL for toolchain downloads
30
+ BASE_URL = "https://downloads.arduino.cc/tools"
31
+
32
+ # Platform-specific toolchain packages
33
+ PACKAGES = {
34
+ "windows": {
35
+ "x86_64": "avr-gcc-7.3.0-atmel3.6.1-arduino7-i686-w64-mingw32.zip",
36
+ "checksum": "a54f64755fff4cb792a1495e5defdd789902a2a3503982e81b898299cf39800e",
37
+ },
38
+ "linux": {
39
+ "x86_64": "avr-gcc-7.3.0-atmel3.6.1-arduino7-x86_64-pc-linux-gnu.tar.bz2",
40
+ "checksum": "bd8c37f6952a2130ac9ee32c53f6a660feb79bee8353c8e289eb60fdcefed91e",
41
+ "i686": "avr-gcc-7.3.0-atmel3.6.1-arduino7-i686-pc-linux-gnu.tar.bz2",
42
+ "aarch64": "avr-gcc-7.3.0-atmel3.6.1-arduino7-aarch64-pc-linux-gnu.tar.bz2",
43
+ "armv7l": "avr-gcc-7.3.0-atmel3.6.1-arduino7-arm-linux-gnueabihf.tar.bz2",
44
+ },
45
+ "darwin": {
46
+ "x86_64": "avr-gcc-7.3.0-atmel3.6.1-arduino7-x86_64-apple-darwin14.tar.bz2",
47
+ "checksum": "4c9ca2d87b5c1b5c82f567a9bfc0fdaef57fe8b9f74bae1e32b3e1964612d85e",
48
+ },
49
+ }
50
+
51
+ # Required tools (executables in bin/)
52
+ REQUIRED_TOOLS = [
53
+ "avr-gcc",
54
+ "avr-g++",
55
+ "avr-ar",
56
+ "avr-objcopy",
57
+ "avr-size",
58
+ "avr-nm",
59
+ "avr-objdump",
60
+ "avr-ranlib",
61
+ "avr-strip",
62
+ ]
63
+
64
+ # Required subdirectories (libs and headers)
65
+ REQUIRED_DIRS = [
66
+ "bin", # Executables
67
+ "avr/include", # AVR C library headers
68
+ "lib/gcc/avr", # GCC AVR libraries
69
+ ]
70
+
71
+ # Key header files that must exist
72
+ REQUIRED_HEADERS = [
73
+ "avr/include/avr/io.h",
74
+ "avr/include/avr/interrupt.h",
75
+ "avr/include/stdio.h",
76
+ "avr/include/stdlib.h",
77
+ "avr/include/string.h",
78
+ ]
79
+
80
+ # Key library files patterns
81
+ REQUIRED_LIB_PATTERNS = [
82
+ "lib/gcc/avr/*/libgcc.a",
83
+ "avr/lib/libc.a",
84
+ "avr/lib/libm.a",
85
+ ]
86
+
87
+ def __init__(self, cache: Cache):
88
+ """Initialize toolchain manager.
89
+
90
+ Args:
91
+ cache: Cache instance for storing toolchain
92
+ """
93
+ self.cache = cache
94
+ self.downloader = PackageDownloader()
95
+ self._toolchain_path: Optional[Path] = None
96
+
97
+ @staticmethod
98
+ def detect_platform() -> tuple[str, str]:
99
+ """Detect host platform and architecture.
100
+
101
+ Returns:
102
+ Tuple of (platform, architecture)
103
+ Platform: 'windows', 'linux', or 'darwin'
104
+ Architecture: 'x86_64', 'i686', 'aarch64', 'armv7l'
105
+
106
+ Raises:
107
+ ToolchainError: If platform is not supported
108
+ """
109
+ try:
110
+ return PlatformDetector.detect_avr_platform()
111
+ except PlatformError as e:
112
+ raise ToolchainError(str(e))
113
+
114
+ def _get_package_details(self) -> tuple[str, Optional[str]]:
115
+ """Get package filename and checksum for current platform.
116
+
117
+ Returns:
118
+ Tuple of (package_filename, checksum)
119
+
120
+ Raises:
121
+ ToolchainError: If no package available for platform
122
+ """
123
+ plat, arch = self.detect_platform()
124
+
125
+ if plat not in self.PACKAGES:
126
+ raise ToolchainError(f"No toolchain package for platform: {plat}")
127
+
128
+ platform_packages = self.PACKAGES[plat]
129
+
130
+ # For Windows and macOS, only x86_64 is available
131
+ if plat in ("windows", "darwin"):
132
+ if arch != "x86_64":
133
+ # Try to use x86_64 package anyway
134
+ arch = "x86_64"
135
+
136
+ if arch not in platform_packages:
137
+ # Try x86_64 as fallback
138
+ if "x86_64" in platform_packages:
139
+ arch = "x86_64"
140
+ else:
141
+ raise ToolchainError(f"No toolchain package for {plat}/{arch}. " + f"Available: {list(platform_packages.keys())}")
142
+
143
+ package_name = platform_packages[arch]
144
+ checksum = platform_packages.get("checksum")
145
+
146
+ return package_name, checksum
147
+
148
+ def ensure_toolchain(self, force_download: bool = False) -> Path:
149
+ """Ensure toolchain is available, downloading if necessary.
150
+
151
+ Args:
152
+ force_download: Force re-download even if cached
153
+
154
+ Returns:
155
+ Path to toolchain root directory
156
+
157
+ Raises:
158
+ ToolchainError: If toolchain cannot be obtained or verified
159
+ """
160
+ # Check if already loaded
161
+ if self._toolchain_path and not force_download:
162
+ return self._toolchain_path
163
+
164
+ # Get package info
165
+ package_name, checksum = self._get_package_details()
166
+
167
+ # Use URL and version for cache path
168
+ url = f"{self.BASE_URL}/{package_name}"
169
+ toolchain_path = self.cache.get_toolchain_path(self.BASE_URL, self.VERSION)
170
+ package_path = self.cache.get_package_path(self.BASE_URL, self.VERSION, package_name)
171
+
172
+ # Check if already extracted and verified
173
+ if not force_download and self.cache.is_toolchain_cached(self.BASE_URL, self.VERSION):
174
+ # Comprehensive verification
175
+ if self._verify_toolchain(toolchain_path):
176
+ self._toolchain_path = toolchain_path
177
+ return toolchain_path
178
+ else:
179
+ print("Cached toolchain failed validation, re-downloading...")
180
+
181
+ # Need to download and extract
182
+ self.cache.ensure_directories()
183
+
184
+ print(f"Downloading AVR-GCC toolchain ({self.VERSION})...")
185
+
186
+ try:
187
+ # Ensure package directory exists
188
+ package_path.parent.mkdir(parents=True, exist_ok=True)
189
+
190
+ # Download if not cached
191
+ if force_download or not package_path.exists():
192
+ self.downloader.download(url, package_path, checksum)
193
+ else:
194
+ print(f"Using cached {package_name}")
195
+
196
+ # Extract
197
+ print("Extracting toolchain...")
198
+ toolchain_path.parent.mkdir(parents=True, exist_ok=True)
199
+
200
+ # Extract to a temporary location first
201
+ import shutil
202
+ import tempfile
203
+
204
+ with tempfile.TemporaryDirectory() as temp_dir:
205
+ temp_path = Path(temp_dir)
206
+ self.downloader.extract_archive(package_path, temp_path, show_progress=False)
207
+
208
+ # Find the actual toolchain directory (may be nested)
209
+ extracted_dirs = list(temp_path.iterdir())
210
+ if len(extracted_dirs) == 1 and extracted_dirs[0].is_dir():
211
+ # Single directory extracted, use it
212
+ src_dir = extracted_dirs[0]
213
+ else:
214
+ # Multiple items extracted, use the temp dir itself
215
+ src_dir = temp_path
216
+
217
+ # Move to final location
218
+ if toolchain_path.exists():
219
+ shutil.rmtree(toolchain_path)
220
+ shutil.move(str(src_dir), str(toolchain_path))
221
+
222
+ # Comprehensive verification
223
+ if not self._verify_toolchain(toolchain_path):
224
+ raise ToolchainError("Toolchain verification failed after extraction")
225
+
226
+ self._toolchain_path = toolchain_path
227
+ print(f"Toolchain ready at {toolchain_path}")
228
+ return toolchain_path
229
+
230
+ except KeyboardInterrupt as ke:
231
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
232
+
233
+ handle_keyboard_interrupt_properly(ke)
234
+ raise # Never reached, but satisfies type checker
235
+ except Exception as e:
236
+ raise ToolchainError(f"Failed to setup toolchain: {e}")
237
+
238
+ def _verify_toolchain(self, toolchain_path: Path) -> bool:
239
+ """Comprehensively verify toolchain installation.
240
+
241
+ Checks for:
242
+ - All required executables in bin/
243
+ - Required directories (lib, include, etc.)
244
+ - Key header files
245
+ - Key library files
246
+
247
+ Args:
248
+ toolchain_path: Path to toolchain directory
249
+
250
+ Returns:
251
+ True if toolchain is complete and valid
252
+ """
253
+ # Check required directories
254
+ for dir_path in self.REQUIRED_DIRS:
255
+ if not (toolchain_path / dir_path).exists():
256
+ print(f"Missing directory: {dir_path}")
257
+ return False
258
+
259
+ # Check required executables
260
+ bin_dir = toolchain_path / "bin"
261
+ exe_suffix = ".exe" if sys.platform == "win32" else ""
262
+
263
+ for tool in self.REQUIRED_TOOLS:
264
+ tool_path = bin_dir / f"{tool}{exe_suffix}"
265
+ if not tool_path.exists():
266
+ print(f"Missing tool: {tool}")
267
+ return False
268
+
269
+ # Check required headers
270
+ for header in self.REQUIRED_HEADERS:
271
+ header_path = toolchain_path / header
272
+ if not header_path.exists():
273
+ print(f"Missing header: {header}")
274
+ return False
275
+
276
+ # Check required libraries (using glob patterns)
277
+ for lib_pattern in self.REQUIRED_LIB_PATTERNS:
278
+ lib_paths = list(toolchain_path.glob(lib_pattern))
279
+ if not lib_paths:
280
+ print(f"Missing library matching: {lib_pattern}")
281
+ return False
282
+
283
+ return True
284
+
285
+ def get_tool_path(self, tool_name: str) -> Path:
286
+ """Get path to a specific tool.
287
+
288
+ Args:
289
+ tool_name: Name of the tool (e.g., 'avr-gcc')
290
+
291
+ Returns:
292
+ Path to the tool executable
293
+
294
+ Raises:
295
+ ToolchainError: If toolchain not initialized or tool not found
296
+ """
297
+ if not self._toolchain_path:
298
+ raise ToolchainError("Toolchain not initialized. Call ensure_toolchain() first.")
299
+
300
+ exe_suffix = ".exe" if sys.platform == "win32" else ""
301
+ tool_path = self._toolchain_path / "bin" / f"{tool_name}{exe_suffix}"
302
+
303
+ if not tool_path.exists():
304
+ raise ToolchainError(f"Tool not found: {tool_name}")
305
+
306
+ return tool_path
307
+
308
+ def get_all_tools(self) -> Dict[str, Path]:
309
+ """Get paths to all required tools.
310
+
311
+ Returns:
312
+ Dictionary mapping tool names to their paths
313
+
314
+ Raises:
315
+ ToolchainError: If toolchain not initialized
316
+ """
317
+ if not self._toolchain_path:
318
+ raise ToolchainError("Toolchain not initialized. Call ensure_toolchain() first.")
319
+
320
+ return {tool: self.get_tool_path(tool) for tool in self.REQUIRED_TOOLS}
321
+
322
+ # Implement BaseToolchain interface
323
+ def get_gcc_path(self) -> Optional[Path]:
324
+ """Get path to GCC compiler.
325
+
326
+ Returns:
327
+ Path to gcc binary or None if not found
328
+ """
329
+ try:
330
+ return self.get_tool_path("avr-gcc")
331
+ except ToolchainError:
332
+ return None
333
+
334
+ def get_gxx_path(self) -> Optional[Path]:
335
+ """Get path to G++ compiler.
336
+
337
+ Returns:
338
+ Path to g++ binary or None if not found
339
+ """
340
+ try:
341
+ return self.get_tool_path("avr-g++")
342
+ except ToolchainError:
343
+ return None
344
+
345
+ def get_ar_path(self) -> Optional[Path]:
346
+ """Get path to archiver (ar).
347
+
348
+ Returns:
349
+ Path to ar binary or None if not found
350
+ """
351
+ try:
352
+ return self.get_tool_path("avr-ar")
353
+ except ToolchainError:
354
+ return None
355
+
356
+ def get_objcopy_path(self) -> Optional[Path]:
357
+ """Get path to objcopy utility.
358
+
359
+ Returns:
360
+ Path to objcopy binary or None if not found
361
+ """
362
+ try:
363
+ return self.get_tool_path("avr-objcopy")
364
+ except ToolchainError:
365
+ return None
366
+
367
+ def get_size_path(self) -> Optional[Path]:
368
+ """Get path to size utility.
369
+
370
+ Returns:
371
+ Path to size binary or None if not found
372
+ """
373
+ try:
374
+ return self.get_tool_path("avr-size")
375
+ except ToolchainError:
376
+ return None
377
+
378
+ def get_bin_dir(self) -> Optional[Path]:
379
+ """Get path to toolchain bin directory.
380
+
381
+ Returns:
382
+ Path to bin directory containing compiler binaries
383
+ """
384
+ if not self._toolchain_path:
385
+ return None
386
+ return self._toolchain_path / "bin"
387
+
388
+ def is_installed(self) -> bool:
389
+ """Check if toolchain is already installed.
390
+
391
+ Returns:
392
+ True if toolchain directory exists and is valid
393
+ """
394
+ if not self._toolchain_path:
395
+ # Try to get from cache
396
+ toolchain_path = self.cache.get_toolchain_path(self.BASE_URL, self.VERSION)
397
+ if not toolchain_path.exists():
398
+ return False
399
+ # Verify it
400
+ if self._verify_toolchain(toolchain_path):
401
+ self._toolchain_path = toolchain_path
402
+ return True
403
+ return False
404
+ return self._verify_toolchain(self._toolchain_path)
405
+
406
+ # Implement BasePackage interface
407
+ def ensure_package(self) -> Path:
408
+ """Ensure package is downloaded and extracted.
409
+
410
+ Returns:
411
+ Path to the extracted package directory
412
+
413
+ Raises:
414
+ PackageError: If download or extraction fails
415
+ """
416
+ return self.ensure_toolchain()
417
+
418
+ def get_package_info(self) -> Dict[str, Any]:
419
+ """Get information about the package.
420
+
421
+ Returns:
422
+ Dictionary with package metadata (version, path, etc.)
423
+ """
424
+ info = {
425
+ "type": "toolchain",
426
+ "platform": "avr",
427
+ "version": self.VERSION,
428
+ "base_url": self.BASE_URL,
429
+ "installed": self._toolchain_path is not None,
430
+ }
431
+
432
+ if self._toolchain_path:
433
+ info["path"] = str(self._toolchain_path)
434
+ info["tools"] = {name: str(path) for name, path in self.get_all_tools().items()}
435
+
436
+ return info
@@ -0,0 +1,196 @@
1
+ """Toolchain Binary Finder Utilities.
2
+
3
+ This module provides utilities for locating and verifying toolchain binaries
4
+ in the toolchain installation directory.
5
+
6
+ Binary Naming Conventions:
7
+ - AVR: avr-gcc, avr-g++, avr-ar, avr-objcopy, etc.
8
+ - RISC-V ESP32: riscv32-esp-elf-gcc, riscv32-esp-elf-g++, etc.
9
+ - Xtensa ESP32: xtensa-esp32-elf-gcc, xtensa-esp32-elf-g++, etc.
10
+
11
+ Directory Structure:
12
+ The toolchain binaries are typically located in:
13
+ - toolchain_path/bin/bin/ (nested bin directory after extraction)
14
+ - toolchain_path/bin/{toolchain_name}/bin/ (e.g., bin/riscv32-esp-elf/bin/)
15
+ """
16
+
17
+ from pathlib import Path
18
+ from typing import Dict, List, Optional
19
+
20
+
21
+ class BinaryNotFoundError(Exception):
22
+ """Raised when a required toolchain binary is not found."""
23
+
24
+ pass
25
+
26
+
27
+ class ToolchainBinaryFinder:
28
+ """Finds and verifies toolchain binaries in the installation directory."""
29
+
30
+ def __init__(self, toolchain_path: Path, binary_prefix: str):
31
+ """Initialize the binary finder.
32
+
33
+ Args:
34
+ toolchain_path: Base path to the toolchain installation
35
+ binary_prefix: Binary name prefix (e.g., "avr", "riscv32-esp-elf", "xtensa-esp32-elf")
36
+ """
37
+ self.toolchain_path = toolchain_path
38
+ self.binary_prefix = binary_prefix
39
+
40
+ def find_bin_dir(self) -> Optional[Path]:
41
+ """Find the bin directory containing toolchain binaries.
42
+
43
+ Searches for binaries in common locations:
44
+ 1. toolchain_path/bin/bin/ (nested bin, common after extraction)
45
+ 2. toolchain_path/bin/{toolchain_name}/bin/ (nested toolchain directory)
46
+ 3. toolchain_path/bin/ (direct bin directory)
47
+
48
+ Returns:
49
+ Path to bin directory, or None if not found
50
+ """
51
+ # The toolchain structure is: toolchain_path/bin/bin/
52
+ # (after extraction, the toolchain extracts to a subdirectory,
53
+ # and we copy it to toolchain_path/bin/)
54
+ bin_parent = self.toolchain_path.parent / "bin"
55
+
56
+ if not bin_parent.exists():
57
+ return None
58
+
59
+ # Check for bin/bin/ (most common after extraction)
60
+ bin_dir = bin_parent / "bin"
61
+ if bin_dir.exists() and bin_dir.is_dir():
62
+ # Verify it has binaries
63
+ binaries = list(bin_dir.glob("*.exe")) or list(bin_dir.glob("*-gcc"))
64
+ if binaries:
65
+ return bin_dir
66
+
67
+ # Look for nested toolchain directory (e.g., bin/riscv32-esp-elf/bin/)
68
+ for item in bin_parent.iterdir():
69
+ if item.is_dir() and "esp" in item.name.lower():
70
+ nested_bin = item / "bin"
71
+ if nested_bin.exists():
72
+ return nested_bin
73
+
74
+ # Check if bin_parent itself has binaries
75
+ binaries = list(bin_parent.glob("*.exe")) or list(bin_parent.glob("*-gcc"))
76
+ if binaries:
77
+ return bin_parent
78
+
79
+ return None
80
+
81
+ def find_binary(self, binary_name: str) -> Optional[Path]:
82
+ """Find a specific binary in the toolchain bin directory.
83
+
84
+ Args:
85
+ binary_name: Name of the binary without prefix (e.g., "gcc", "g++", "ar")
86
+
87
+ Returns:
88
+ Path to the binary, or None if not found
89
+ """
90
+ bin_dir = self.find_bin_dir()
91
+ if bin_dir is None or not bin_dir.exists():
92
+ return None
93
+
94
+ # Construct full binary name with prefix
95
+ binary_with_prefix = f"{self.binary_prefix}-{binary_name}"
96
+
97
+ # Check both with and without .exe extension (Windows compatibility)
98
+ for ext in [".exe", ""]:
99
+ binary_path = bin_dir / f"{binary_with_prefix}{ext}"
100
+ if binary_path.exists():
101
+ return binary_path
102
+
103
+ return None
104
+
105
+ def find_all_binaries(self, binary_names: List[str]) -> Dict[str, Optional[Path]]:
106
+ """Find multiple binaries at once.
107
+
108
+ Args:
109
+ binary_names: List of binary names without prefix (e.g., ["gcc", "g++", "ar"])
110
+
111
+ Returns:
112
+ Dictionary mapping binary names to their paths (None if not found)
113
+ """
114
+ return {name: self.find_binary(name) for name in binary_names}
115
+
116
+ def get_common_tool_paths(self) -> Dict[str, Optional[Path]]:
117
+ """Get paths to common toolchain binaries.
118
+
119
+ Returns:
120
+ Dictionary mapping tool names to their paths
121
+ """
122
+ common_tools = ["gcc", "g++", "ar", "objcopy", "size", "objdump"]
123
+ return self.find_all_binaries(common_tools)
124
+
125
+ def verify_binary_exists(self, binary_name: str) -> bool:
126
+ """Verify that a specific binary exists.
127
+
128
+ Args:
129
+ binary_name: Name of the binary without prefix
130
+
131
+ Returns:
132
+ True if binary exists and is a file
133
+ """
134
+ binary_path = self.find_binary(binary_name)
135
+ return binary_path is not None and binary_path.exists() and binary_path.is_file()
136
+
137
+ def verify_required_binaries(self, required_binaries: List[str]) -> tuple[bool, List[str]]:
138
+ """Verify that all required binaries exist.
139
+
140
+ Args:
141
+ required_binaries: List of required binary names without prefix
142
+
143
+ Returns:
144
+ Tuple of (all_found, missing_binaries)
145
+ - all_found: True if all binaries were found
146
+ - missing_binaries: List of binary names that were not found
147
+ """
148
+ missing = []
149
+ for binary_name in required_binaries:
150
+ if not self.verify_binary_exists(binary_name):
151
+ missing.append(binary_name)
152
+
153
+ return len(missing) == 0, missing
154
+
155
+ def verify_installation(self) -> bool:
156
+ """Verify that the toolchain is properly installed.
157
+
158
+ Checks for essential binaries: gcc, g++, ar, objcopy
159
+
160
+ Returns:
161
+ True if all essential binaries are present
162
+
163
+ Raises:
164
+ BinaryNotFoundError: If essential binaries are missing
165
+ """
166
+ required_tools = ["gcc", "g++", "ar", "objcopy"]
167
+ all_found, missing = self.verify_required_binaries(required_tools)
168
+
169
+ if not all_found:
170
+ raise BinaryNotFoundError(f"Toolchain installation incomplete. Missing binaries: {', '.join(missing)}")
171
+
172
+ return True
173
+
174
+ def get_gcc_path(self) -> Optional[Path]:
175
+ """Get path to GCC compiler."""
176
+ return self.find_binary("gcc")
177
+
178
+ def get_gxx_path(self) -> Optional[Path]:
179
+ """Get path to G++ compiler."""
180
+ return self.find_binary("g++")
181
+
182
+ def get_ar_path(self) -> Optional[Path]:
183
+ """Get path to archiver (ar)."""
184
+ return self.find_binary("ar")
185
+
186
+ def get_objcopy_path(self) -> Optional[Path]:
187
+ """Get path to objcopy utility."""
188
+ return self.find_binary("objcopy")
189
+
190
+ def get_size_path(self) -> Optional[Path]:
191
+ """Get path to size utility."""
192
+ return self.find_binary("size")
193
+
194
+ def get_objdump_path(self) -> Optional[Path]:
195
+ """Get path to objdump utility."""
196
+ return self.find_binary("objdump")