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,163 @@
1
+ """Abstract base classes for package management.
2
+
3
+ This module defines the interface for platform-specific package managers
4
+ (toolchains, frameworks, cores, etc.) to ensure consistent behavior across
5
+ different platforms.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Optional
11
+
12
+
13
+ class PackageError(Exception):
14
+ """Base exception for package management errors."""
15
+
16
+ pass
17
+
18
+
19
+ class IPackage(ABC):
20
+ """Interface for downloadable packages.
21
+
22
+ This interface defines the common contract for all package types:
23
+ - Toolchains (AVR, RISC-V, Xtensa)
24
+ - Frameworks (Arduino-ESP32)
25
+ - Cores (Arduino AVR Core)
26
+ - Platforms (ESP32 Platform)
27
+ """
28
+
29
+ @abstractmethod
30
+ def ensure_package(self) -> Path:
31
+ """Ensure package is downloaded and extracted.
32
+
33
+ Returns:
34
+ Path to the extracted package directory
35
+
36
+ Raises:
37
+ PackageError: If download or extraction fails
38
+ """
39
+ pass
40
+
41
+ @abstractmethod
42
+ def is_installed(self) -> bool:
43
+ """Check if package is already installed.
44
+
45
+ Returns:
46
+ True if package directory exists and is valid
47
+ """
48
+ pass
49
+
50
+ @abstractmethod
51
+ def get_package_info(self) -> Dict[str, Any]:
52
+ """Get information about the package.
53
+
54
+ Returns:
55
+ Dictionary with package metadata (version, path, etc.)
56
+ """
57
+ pass
58
+
59
+
60
+ class IToolchain(IPackage):
61
+ """Interface for toolchain packages.
62
+
63
+ Toolchains provide compiler, linker, and binary utilities for
64
+ specific architectures (AVR, RISC-V, Xtensa).
65
+ """
66
+
67
+ @abstractmethod
68
+ def get_gcc_path(self) -> Optional[Path]:
69
+ """Get path to GCC compiler.
70
+
71
+ Returns:
72
+ Path to gcc binary or None if not found
73
+ """
74
+ pass
75
+
76
+ @abstractmethod
77
+ def get_gxx_path(self) -> Optional[Path]:
78
+ """Get path to G++ compiler.
79
+
80
+ Returns:
81
+ Path to g++ binary or None if not found
82
+ """
83
+ pass
84
+
85
+ @abstractmethod
86
+ def get_ar_path(self) -> Optional[Path]:
87
+ """Get path to archiver (ar).
88
+
89
+ Returns:
90
+ Path to ar binary or None if not found
91
+ """
92
+ pass
93
+
94
+ @abstractmethod
95
+ def get_objcopy_path(self) -> Optional[Path]:
96
+ """Get path to objcopy utility.
97
+
98
+ Returns:
99
+ Path to objcopy binary or None if not found
100
+ """
101
+ pass
102
+
103
+ @abstractmethod
104
+ def get_size_path(self) -> Optional[Path]:
105
+ """Get path to size utility.
106
+
107
+ Returns:
108
+ Path to size binary or None if not found
109
+ """
110
+ pass
111
+
112
+ @abstractmethod
113
+ def get_bin_dir(self) -> Optional[Path]:
114
+ """Get path to toolchain bin directory.
115
+
116
+ Returns:
117
+ Path to bin directory containing compiler binaries
118
+ """
119
+ pass
120
+
121
+ @abstractmethod
122
+ def get_all_tools(self) -> Dict[str, Path]:
123
+ """Get paths to all required tools.
124
+
125
+ Returns:
126
+ Dictionary mapping tool names to their paths
127
+ """
128
+ pass
129
+
130
+
131
+ class IFramework(IPackage):
132
+ """Interface for framework packages.
133
+
134
+ Frameworks provide core Arduino implementation, variants,
135
+ and built-in libraries for specific platforms.
136
+ """
137
+
138
+ @abstractmethod
139
+ def get_cores_dir(self) -> Path:
140
+ """Get path to cores directory.
141
+
142
+ Returns:
143
+ Path to cores directory containing Arduino core implementation
144
+ """
145
+ pass
146
+
147
+ @abstractmethod
148
+ def get_variants_dir(self) -> Path:
149
+ """Get path to variants directory.
150
+
151
+ Returns:
152
+ Path to variants directory containing board-specific configurations
153
+ """
154
+ pass
155
+
156
+ @abstractmethod
157
+ def get_libraries_dir(self) -> Path:
158
+ """Get path to built-in libraries directory.
159
+
160
+ Returns:
161
+ Path to libraries directory
162
+ """
163
+ pass
@@ -0,0 +1,383 @@
1
+ """ESP32 Platform Package Management.
2
+
3
+ This module handles downloading, extracting, and managing ESP32 platform packages
4
+ from GitHub releases. It provides access to the Arduino-ESP32 core, toolchains,
5
+ and platform-specific tools needed for ESP32 builds.
6
+
7
+ Platform Structure (after extraction):
8
+ platform-espressif32/
9
+ ├── platform.json # Package metadata with download URLs
10
+ ├── boards/ # Board definitions (JSON files)
11
+ │ └── esp32-c6-devkitm-1.json
12
+ ├── builder/ # PlatformIO build scripts
13
+ │ └── frameworks/
14
+ │ └── arduino.py
15
+ └── ... # Other platform files
16
+
17
+ Key Packages (from platform.json):
18
+ - framework-arduinoespressif32: Arduino core (cores/, variants/)
19
+ - framework-arduinoespressif32-libs: Pre-built ESP-IDF libraries
20
+ - toolchain-riscv32-esp: RISC-V GCC (for C3, C6, H2)
21
+ - toolchain-xtensa-esp-elf: Xtensa GCC (for ESP32, S2, S3)
22
+ - tool-esptoolpy: Upload tool
23
+ """
24
+
25
+ import json
26
+ from pathlib import Path
27
+ from typing import Any, Dict, Optional
28
+
29
+ from .cache import Cache
30
+ from .downloader import DownloadError, ExtractionError, PackageDownloader
31
+ from .package import IPackage, PackageError
32
+
33
+
34
+ class PlatformErrorESP32(PackageError):
35
+ """Raised when ESP32 platform operations fail."""
36
+
37
+ pass
38
+
39
+
40
+ class PlatformESP32(IPackage):
41
+ """Manages ESP32 platform package download, extraction, and access.
42
+
43
+ This class handles the pioarduino/platform-espressif32 package which contains:
44
+ - Arduino core for ESP32 family (C3, C6, S2, S3, H2, etc.)
45
+ - Toolchains (riscv32-esp-elf-gcc, xtensa-esp-elf-gcc)
46
+ - Platform tools (esptool, mkspiffs, etc.)
47
+ - Board definitions and variants
48
+ """
49
+
50
+ def __init__(self, cache: Cache, platform_url: str, show_progress: bool = True):
51
+ """Initialize ESP32 platform manager.
52
+
53
+ Args:
54
+ cache: Cache manager instance
55
+ platform_url: URL to platform package (e.g., GitHub release ZIP)
56
+ show_progress: Whether to show download/extraction progress
57
+ """
58
+ self.cache = cache
59
+ self.platform_url = platform_url
60
+ self.show_progress = show_progress
61
+ self.downloader = PackageDownloader()
62
+
63
+ # Extract version from URL (e.g., "55.03.34" from release tag)
64
+ self.version = self._extract_version_from_url(platform_url)
65
+
66
+ # Get platform path from cache
67
+ self.platform_path = cache.get_platform_path(platform_url, self.version)
68
+
69
+ @staticmethod
70
+ def _extract_version_from_url(url: str) -> str:
71
+ """Extract version string from platform URL.
72
+
73
+ Args:
74
+ url: Platform URL (e.g., https://github.com/.../55.03.34/platform.zip)
75
+
76
+ Returns:
77
+ Version string (e.g., "55.03.34")
78
+ """
79
+ # URL format: .../releases/download/{version}/platform-espressif32.zip
80
+ parts = url.split("/")
81
+ for i, part in enumerate(parts):
82
+ if part == "download" and i + 1 < len(parts):
83
+ return parts[i + 1]
84
+
85
+ # Fallback: use URL hash if version extraction fails
86
+ from .cache import Cache
87
+
88
+ return Cache.hash_url(url)[:8]
89
+
90
+ def ensure_package(self) -> Path:
91
+ """Ensure platform is downloaded and extracted.
92
+
93
+ Returns:
94
+ Path to the extracted platform directory
95
+
96
+ Raises:
97
+ PlatformErrorESP32: If download or extraction fails
98
+ """
99
+ return self.ensure_platform()
100
+
101
+ def ensure_platform(self) -> Path:
102
+ """Ensure platform is downloaded and extracted.
103
+
104
+ Returns:
105
+ Path to the extracted platform directory
106
+
107
+ Raises:
108
+ PlatformErrorESP32: If download or extraction fails
109
+ """
110
+ if self.is_installed():
111
+ if self.show_progress:
112
+ print(f"Using cached ESP32 platform {self.version}")
113
+ return self.platform_path
114
+
115
+ try:
116
+ if self.show_progress:
117
+ print(f"Downloading ESP32 platform {self.version}...")
118
+
119
+ # Download and extract platform package
120
+ self.cache.ensure_directories()
121
+
122
+ # Use downloader to handle download and extraction
123
+ archive_name = Path(self.platform_url).name
124
+ archive_path = self.platform_path.parent / archive_name
125
+
126
+ # Download if not cached
127
+ if not archive_path.exists():
128
+ archive_path.parent.mkdir(parents=True, exist_ok=True)
129
+ self.downloader.download(self.platform_url, archive_path, show_progress=self.show_progress)
130
+ else:
131
+ if self.show_progress:
132
+ print(f"Using cached archive {archive_name}")
133
+
134
+ # Extract to platform directory
135
+ if self.show_progress:
136
+ print(f"Extracting platform to {self.platform_path}...")
137
+
138
+ # Create temp extraction directory
139
+ temp_extract = self.platform_path.parent / "temp_extract"
140
+ temp_extract.mkdir(parents=True, exist_ok=True)
141
+
142
+ self.downloader.extract_archive(archive_path, temp_extract, show_progress=self.show_progress)
143
+
144
+ # Find the platform directory in the extracted content
145
+ # Usually it's a subdirectory like "platform-espressif32/"
146
+ extracted_dirs = list(temp_extract.glob("platform-*"))
147
+ if not extracted_dirs:
148
+ # Maybe it extracted directly
149
+ extracted_dirs = [temp_extract]
150
+
151
+ source_dir = extracted_dirs[0]
152
+
153
+ # Move to final location
154
+ if self.platform_path.exists():
155
+ import shutil
156
+
157
+ shutil.rmtree(self.platform_path)
158
+
159
+ source_dir.rename(self.platform_path)
160
+
161
+ # Clean up temp directory
162
+ if temp_extract.exists() and temp_extract != self.platform_path:
163
+ import shutil
164
+
165
+ shutil.rmtree(temp_extract, ignore_errors=True)
166
+
167
+ if self.show_progress:
168
+ print(f"ESP32 platform installed to {self.platform_path}")
169
+
170
+ return self.platform_path
171
+
172
+ except (DownloadError, ExtractionError) as e:
173
+ raise PlatformErrorESP32(f"Failed to install ESP32 platform: {e}")
174
+ except KeyboardInterrupt as ke:
175
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
176
+
177
+ handle_keyboard_interrupt_properly(ke)
178
+ raise # Never reached, but satisfies type checker
179
+ except Exception as e:
180
+ raise PlatformErrorESP32(f"Unexpected error installing platform: {e}")
181
+
182
+ def is_installed(self) -> bool:
183
+ """Check if platform is already installed.
184
+
185
+ Returns:
186
+ True if platform directory exists with key files
187
+ """
188
+ if not self.platform_path.exists():
189
+ return False
190
+
191
+ # Verify essential platform files exist
192
+ required_files = [
193
+ self.platform_path / "platform.json",
194
+ self.platform_path / "boards",
195
+ ]
196
+
197
+ return all(f.exists() for f in required_files)
198
+
199
+ def get_platform_json(self) -> Dict[str, Any]:
200
+ """Load and parse platform.json metadata.
201
+
202
+ Returns:
203
+ Dictionary containing platform metadata
204
+
205
+ Raises:
206
+ PlatformErrorESP32: If platform.json doesn't exist or is invalid
207
+ """
208
+ platform_json_path = self.platform_path / "platform.json"
209
+
210
+ if not platform_json_path.exists():
211
+ raise PlatformErrorESP32(f"platform.json not found at {platform_json_path}. " + "Ensure platform is downloaded first.")
212
+
213
+ try:
214
+ with open(platform_json_path, "r") as f:
215
+ return json.load(f)
216
+ except json.JSONDecodeError as e:
217
+ raise PlatformErrorESP32(f"Failed to parse platform.json: {e}")
218
+ except KeyboardInterrupt as ke:
219
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
220
+
221
+ handle_keyboard_interrupt_properly(ke)
222
+ raise # Never reached, but satisfies type checker
223
+ except Exception as e:
224
+ raise PlatformErrorESP32(f"Failed to read platform.json: {e}")
225
+
226
+ def get_package_url(self, package_name: str) -> Optional[str]:
227
+ """Get download URL for a specific package.
228
+
229
+ Args:
230
+ package_name: Name of the package (e.g., "toolchain-riscv32-esp")
231
+
232
+ Returns:
233
+ URL string or None if package not found
234
+ """
235
+ platform_json = self.get_platform_json()
236
+ packages = platform_json.get("packages", {})
237
+
238
+ if package_name not in packages:
239
+ return None
240
+
241
+ package_info = packages[package_name]
242
+ return package_info.get("version") # "version" field contains URL
243
+
244
+ def get_required_packages(self, board_mcu: str) -> Dict[str, str]:
245
+ """Get required packages for a specific MCU.
246
+
247
+ Args:
248
+ board_mcu: MCU type (e.g., "esp32c6", "esp32s3", "esp32")
249
+
250
+ Returns:
251
+ Dictionary of package_name -> url for required packages
252
+ """
253
+ packages = {}
254
+
255
+ # All ESP32 boards need the Arduino framework
256
+ framework_url = self.get_package_url("framework-arduinoespressif32")
257
+ if framework_url:
258
+ packages["framework-arduinoespressif32"] = framework_url
259
+
260
+ libs_url = self.get_package_url("framework-arduinoespressif32-libs")
261
+ if libs_url:
262
+ packages["framework-arduinoespressif32-libs"] = libs_url
263
+
264
+ # Check for MCU-specific skeleton libraries
265
+ # These are used when the main libs package is empty or incomplete
266
+ # The naming pattern is: framework-arduino-{mcu_suffix}-skeleton-lib
267
+ # where mcu_suffix is extracted from the MCU name (e.g., "esp32c2" -> "c2")
268
+ mcu_suffix = board_mcu.replace("esp32", "")
269
+ skeleton_lib_name = f"framework-arduino-{mcu_suffix}-skeleton-lib"
270
+ skeleton_lib_url = self.get_package_url(skeleton_lib_name)
271
+ if skeleton_lib_url:
272
+ packages[skeleton_lib_name] = skeleton_lib_url
273
+
274
+ # Determine which toolchain is needed based on MCU architecture
275
+ if board_mcu in [
276
+ "esp32c3",
277
+ "esp32c6",
278
+ "esp32h2",
279
+ "esp32c2",
280
+ "esp32c5",
281
+ "esp32p4",
282
+ ]:
283
+ # RISC-V based ESP32s
284
+ toolchain_url = self.get_package_url("toolchain-riscv32-esp")
285
+ if toolchain_url:
286
+ packages["toolchain-riscv32-esp"] = toolchain_url
287
+ else:
288
+ # Xtensa based ESP32s (original ESP32, S2, S3)
289
+ toolchain_url = self.get_package_url("toolchain-xtensa-esp-elf")
290
+ if toolchain_url:
291
+ packages["toolchain-xtensa-esp-elf"] = toolchain_url
292
+
293
+ # Add esptool (needed for all ESP32 boards)
294
+ esptool_url = self.get_package_url("tool-esptoolpy")
295
+ if esptool_url:
296
+ packages["tool-esptoolpy"] = esptool_url
297
+
298
+ return packages
299
+
300
+ def get_boards_dir(self) -> Path:
301
+ """Get path to boards directory.
302
+
303
+ Returns:
304
+ Path to boards directory containing JSON board definitions
305
+ """
306
+ return self.platform_path / "boards"
307
+
308
+ def get_board_json(self, board_id: str) -> Dict[str, Any]:
309
+ """Load board configuration from JSON.
310
+
311
+ Args:
312
+ board_id: Board identifier (e.g., "esp32-c6-devkitm-1")
313
+
314
+ Returns:
315
+ Dictionary containing board configuration
316
+
317
+ Raises:
318
+ PlatformErrorESP32: If board JSON doesn't exist or is invalid
319
+ """
320
+ board_json_path = self.get_boards_dir() / f"{board_id}.json"
321
+
322
+ if not board_json_path.exists():
323
+ raise PlatformErrorESP32(f"Board definition not found: {board_id} " + f"at {board_json_path}")
324
+
325
+ try:
326
+ with open(board_json_path, "r") as f:
327
+ return json.load(f)
328
+ except json.JSONDecodeError as e:
329
+ raise PlatformErrorESP32(f"Failed to parse board JSON: {e}")
330
+ except KeyboardInterrupt as ke:
331
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
332
+
333
+ handle_keyboard_interrupt_properly(ke)
334
+ raise # Never reached, but satisfies type checker
335
+ except Exception as e:
336
+ raise PlatformErrorESP32(f"Failed to read board JSON: {e}")
337
+
338
+ def list_boards(self) -> list[str]:
339
+ """List all available board IDs.
340
+
341
+ Returns:
342
+ List of board identifiers
343
+ """
344
+ boards_dir = self.get_boards_dir()
345
+ if not boards_dir.exists():
346
+ return []
347
+
348
+ return [f.stem for f in boards_dir.glob("*.json") if f.is_file() and not f.name.endswith(".py")]
349
+
350
+ def get_package_info(self) -> Dict[str, Any]:
351
+ """Get information about the installed platform.
352
+
353
+ Returns:
354
+ Dictionary with platform information
355
+ """
356
+ return self.get_platform_info()
357
+
358
+ def get_platform_info(self) -> Dict[str, Any]:
359
+ """Get information about the installed platform.
360
+
361
+ Returns:
362
+ Dictionary with platform information
363
+ """
364
+ info = {
365
+ "version": self.version,
366
+ "path": str(self.platform_path),
367
+ "url": self.platform_url,
368
+ "installed": self.is_installed(),
369
+ }
370
+
371
+ if self.is_installed():
372
+ info["boards_dir"] = str(self.get_boards_dir())
373
+ info["available_boards"] = len(self.list_boards())
374
+
375
+ # Get package information
376
+ try:
377
+ platform_json = self.get_platform_json()
378
+ info["platform_version"] = platform_json.get("version")
379
+ info["available_packages"] = list(platform_json.get("packages", {}).keys())
380
+ except PlatformErrorESP32:
381
+ pass
382
+
383
+ return info