fbuild 1.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. fbuild/__init__.py +390 -0
  2. fbuild/assets/example.txt +1 -0
  3. fbuild/build/__init__.py +117 -0
  4. fbuild/build/archive_creator.py +186 -0
  5. fbuild/build/binary_generator.py +444 -0
  6. fbuild/build/build_component_factory.py +131 -0
  7. fbuild/build/build_info_generator.py +624 -0
  8. fbuild/build/build_state.py +325 -0
  9. fbuild/build/build_utils.py +93 -0
  10. fbuild/build/compilation_executor.py +422 -0
  11. fbuild/build/compiler.py +165 -0
  12. fbuild/build/compiler_avr.py +574 -0
  13. fbuild/build/configurable_compiler.py +664 -0
  14. fbuild/build/configurable_linker.py +637 -0
  15. fbuild/build/flag_builder.py +214 -0
  16. fbuild/build/library_dependency_processor.py +185 -0
  17. fbuild/build/linker.py +708 -0
  18. fbuild/build/orchestrator.py +67 -0
  19. fbuild/build/orchestrator_avr.py +651 -0
  20. fbuild/build/orchestrator_esp32.py +878 -0
  21. fbuild/build/orchestrator_rp2040.py +719 -0
  22. fbuild/build/orchestrator_stm32.py +696 -0
  23. fbuild/build/orchestrator_teensy.py +580 -0
  24. fbuild/build/source_compilation_orchestrator.py +218 -0
  25. fbuild/build/source_scanner.py +516 -0
  26. fbuild/cli.py +717 -0
  27. fbuild/cli_utils.py +314 -0
  28. fbuild/config/__init__.py +16 -0
  29. fbuild/config/board_config.py +542 -0
  30. fbuild/config/board_loader.py +92 -0
  31. fbuild/config/ini_parser.py +369 -0
  32. fbuild/config/mcu_specs.py +88 -0
  33. fbuild/daemon/__init__.py +42 -0
  34. fbuild/daemon/async_client.py +531 -0
  35. fbuild/daemon/client.py +1505 -0
  36. fbuild/daemon/compilation_queue.py +293 -0
  37. fbuild/daemon/configuration_lock.py +865 -0
  38. fbuild/daemon/daemon.py +585 -0
  39. fbuild/daemon/daemon_context.py +293 -0
  40. fbuild/daemon/error_collector.py +263 -0
  41. fbuild/daemon/file_cache.py +332 -0
  42. fbuild/daemon/firmware_ledger.py +546 -0
  43. fbuild/daemon/lock_manager.py +508 -0
  44. fbuild/daemon/logging_utils.py +149 -0
  45. fbuild/daemon/messages.py +957 -0
  46. fbuild/daemon/operation_registry.py +288 -0
  47. fbuild/daemon/port_state_manager.py +249 -0
  48. fbuild/daemon/process_tracker.py +366 -0
  49. fbuild/daemon/processors/__init__.py +18 -0
  50. fbuild/daemon/processors/build_processor.py +248 -0
  51. fbuild/daemon/processors/deploy_processor.py +664 -0
  52. fbuild/daemon/processors/install_deps_processor.py +431 -0
  53. fbuild/daemon/processors/locking_processor.py +777 -0
  54. fbuild/daemon/processors/monitor_processor.py +285 -0
  55. fbuild/daemon/request_processor.py +457 -0
  56. fbuild/daemon/shared_serial.py +819 -0
  57. fbuild/daemon/status_manager.py +238 -0
  58. fbuild/daemon/subprocess_manager.py +316 -0
  59. fbuild/deploy/__init__.py +21 -0
  60. fbuild/deploy/deployer.py +67 -0
  61. fbuild/deploy/deployer_esp32.py +310 -0
  62. fbuild/deploy/docker_utils.py +315 -0
  63. fbuild/deploy/monitor.py +519 -0
  64. fbuild/deploy/qemu_runner.py +603 -0
  65. fbuild/interrupt_utils.py +34 -0
  66. fbuild/ledger/__init__.py +52 -0
  67. fbuild/ledger/board_ledger.py +560 -0
  68. fbuild/output.py +352 -0
  69. fbuild/packages/__init__.py +66 -0
  70. fbuild/packages/archive_utils.py +1098 -0
  71. fbuild/packages/arduino_core.py +412 -0
  72. fbuild/packages/cache.py +256 -0
  73. fbuild/packages/concurrent_manager.py +510 -0
  74. fbuild/packages/downloader.py +518 -0
  75. fbuild/packages/fingerprint.py +423 -0
  76. fbuild/packages/framework_esp32.py +538 -0
  77. fbuild/packages/framework_rp2040.py +349 -0
  78. fbuild/packages/framework_stm32.py +459 -0
  79. fbuild/packages/framework_teensy.py +346 -0
  80. fbuild/packages/github_utils.py +96 -0
  81. fbuild/packages/header_trampoline_cache.py +394 -0
  82. fbuild/packages/library_compiler.py +203 -0
  83. fbuild/packages/library_manager.py +549 -0
  84. fbuild/packages/library_manager_esp32.py +725 -0
  85. fbuild/packages/package.py +163 -0
  86. fbuild/packages/platform_esp32.py +383 -0
  87. fbuild/packages/platform_rp2040.py +400 -0
  88. fbuild/packages/platform_stm32.py +581 -0
  89. fbuild/packages/platform_teensy.py +312 -0
  90. fbuild/packages/platform_utils.py +131 -0
  91. fbuild/packages/platformio_registry.py +369 -0
  92. fbuild/packages/sdk_utils.py +231 -0
  93. fbuild/packages/toolchain.py +436 -0
  94. fbuild/packages/toolchain_binaries.py +196 -0
  95. fbuild/packages/toolchain_esp32.py +489 -0
  96. fbuild/packages/toolchain_metadata.py +185 -0
  97. fbuild/packages/toolchain_rp2040.py +436 -0
  98. fbuild/packages/toolchain_stm32.py +417 -0
  99. fbuild/packages/toolchain_teensy.py +404 -0
  100. fbuild/platform_configs/esp32.json +150 -0
  101. fbuild/platform_configs/esp32c2.json +144 -0
  102. fbuild/platform_configs/esp32c3.json +143 -0
  103. fbuild/platform_configs/esp32c5.json +151 -0
  104. fbuild/platform_configs/esp32c6.json +151 -0
  105. fbuild/platform_configs/esp32p4.json +149 -0
  106. fbuild/platform_configs/esp32s3.json +151 -0
  107. fbuild/platform_configs/imxrt1062.json +56 -0
  108. fbuild/platform_configs/rp2040.json +70 -0
  109. fbuild/platform_configs/rp2350.json +76 -0
  110. fbuild/platform_configs/stm32f1.json +59 -0
  111. fbuild/platform_configs/stm32f4.json +63 -0
  112. fbuild/py.typed +0 -0
  113. fbuild-1.2.8.dist-info/METADATA +468 -0
  114. fbuild-1.2.8.dist-info/RECORD +121 -0
  115. fbuild-1.2.8.dist-info/WHEEL +5 -0
  116. fbuild-1.2.8.dist-info/entry_points.txt +5 -0
  117. fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
  118. fbuild-1.2.8.dist-info/top_level.txt +2 -0
  119. fbuild_lint/__init__.py +0 -0
  120. fbuild_lint/ruff_plugins/__init__.py +0 -0
  121. fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
