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,131 @@
1
+ """Platform Detection Utilities.
2
+
3
+ This module provides utilities for detecting the current platform and architecture
4
+ for toolchain selection and package management.
5
+
6
+ Supported Platforms:
7
+ - Windows: win32, win64
8
+ - Linux: linux-amd64, linux-arm64, linux-armhf, linux-i686
9
+ - macOS: macos, macos-arm64
10
+ """
11
+
12
+ import platform
13
+ import sys
14
+ from typing import Literal, Tuple
15
+
16
+
17
+ class PlatformError(Exception):
18
+ """Raised when platform detection fails or platform is unsupported."""
19
+
20
+ pass
21
+
22
+
23
+ PlatformIdentifier = Literal[
24
+ "win32",
25
+ "win64",
26
+ "linux-amd64",
27
+ "linux-arm64",
28
+ "linux-armhf",
29
+ "linux-i686",
30
+ "macos",
31
+ "macos-arm64",
32
+ ]
33
+
34
+
35
+ class PlatformDetector:
36
+ """Detects the current platform and architecture for toolchain selection."""
37
+
38
+ @staticmethod
39
+ def detect_esp32_platform() -> str:
40
+ """Detect the current platform for ESP32 toolchain selection.
41
+
42
+ This format is used by ESP-IDF toolchain packages.
43
+
44
+ Returns:
45
+ Platform identifier (win32, win64, linux-amd64, linux-arm64, macos, macos-arm64)
46
+
47
+ Raises:
48
+ PlatformError: If platform is unsupported
49
+ """
50
+ system = platform.system().lower()
51
+ machine = platform.machine().lower()
52
+
53
+ if system == "windows":
54
+ # Check if 64-bit
55
+ return "win64" if sys.maxsize > 2**32 else "win32"
56
+ elif system == "linux":
57
+ if "aarch64" in machine or "arm64" in machine:
58
+ return "linux-arm64"
59
+ elif "arm" in machine:
60
+ # Check for hard float vs soft float
61
+ return "linux-armhf" # Default to hard float
62
+ elif "i686" in machine or "i386" in machine:
63
+ return "linux-i686"
64
+ else:
65
+ return "linux-amd64"
66
+ elif system == "darwin":
67
+ if "arm64" in machine or "aarch64" in machine:
68
+ return "macos-arm64"
69
+ else:
70
+ return "macos"
71
+ else:
72
+ raise PlatformError(f"Unsupported platform: {system} {machine}")
73
+
74
+ @staticmethod
75
+ def detect_avr_platform() -> Tuple[str, str]:
76
+ """Detect the current platform for AVR toolchain selection.
77
+
78
+ This format is used by Arduino AVR toolchain packages.
79
+
80
+ Returns:
81
+ Tuple of (platform, architecture)
82
+ Platform: 'windows', 'linux', or 'darwin'
83
+ Architecture: 'x86_64', 'i686', 'aarch64', 'armv7l'
84
+
85
+ Raises:
86
+ PlatformError: If platform is not supported
87
+ """
88
+ system = platform.system().lower()
89
+ machine = platform.machine().lower()
90
+
91
+ # Normalize platform name
92
+ if system == "windows":
93
+ plat = "windows"
94
+ elif system == "linux":
95
+ plat = "linux"
96
+ elif system == "darwin":
97
+ plat = "darwin"
98
+ else:
99
+ raise PlatformError(f"Unsupported platform: {system}")
100
+
101
+ # Normalize architecture
102
+ if machine in ("x86_64", "amd64"):
103
+ arch = "x86_64"
104
+ elif machine in ("i386", "i686"):
105
+ arch = "i686"
106
+ elif machine in ("aarch64", "arm64"):
107
+ arch = "aarch64"
108
+ elif machine.startswith("arm"):
109
+ arch = "armv7l"
110
+ else:
111
+ # Default to x86_64 if unknown
112
+ arch = "x86_64"
113
+
114
+ return plat, arch
115
+
116
+ @staticmethod
117
+ def get_platform_info() -> dict:
118
+ """Get detailed information about the current platform.
119
+
120
+ Returns:
121
+ Dictionary with platform information including system, machine, and Python info
122
+ """
123
+ return {
124
+ "system": platform.system(),
125
+ "machine": platform.machine(),
126
+ "platform": platform.platform(),
127
+ "python_version": platform.python_version(),
128
+ "is_64bit": sys.maxsize > 2**32,
129
+ "esp32_format": PlatformDetector.detect_esp32_platform(),
130
+ "avr_format": PlatformDetector.detect_avr_platform(),
131
+ }
@@ -0,0 +1,325 @@
1
+ """PlatformIO Registry client for downloading libraries.
2
+
3
+ This module provides access to the PlatformIO registry API for resolving
4
+ and downloading library dependencies.
5
+ """
6
+
7
+ import json
8
+ import re
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import requests
14
+
15
+ from fbuild.packages.downloader import PackageDownloader
16
+
17
+
18
+ class RegistryError(Exception):
19
+ """Exception raised for registry-related errors."""
20
+
21
+ pass
22
+
23
+
24
+ @dataclass
25
+ class LibrarySpec:
26
+ """Parsed library specification from platformio.ini lib_deps."""
27
+
28
+ owner: str
29
+ name: str
30
+ version: Optional[str] = None
31
+
32
+ @classmethod
33
+ def parse(cls, spec: str) -> "LibrarySpec":
34
+ """Parse a library specification string.
35
+
36
+ Supports formats:
37
+ - owner/name@version (e.g., fastled/FastLED@^3.7.8)
38
+ - owner/name (e.g., fastled/FastLED)
39
+ - name@version (e.g., FastLED@^3.7.8)
40
+ - name (e.g., FastLED)
41
+ - URL (e.g., https://github.com/FastLED/FastLED)
42
+
43
+ Args:
44
+ spec: Library specification string
45
+
46
+ Returns:
47
+ LibrarySpec instance
48
+
49
+ Raises:
50
+ RegistryError: If spec format is invalid
51
+ """
52
+ # Handle URLs - convert to owner/name format
53
+ if spec.startswith("http://") or spec.startswith("https://"):
54
+ # Extract owner/name from GitHub URL
55
+ match = re.search(r"github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$", spec)
56
+ if match:
57
+ owner, name = match.groups()
58
+ return cls(owner=owner, name=name, version=None)
59
+ raise RegistryError(f"Cannot parse URL as library spec: {spec}")
60
+
61
+ # Parse owner/name@version format
62
+ if "@" in spec:
63
+ lib_part, version = spec.rsplit("@", 1)
64
+ else:
65
+ lib_part = spec
66
+ version = None
67
+
68
+ # Split owner/name
69
+ if "/" in lib_part:
70
+ owner, name = lib_part.split("/", 1)
71
+ else:
72
+ # If no owner specified, we'll need to search registry
73
+ owner = ""
74
+ name = lib_part
75
+
76
+ return cls(owner=owner, name=name, version=version)
77
+
78
+ def __str__(self) -> str:
79
+ """String representation."""
80
+ result = f"{self.owner}/{self.name}" if self.owner else self.name
81
+ if self.version:
82
+ result += f"@{self.version}"
83
+ return result
84
+
85
+
86
+ @dataclass
87
+ class LibraryVersion:
88
+ """Information about a specific library version."""
89
+
90
+ version: str
91
+ download_url: str
92
+ homepage: Optional[str] = None
93
+ repository: Optional[str] = None
94
+
95
+
96
+ class PlatformIORegistry:
97
+ """Client for PlatformIO registry API."""
98
+
99
+ API_URL = "https://api.registry.platformio.org/v3"
100
+
101
+ def __init__(self, downloader: Optional[PackageDownloader] = None):
102
+ """Initialize registry client.
103
+
104
+ Args:
105
+ downloader: Optional package downloader instance
106
+ """
107
+ self.downloader = downloader or PackageDownloader()
108
+
109
+ def search_library(self, name: str) -> Optional[str]:
110
+ """Search for a library by name to find its owner.
111
+
112
+ Args:
113
+ name: Library name
114
+
115
+ Returns:
116
+ Owner name if found, None otherwise
117
+ """
118
+ try:
119
+ # Use search API
120
+ search_url = f"{self.API_URL}/search"
121
+ response = requests.get(search_url, params={"query": name}, timeout=10)
122
+ response.raise_for_status()
123
+
124
+ result = response.json()
125
+ items = result.get("items", [])
126
+
127
+ if items:
128
+ # Return first match owner
129
+ first = items[0]
130
+ owner_info = first.get("owner", {})
131
+ if isinstance(owner_info, dict):
132
+ return owner_info.get("username")
133
+ return owner_info
134
+
135
+ except KeyboardInterrupt as ke:
136
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
137
+
138
+ handle_keyboard_interrupt_properly(ke)
139
+ except Exception as e:
140
+ print(f"Warning: Could not search for library {name}: {e}")
141
+
142
+ return None
143
+
144
+ def get_library_info(self, owner: str, name: str) -> dict:
145
+ """Get library information from registry using search API.
146
+
147
+ Args:
148
+ owner: Library owner (user/org name)
149
+ name: Library name
150
+
151
+ Returns:
152
+ Library information dictionary
153
+
154
+ Raises:
155
+ RegistryError: If library not found or API error
156
+ """
157
+ try:
158
+ # Build query
159
+ query = f"{owner}/{name}".lower() if owner else name.lower()
160
+
161
+ search_url = f"{self.API_URL}/search"
162
+ response = requests.get(search_url, params={"query": query}, timeout=10)
163
+ response.raise_for_status()
164
+
165
+ result = response.json()
166
+ items = result.get("items", [])
167
+
168
+ # Find exact match
169
+ for item in items:
170
+ item_owner = item.get("owner", {}).get("username", "").lower()
171
+ item_name = item.get("name", "").lower()
172
+
173
+ # Exact match
174
+ if item_name == name.lower():
175
+ if not owner or item_owner == owner.lower():
176
+ return item
177
+
178
+ # No match found
179
+ raise RegistryError(f"Library '{owner}/{name}' not found in registry")
180
+
181
+ except requests.RequestException as e:
182
+ raise RegistryError(f"Registry API error: {e}") from e
183
+
184
+ def resolve_version(self, owner: str, name: str, version_spec: Optional[str] = None) -> LibraryVersion:
185
+ """Resolve a version specification to a specific version.
186
+
187
+ Args:
188
+ owner: Library owner
189
+ name: Library name
190
+ version_spec: Version specification (e.g., "^3.7.8", "3.7.8", "latest")
191
+ If None, uses latest version
192
+
193
+ Returns:
194
+ LibraryVersion with download URL
195
+
196
+ Raises:
197
+ RegistryError: If version cannot be resolved
198
+ """
199
+ info = self.get_library_info(owner, name)
200
+
201
+ # Get version info from search result (latest version)
202
+ version_info = info.get("version")
203
+ if not version_info:
204
+ raise RegistryError(f"No version information available for {owner}/{name}")
205
+
206
+ # For now, use the latest version from search results
207
+ # TODO: Implement proper version constraint matching by querying version history
208
+ version_str = version_info.get("name")
209
+ if not version_str:
210
+ raise RegistryError(f"No version name for {owner}/{name}")
211
+
212
+ files = version_info.get("files", [])
213
+ if not files:
214
+ raise RegistryError(f"No download files available for {owner}/{name}@{version_str}")
215
+
216
+ # Use first file (should be .tar.gz)
217
+ download_url = files[0].get("download_url")
218
+ if not download_url:
219
+ raise RegistryError(f"No download URL for {owner}/{name}@{version_str}")
220
+
221
+ return LibraryVersion(
222
+ version=version_str,
223
+ download_url=download_url,
224
+ homepage=None,
225
+ repository=None,
226
+ )
227
+
228
+ def download_library(self, spec: LibrarySpec, dest_dir: Path, show_progress: bool = True) -> Path:
229
+ """Download a library from the registry.
230
+
231
+ Args:
232
+ spec: Library specification
233
+ dest_dir: Destination directory for extraction
234
+ show_progress: Whether to show download progress
235
+
236
+ Returns:
237
+ Path to extracted library directory
238
+
239
+ Raises:
240
+ RegistryError: If download fails
241
+ """
242
+ # Resolve owner if not specified
243
+ owner = spec.owner
244
+ if not owner:
245
+ owner = self.search_library(spec.name)
246
+ if not owner:
247
+ raise RegistryError(f"Could not find owner for library '{spec.name}'")
248
+
249
+ # Resolve version
250
+ lib_version = self.resolve_version(owner, spec.name, spec.version)
251
+
252
+ if show_progress:
253
+ print(f"Downloading {owner}/{spec.name}@{lib_version.version}")
254
+
255
+ # Download archive
256
+ dest_dir.mkdir(parents=True, exist_ok=True)
257
+ archive_path = dest_dir / "library.tar.gz"
258
+
259
+ try:
260
+ self.downloader.download(lib_version.download_url, archive_path, show_progress=show_progress)
261
+ except KeyboardInterrupt as ke:
262
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
263
+
264
+ handle_keyboard_interrupt_properly(ke)
265
+ except Exception as e:
266
+ raise RegistryError(f"Failed to download library: {e}") from e
267
+
268
+ # Extract archive
269
+ if show_progress:
270
+ print(f"Extracting {spec.name}...")
271
+
272
+ extract_dir = dest_dir / "_extract"
273
+ extract_dir.mkdir(exist_ok=True)
274
+
275
+ try:
276
+ self.downloader.extract_archive(archive_path, extract_dir, show_progress=show_progress)
277
+ except KeyboardInterrupt as ke:
278
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
279
+
280
+ handle_keyboard_interrupt_properly(ke)
281
+ except Exception as e:
282
+ raise RegistryError(f"Failed to extract library: {e}") from e
283
+
284
+ # Find the actual library directory
285
+ # Archives often have a top-level directory
286
+ extracted_items = list(extract_dir.iterdir())
287
+
288
+ if len(extracted_items) == 1 and extracted_items[0].is_dir():
289
+ src_dir = extracted_items[0]
290
+ else:
291
+ src_dir = extract_dir
292
+
293
+ # Move to final location
294
+ final_dir = dest_dir / "src"
295
+ if final_dir.exists():
296
+ import shutil
297
+
298
+ shutil.rmtree(final_dir)
299
+
300
+ src_dir.rename(final_dir)
301
+
302
+ # Clean up
303
+ if extract_dir.exists():
304
+ import shutil
305
+
306
+ shutil.rmtree(extract_dir)
307
+ archive_path.unlink()
308
+
309
+ # Save library info
310
+ info_file = dest_dir / "library.json"
311
+ with open(info_file, "w", encoding="utf-8") as f:
312
+ json.dump(
313
+ {
314
+ "name": spec.name,
315
+ "owner": owner,
316
+ "version": lib_version.version,
317
+ "download_url": lib_version.download_url,
318
+ "repository": lib_version.repository,
319
+ "homepage": lib_version.homepage,
320
+ },
321
+ f,
322
+ indent=2,
323
+ )
324
+
325
+ return final_dir
@@ -0,0 +1,231 @@
1
+ """ESP32 SDK Path Utilities.
2
+
3
+ This module provides utilities for discovering and managing ESP-IDF SDK paths,
4
+ including include directories and precompiled libraries.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import List
9
+
10
+
11
+ class SDKPathResolver:
12
+ """Resolves SDK paths for ESP-IDF frameworks.
13
+
14
+ Provides methods for discovering include directories and libraries
15
+ used by ESP-IDF based projects.
16
+ """
17
+
18
+ # MCU fallback mappings for platforms that don't have full SDK support
19
+ MCU_FALLBACKS = {
20
+ "esp32c2": "esp32c3", # ESP32-C2 can use ESP32-C3 SDK (both rv32imc RISC-V)
21
+ }
22
+
23
+ def __init__(self, sdk_base_dir: Path, show_progress: bool = True):
24
+ """Initialize SDK path resolver.
25
+
26
+ Args:
27
+ sdk_base_dir: Base directory of the SDK (e.g., framework_path/tools/sdk)
28
+ show_progress: Whether to show progress messages
29
+ """
30
+ self.sdk_base_dir = sdk_base_dir
31
+ self.show_progress = show_progress
32
+
33
+ def _resolve_mcu(self, mcu: str) -> str:
34
+ """Resolve MCU to actual SDK directory, applying fallback if needed.
35
+
36
+ Args:
37
+ mcu: MCU type (e.g., "esp32c2", "esp32c6")
38
+
39
+ Returns:
40
+ Resolved MCU type for SDK lookup
41
+ """
42
+ # Check if MCU SDK directory exists
43
+ mcu_dir = self.sdk_base_dir / mcu
44
+ if mcu_dir.exists():
45
+ return mcu
46
+
47
+ # Try fallback if available
48
+ if mcu in self.MCU_FALLBACKS:
49
+ fallback_mcu = self.MCU_FALLBACKS[mcu]
50
+ fallback_dir = self.sdk_base_dir / fallback_mcu
51
+ if fallback_dir.exists():
52
+ if self.show_progress:
53
+ print(f" Note: Using {fallback_mcu} SDK for {mcu} (compatible)")
54
+ return fallback_mcu
55
+
56
+ # No fallback available, return original
57
+ return mcu
58
+
59
+ def get_sdk_includes(self, mcu: str) -> List[Path]:
60
+ """Get list of ESP-IDF include directories for a specific MCU.
61
+
62
+ This method reads the SDK's own includes file which lists the exact
63
+ include paths used by ESP-IDF, avoiding C++ stdlib conflicts that
64
+ occur when recursively discovering paths.
65
+
66
+ Args:
67
+ mcu: MCU type (e.g., "esp32c6", "esp32s3")
68
+
69
+ Returns:
70
+ List of include directory paths (305 paths for esp32c6)
71
+ """
72
+ # Resolve MCU with fallback if needed
73
+ resolved_mcu = self._resolve_mcu(mcu)
74
+
75
+ # Read the SDK's includes file
76
+ includes_file = self.get_sdk_flags_dir(resolved_mcu) / "includes"
77
+ if not includes_file.exists():
78
+ # Fallback to recursive discovery if includes file doesn't exist
79
+ return self._get_sdk_includes_recursive(resolved_mcu)
80
+
81
+ try:
82
+ # Read includes file (single line with space-separated entries)
83
+ includes_content = includes_file.read_text().strip()
84
+
85
+ # Parse the includes: "-iwithprefixbefore path1 -iwithprefixbefore path2 ..."
86
+ # The -iwithprefixbefore flag means to prepend the SDK include directory
87
+ sdk_include_base = self.sdk_base_dir / resolved_mcu / "include"
88
+
89
+ includes = []
90
+ parts = includes_content.split()
91
+ i = 0
92
+ while i < len(parts):
93
+ if parts[i] == "-iwithprefixbefore":
94
+ # Next part is the relative path
95
+ if i + 1 < len(parts):
96
+ rel_path = parts[i + 1]
97
+ abs_path = sdk_include_base / rel_path
98
+ if abs_path.exists():
99
+ includes.append(abs_path)
100
+ i += 2
101
+ else:
102
+ i += 1
103
+ else:
104
+ i += 1
105
+
106
+ return includes
107
+
108
+ except KeyboardInterrupt as ke:
109
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
110
+
111
+ handle_keyboard_interrupt_properly(ke)
112
+ raise # Never reached, but satisfies type checker
113
+ except Exception as e:
114
+ # Fallback to recursive discovery on error
115
+ if self.show_progress:
116
+ print(f"Warning: Failed to parse includes file: {e}")
117
+ print("Falling back to recursive include discovery")
118
+ return self._get_sdk_includes_recursive(mcu)
119
+
120
+ def _get_sdk_includes_recursive(self, mcu: str) -> List[Path]:
121
+ """Fallback method: recursively discover include directories.
122
+
123
+ This discovers 557 paths for esp32c6 but causes C++ stdlib conflicts.
124
+ Kept as a fallback when the includes file is not available.
125
+
126
+ Args:
127
+ mcu: MCU type (e.g., "esp32c6", "esp32s3")
128
+
129
+ Returns:
130
+ List of include directory paths
131
+ """
132
+ sdk_mcu_dir = self.sdk_base_dir / mcu / "include"
133
+ if not sdk_mcu_dir.exists():
134
+ return []
135
+
136
+ # Recursively find all subdirectories with header files
137
+ # ESP-IDF has a deep nested structure for includes
138
+ includes = []
139
+
140
+ def add_includes_recursive(directory: Path, max_depth: int = 6, current_depth: int = 0):
141
+ """Recursively add directories that contain header files."""
142
+ if current_depth > max_depth:
143
+ return
144
+
145
+ # Add this directory if it contains headers or is named 'include'
146
+ has_headers = any(directory.glob("*.h"))
147
+ if directory.name == "include" or has_headers:
148
+ includes.append(directory)
149
+
150
+ # Special handling for parent directories that have subdirs with headers
151
+ # but no headers themselves. Examples:
152
+ # - .../soc/esp32c6/register/ (has soc/ subdir with headers)
153
+ # - .../esp_rom/esp32c6/include/esp32c6/ (has rom/ subdir with headers)
154
+ # Only add if it matches specific patterns to avoid adding too many paths
155
+ is_parent_dir = False
156
+ if not has_headers:
157
+ # Check for specific directory names that are known parent directories
158
+ # Only add 'register' and MCU dirs that are under 'esp_rom' to be conservative
159
+ if directory.name == "register":
160
+ is_parent_dir = True
161
+ elif directory.name.startswith("esp32"):
162
+ # Only add MCU directories if they're under esp_rom component
163
+ if "esp_rom" in str(directory):
164
+ is_parent_dir = True
165
+
166
+ if is_parent_dir:
167
+ try:
168
+ # Check if it has immediate subdirs with headers
169
+ for subdir in directory.iterdir():
170
+ if subdir.is_dir() and any(subdir.glob("*.h")):
171
+ includes.append(directory)
172
+ break
173
+ except (PermissionError, OSError):
174
+ pass
175
+
176
+ # Recurse into subdirectories
177
+ try:
178
+ for subdir in directory.iterdir():
179
+ if subdir.is_dir() and not subdir.name.startswith("."):
180
+ add_includes_recursive(subdir, max_depth, current_depth + 1)
181
+ except (PermissionError, OSError):
182
+ pass
183
+
184
+ add_includes_recursive(sdk_mcu_dir)
185
+ return includes
186
+
187
+ def get_sdk_libs(self, mcu: str, flash_mode: str = "qio") -> List[Path]:
188
+ """Get list of ESP-IDF precompiled libraries for a specific MCU.
189
+
190
+ Args:
191
+ mcu: MCU type (e.g., "esp32c6", "esp32s3")
192
+ flash_mode: Flash mode (e.g., "qio", "dio") - determines flash library variant
193
+
194
+ Returns:
195
+ List of .a library file paths
196
+ """
197
+ # Resolve MCU with fallback if needed
198
+ resolved_mcu = self._resolve_mcu(mcu)
199
+
200
+ libs = []
201
+
202
+ # Get main SDK libraries
203
+ sdk_lib_dir = self.sdk_base_dir / resolved_mcu / "lib"
204
+ if sdk_lib_dir.exists():
205
+ libs.extend(sdk_lib_dir.glob("*.a"))
206
+
207
+ # Get flash mode-specific libraries (qio_qspi or dio_qspi)
208
+ # For ESP32-C6: Only libspi_flash.a
209
+ # For ESP32-S3: Multiple libraries including libfreertos.a, libesp_system.a, etc.
210
+ flash_lib_dir = self.sdk_base_dir / resolved_mcu / f"{flash_mode}_qspi"
211
+ if flash_lib_dir.exists():
212
+ # Collect ALL .a libraries from flash mode directory
213
+ # ESP32-S3 has: libfreertos.a, libspi_flash.a, libesp_system.a,
214
+ # libesp_hw_support.a, libesp_psram.a, libbootloader_support.a
215
+ flash_libs = list(flash_lib_dir.glob("*.a"))
216
+ libs.extend(flash_libs)
217
+
218
+ return libs
219
+
220
+ def get_sdk_flags_dir(self, mcu: str) -> Path:
221
+ """Get path to SDK flags directory for a specific MCU.
222
+
223
+ Args:
224
+ mcu: MCU type (e.g., "esp32c6", "esp32s3")
225
+
226
+ Returns:
227
+ Path to flags directory
228
+ """
229
+ # Resolve MCU with fallback if needed
230
+ resolved_mcu = self._resolve_mcu(mcu)
231
+ return self.sdk_base_dir / resolved_mcu / "flags"