@@ -0,0 +1,369 @@
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
+ is_local: bool = False
32
+ local_path: Optional[Path] = None
33
+
34
+ @classmethod
35
+ def parse(cls, spec: str) -> "LibrarySpec":
36
+ """Parse a library specification string.
37
+
38
+ Supports formats:
39
+ - owner/name@version (e.g., fastled/FastLED@^3.7.8)
40
+ - owner/name (e.g., fastled/FastLED)
41
+ - name@version (e.g., FastLED@^3.7.8)
42
+ - name (e.g., FastLED)
43
+ - URL (e.g., https://github.com/FastLED/FastLED)
44
+ - symlink:// path (e.g., symlink://./)
45
+ - Name=symlink://path (e.g., FastLED=symlink://./)
46
+ - file:// path (e.g., file://../../fastled9)
47
+ - Relative path (e.g., ../../fastled9)
48
+
49
+ Args:
50
+ spec: Library specification string
51
+
52
+ Returns:
53
+ LibrarySpec instance
54
+
55
+ Raises:
56
+ RegistryError: If spec format is invalid
57
+ """
58
+ # Handle Name=symlink://path format (FastLED pattern)
59
+ if "=" in spec and "symlink://" in spec:
60
+ name, path_spec = spec.split("=", 1)
61
+ path_spec = path_spec.replace("symlink://", "", 1).strip()
62
+ local_path = Path(path_spec)
63
+ return cls(owner="", name=name.strip(), version=None, is_local=True, local_path=local_path)
64
+
65
+ # Handle bare symlink://path format
66
+ if spec.startswith("symlink://"):
67
+ path_str = spec.replace("symlink://", "", 1)
68
+ local_path = Path(path_str)
69
+ # Extract library name from path (last component)
70
+ name = local_path.name if local_path.name else "library"
71
+ return cls(owner="", name=name, version=None, is_local=True, local_path=local_path)
72
+
73
+ # Handle file:// URLs - local library paths
74
+ if spec.startswith("file://"):
75
+ path_str = spec.replace("file://", "", 1)
76
+ local_path = Path(path_str)
77
+ # Extract library name from path
78
+ name = local_path.name
79
+ return cls(owner="", name=name, version=None, is_local=True, local_path=local_path)
80
+
81
+ # Handle URLs - convert to owner/name format
82
+ if spec.startswith("http://") or spec.startswith("https://"):
83
+ # Extract owner/name from GitHub URL
84
+ match = re.search(r"github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$", spec)
85
+ if match:
86
+ owner, name = match.groups()
87
+ return cls(owner=owner, name=name, version=None)
88
+ raise RegistryError(f"Cannot parse URL as library spec: {spec}")
89
+
90
+ # Handle relative/absolute paths (../foo, ./foo, /abs/path, C:/path)
91
+ # IMPORTANT: Check this AFTER URLs and BEFORE registry specs
92
+ # to avoid confusing "owner/name" registry specs with paths
93
+ is_path = (
94
+ spec.startswith(".") # Relative paths like ./foo, ../bar
95
+ or spec.startswith("/") # Absolute Unix paths
96
+ or spec.startswith("\\") # Absolute Windows paths (UNC)
97
+ or (len(spec) > 2 and spec[1] == ":") # Windows drive letter (C:/, D:/)
98
+ )
99
+ if is_path:
100
+ local_path = Path(spec)
101
+ name = local_path.name
102
+ return cls(owner="", name=name, version=None, is_local=True, local_path=local_path)
103
+
104
+ # Parse owner/name@version format (registry specs)
105
+ # This must come AFTER path checking to avoid treating "owner/name" as a path
106
+ if "@" in spec:
107
+ lib_part, version = spec.rsplit("@", 1)
108
+ else:
109
+ lib_part = spec
110
+ version = None
111
+
112
+ # Split owner/name
113
+ if "/" in lib_part:
114
+ owner, name = lib_part.split("/", 1)
115
+ else:
116
+ # If no owner specified, we'll need to search registry
117
+ owner = ""
118
+ name = lib_part
119
+
120
+ return cls(owner=owner, name=name, version=version)
121
+
122
+ def __str__(self) -> str:
123
+ """String representation."""
124
+ result = f"{self.owner}/{self.name}" if self.owner else self.name
125
+ if self.version:
126
+ result += f"@{self.version}"
127
+ return result
128
+
129
+
130
+ @dataclass
131
+ class LibraryVersion:
132
+ """Information about a specific library version."""
133
+
134
+ version: str
135
+ download_url: str
136
+ homepage: Optional[str] = None
137
+ repository: Optional[str] = None
138
+
139
+
140
+ class PlatformIORegistry:
141
+ """Client for PlatformIO registry API."""
142
+
143
+ API_URL = "https://api.registry.platformio.org/v3"
144
+
145
+ def __init__(self, downloader: Optional[PackageDownloader] = None):
146
+ """Initialize registry client.
147
+
148
+ Args:
149
+ downloader: Optional package downloader instance
150
+ """
151
+ self.downloader = downloader or PackageDownloader()
152
+
153
+ def search_library(self, name: str) -> Optional[str]:
154
+ """Search for a library by name to find its owner.
155
+
156
+ Args:
157
+ name: Library name
158
+
159
+ Returns:
160
+ Owner name if found, None otherwise
161
+ """
162
+ try:
163
+ # Use search API
164
+ search_url = f"{self.API_URL}/search"
165
+ response = requests.get(search_url, params={"query": name}, timeout=10)
166
+ response.raise_for_status()
167
+
168
+ result = response.json()
169
+ items = result.get("items", [])
170
+
171
+ if items:
172
+ # Return first match owner
173
+ first = items[0]
174
+ owner_info = first.get("owner", {})
175
+ if isinstance(owner_info, dict):
176
+ return owner_info.get("username")
177
+ return owner_info
178
+
179
+ except KeyboardInterrupt as ke:
180
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
181
+
182
+ handle_keyboard_interrupt_properly(ke)
183
+ except Exception as e:
184
+ print(f"Warning: Could not search for library {name}: {e}")
185
+
186
+ return None
187
+
188
+ def get_library_info(self, owner: str, name: str) -> dict:
189
+ """Get library information from registry using search API.
190
+
191
+ Args:
192
+ owner: Library owner (user/org name)
193
+ name: Library name
194
+
195
+ Returns:
196
+ Library information dictionary
197
+
198
+ Raises:
199
+ RegistryError: If library not found or API error
200
+ """
201
+ try:
202
+ # Build query
203
+ query = f"{owner}/{name}".lower() if owner else name.lower()
204
+
205
+ search_url = f"{self.API_URL}/search"
206
+ response = requests.get(search_url, params={"query": query}, timeout=10)
207
+ response.raise_for_status()
208
+
209
+ result = response.json()
210
+ items = result.get("items", [])
211
+
212
+ # Find exact match
213
+ for item in items:
214
+ item_owner = item.get("owner", {}).get("username", "").lower()
215
+ item_name = item.get("name", "").lower()
216
+
217
+ # Exact match
218
+ if item_name == name.lower():
219
+ if not owner or item_owner == owner.lower():
220
+ return item
221
+
222
+ # No match found
223
+ raise RegistryError(f"Library '{owner}/{name}' not found in registry")
224
+
225
+ except requests.RequestException as e:
226
+ raise RegistryError(f"Registry API error: {e}") from e
227
+
228
+ def resolve_version(self, owner: str, name: str, version_spec: Optional[str] = None) -> LibraryVersion:
229
+ """Resolve a version specification to a specific version.
230
+
231
+ Args:
232
+ owner: Library owner
233
+ name: Library name
234
+ version_spec: Version specification (e.g., "^3.7.8", "3.7.8", "latest")
235
+ If None, uses latest version
236
+
237
+ Returns:
238
+ LibraryVersion with download URL
239
+
240
+ Raises:
241
+ RegistryError: If version cannot be resolved
242
+ """
243
+ info = self.get_library_info(owner, name)
244
+
245
+ # Get version info from search result (latest version)
246
+ version_info = info.get("version")
247
+ if not version_info:
248
+ raise RegistryError(f"No version information available for {owner}/{name}")
249
+
250
+ # For now, use the latest version from search results
251
+ # TODO: Implement proper version constraint matching by querying version history
252
+ version_str = version_info.get("name")
253
+ if not version_str:
254
+ raise RegistryError(f"No version name for {owner}/{name}")
255
+
256
+ files = version_info.get("files", [])
257
+ if not files:
258
+ raise RegistryError(f"No download files available for {owner}/{name}@{version_str}")
259
+
260
+ # Use first file (should be .tar.gz)
261
+ download_url = files[0].get("download_url")
262
+ if not download_url:
263
+ raise RegistryError(f"No download URL for {owner}/{name}@{version_str}")
264
+
265
+ return LibraryVersion(
266
+ version=version_str,
267
+ download_url=download_url,
268
+ homepage=None,
269
+ repository=None,
270
+ )
271
+
272
+ def download_library(self, spec: LibrarySpec, dest_dir: Path, show_progress: bool = True) -> Path:
273
+ """Download a library from the registry.
274
+
275
+ Args:
276
+ spec: Library specification
277
+ dest_dir: Destination directory for extraction
278
+ show_progress: Whether to show download progress
279
+
280
+ Returns:
281
+ Path to extracted library directory
282
+
283
+ Raises:
284
+ RegistryError: If download fails
285
+ """
286
+ # Resolve owner if not specified
287
+ owner = spec.owner
288
+ if not owner:
289
+ owner = self.search_library(spec.name)
290
+ if not owner:
291
+ raise RegistryError(f"Could not find owner for library '{spec.name}'")
292
+
293
+ # Resolve version
294
+ lib_version = self.resolve_version(owner, spec.name, spec.version)
295
+
296
+ if show_progress:
297
+ print(f"Downloading {owner}/{spec.name}@{lib_version.version}")
298
+
299
+ # Download archive
300
+ dest_dir.mkdir(parents=True, exist_ok=True)
301
+ archive_path = dest_dir / "library.tar.gz"
302
+
303
+ try:
304
+ self.downloader.download(lib_version.download_url, archive_path, show_progress=show_progress)
305
+ except KeyboardInterrupt as ke:
306
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
307
+
308
+ handle_keyboard_interrupt_properly(ke)
309
+ except Exception as e:
310
+ raise RegistryError(f"Failed to download library: {e}") from e
311
+
312
+ # Extract archive
313
+ if show_progress:
314
+ print(f"Extracting {spec.name}...")
315
+
316
+ extract_dir = dest_dir / "_extract"
317
+ extract_dir.mkdir(exist_ok=True)
318
+
319
+ try:
320
+ self.downloader.extract_archive(archive_path, extract_dir, show_progress=show_progress)
321
+ except KeyboardInterrupt as ke:
322
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
323
+
324
+ handle_keyboard_interrupt_properly(ke)
325
+ except Exception as e:
326
+ raise RegistryError(f"Failed to extract library: {e}") from e
327
+
328
+ # Find the actual library directory
329
+ # Archives often have a top-level directory
330
+ extracted_items = list(extract_dir.iterdir())
331
+
332
+ if len(extracted_items) == 1 and extracted_items[0].is_dir():
333
+ src_dir = extracted_items[0]
334
+ else:
335
+ src_dir = extract_dir
336
+
337
+ # Move to final location
338
+ final_dir = dest_dir / "src"
339
+ if final_dir.exists():
340
+ import shutil
341
+
342
+ shutil.rmtree(final_dir)
343
+
344
+ src_dir.rename(final_dir)
345
+
346
+ # Clean up
347
+ if extract_dir.exists():
348
+ import shutil
349
+
350
+ shutil.rmtree(extract_dir)
351
+ archive_path.unlink()
352
+
353
+ # Save library info
354
+ info_file = dest_dir / "library.json"
355
+ with open(info_file, "w", encoding="utf-8") as f:
356
+ json.dump(
357
+ {
358
+ "name": spec.name,
359
+ "owner": owner,
360
+ "version": lib_version.version,
361
+ "download_url": lib_version.download_url,
362
+ "repository": lib_version.repository,
363
+ "homepage": lib_version.homepage,
364
+ },
365
+ f,
366
+ indent=2,
367
+ )
368
+
369
+ 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"