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,489 @@
1
+ """ESP32 Toolchain Management.
2
+
3
+ This module handles downloading, extracting, and managing ESP32 toolchains
4
+ (RISC-V and Xtensa GCC compilers) needed for ESP32 builds.
5
+
6
+ Toolchain Download Process:
7
+ 1. Download metadata package (contains tools.json with platform-specific URLs)
8
+ 2. Parse tools.json to get correct URL for current platform (win64, linux-amd64, etc.)
9
+ 3. Download platform-specific toolchain archive
10
+ 4. Extract to cache directory
11
+
12
+ Toolchain Structure (after extraction):
13
+ toolchain-riscv32-esp/ # RISC-V toolchain for C3, C6, H2
14
+ ├── riscv32-esp-elf/
15
+ │ ├── bin/
16
+ │ │ ├── riscv32-esp-elf-gcc.exe
17
+ │ │ ├── riscv32-esp-elf-g++.exe
18
+ │ │ ├── riscv32-esp-elf-ar.exe
19
+ │ │ ├── riscv32-esp-elf-objcopy.exe
20
+ │ │ └── ...
21
+ │ ├── lib/
22
+ │ └── include/
23
+
24
+ toolchain-xtensa-esp-elf/ # Xtensa toolchain for ESP32, S2, S3
25
+ ├── xtensa-esp32-elf/
26
+ │ ├── bin/
27
+ │ │ ├── xtensa-esp32-elf-gcc.exe
28
+ │ │ ├── xtensa-esp32-elf-g++.exe
29
+ │ │ ├── xtensa-esp32-elf-ar.exe
30
+ │ │ ├── xtensa-esp32-elf-objcopy.exe
31
+ │ │ └── ...
32
+ │ ├── lib/
33
+ │ └── include/
34
+
35
+ Supported Architectures:
36
+ - RISC-V: ESP32-C3, ESP32-C6, ESP32-H2, ESP32-C2, ESP32-C5, ESP32-P4
37
+ - Xtensa: ESP32, ESP32-S2, ESP32-S3
38
+ """
39
+
40
+ from pathlib import Path
41
+ from typing import Any, Dict, Literal, Optional, cast
42
+
43
+ from .cache import Cache
44
+ from .downloader import DownloadError, ExtractionError, PackageDownloader
45
+ from .package import IToolchain, PackageError
46
+ from .platform_utils import PlatformDetector
47
+ from .toolchain_binaries import ToolchainBinaryFinder
48
+ from .toolchain_metadata import MetadataParseError, ToolchainMetadataParser
49
+
50
+
51
+ class ToolchainErrorESP32(PackageError):
52
+ """Raised when ESP32 toolchain operations fail."""
53
+
54
+ pass
55
+
56
+
57
+ ToolchainType = Literal["riscv32-esp", "xtensa-esp-elf"]
58
+
59
+
60
+ class ToolchainESP32(IToolchain):
61
+ """Manages ESP32 toolchain download, extraction, and access.
62
+
63
+ This class handles downloading and managing GCC toolchains for ESP32 family:
64
+ - RISC-V GCC for ESP32-C3, C6, H2, C2, C5, P4 chips
65
+ - Xtensa GCC for ESP32, S2, S3 chips
66
+ """
67
+
68
+ # Toolchain name mappings
69
+ TOOLCHAIN_NAMES = {
70
+ "riscv32-esp": "riscv32-esp-elf",
71
+ "xtensa-esp-elf": "xtensa-esp32-elf", # Note: xtensa uses esp32 in binary names
72
+ }
73
+
74
+ # MCU to toolchain type mapping
75
+ MCU_TOOLCHAIN_MAP = {
76
+ "esp32": "xtensa-esp-elf",
77
+ "esp32s2": "xtensa-esp-elf",
78
+ "esp32s3": "xtensa-esp-elf",
79
+ "esp32c2": "riscv32-esp",
80
+ "esp32c3": "riscv32-esp",
81
+ "esp32c5": "riscv32-esp",
82
+ "esp32c6": "riscv32-esp",
83
+ "esp32h2": "riscv32-esp",
84
+ "esp32p4": "riscv32-esp",
85
+ }
86
+
87
+ def __init__(
88
+ self,
89
+ cache: Cache,
90
+ toolchain_url: str,
91
+ toolchain_type: ToolchainType,
92
+ show_progress: bool = True,
93
+ ):
94
+ """Initialize ESP32 toolchain manager.
95
+
96
+ Args:
97
+ cache: Cache manager instance
98
+ toolchain_url: URL to toolchain package (e.g., GitHub release ZIP)
99
+ toolchain_type: Type of toolchain ("riscv32-esp" or "xtensa-esp-elf")
100
+ show_progress: Whether to show download/extraction progress
101
+ """
102
+ self.cache = cache
103
+ self.toolchain_url = toolchain_url
104
+ self.toolchain_type = toolchain_type
105
+ self.show_progress = show_progress
106
+ self.downloader = PackageDownloader()
107
+
108
+ # Extract version from URL
109
+ self.version = self._extract_version_from_url(toolchain_url)
110
+
111
+ # Get toolchain path from cache
112
+ self.toolchain_path = cache.get_toolchain_path(toolchain_url, self.version)
113
+
114
+ # Get binary prefix for this toolchain type
115
+ self.binary_prefix = self.TOOLCHAIN_NAMES.get(toolchain_type, toolchain_type)
116
+
117
+ # Initialize utilities
118
+ self.metadata_parser = ToolchainMetadataParser(self.downloader)
119
+ self.binary_finder = ToolchainBinaryFinder(self.toolchain_path, self.binary_prefix)
120
+
121
+ @staticmethod
122
+ def _extract_version_from_url(url: str) -> str:
123
+ """Extract version string from toolchain URL.
124
+
125
+ Args:
126
+ url: Toolchain URL (e.g., https://github.com/.../riscv32-esp-elf-14.2.0_20250730.zip)
127
+
128
+ Returns:
129
+ Version string (e.g., "14.2.0_20250730")
130
+ """
131
+ # URL format: .../registry/releases/download/{version}/{filename}
132
+ # or: .../riscv32-esp-elf-{version}.zip
133
+ filename = url.split("/")[-1]
134
+
135
+ # Try to extract version from filename
136
+ # Format: toolchain-name-VERSION.zip
137
+ for prefix in ["riscv32-esp-elf-", "xtensa-esp-elf-"]:
138
+ if prefix in filename:
139
+ version_part = filename.replace(prefix, "").replace(".zip", "")
140
+ return version_part
141
+
142
+ # Fallback: use URL hash if version extraction fails
143
+ from .cache import Cache
144
+
145
+ return Cache.hash_url(url)[:8]
146
+
147
+ @staticmethod
148
+ def get_toolchain_type_for_mcu(mcu: str) -> ToolchainType:
149
+ """Get the toolchain type needed for a specific MCU.
150
+
151
+ Args:
152
+ mcu: MCU type (e.g., "esp32c6", "esp32s3", "esp32")
153
+
154
+ Returns:
155
+ Toolchain type string ("riscv32-esp" or "xtensa-esp-elf")
156
+
157
+ Raises:
158
+ ToolchainErrorESP32: If MCU type is unknown
159
+ """
160
+ mcu_lower = mcu.lower()
161
+ if mcu_lower in ToolchainESP32.MCU_TOOLCHAIN_MAP:
162
+ return cast(ToolchainType, ToolchainESP32.MCU_TOOLCHAIN_MAP[mcu_lower])
163
+
164
+ raise ToolchainErrorESP32(f"Unknown MCU type: {mcu}")
165
+
166
+ @staticmethod
167
+ def detect_platform() -> str:
168
+ """Detect the current platform for toolchain selection.
169
+
170
+ Returns:
171
+ Platform identifier (win32, win64, linux-amd64, linux-arm64, macos, macos-arm64)
172
+ """
173
+ try:
174
+ return PlatformDetector.detect_esp32_platform()
175
+ except KeyboardInterrupt as ke:
176
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
177
+
178
+ handle_keyboard_interrupt_properly(ke)
179
+ raise # Never reached, but satisfies type checker
180
+ except Exception as e:
181
+ raise ToolchainErrorESP32(str(e))
182
+
183
+ def _get_platform_url_from_metadata(self) -> str:
184
+ """Download metadata package and extract platform-specific toolchain URL.
185
+
186
+ Returns:
187
+ URL to platform-specific toolchain archive
188
+
189
+ Raises:
190
+ ToolchainErrorESP32: If metadata cannot be parsed or platform not found
191
+ """
192
+ try:
193
+ current_platform = self.detect_platform()
194
+ toolchain_name = f"toolchain-{self.toolchain_type}"
195
+
196
+ return self.metadata_parser.get_platform_url(
197
+ metadata_url=self.toolchain_url,
198
+ metadata_path=self.toolchain_path,
199
+ toolchain_name=toolchain_name,
200
+ platform=current_platform,
201
+ show_progress=self.show_progress,
202
+ )
203
+ except MetadataParseError as e:
204
+ raise ToolchainErrorESP32(str(e))
205
+
206
+ def ensure_toolchain(self) -> Path:
207
+ """Ensure toolchain is downloaded and extracted.
208
+
209
+ Returns:
210
+ Path to the extracted toolchain directory
211
+
212
+ Raises:
213
+ ToolchainErrorESP32: If download or extraction fails
214
+ """
215
+ if self.is_installed():
216
+ if self.show_progress:
217
+ print(f"Using cached {self.toolchain_type} toolchain {self.version}")
218
+ return self.toolchain_path
219
+
220
+ try:
221
+ # Step 1: Get platform-specific URL from metadata
222
+ platform_url = self._get_platform_url_from_metadata()
223
+
224
+ if self.show_progress:
225
+ print(f"Downloading {self.toolchain_type} toolchain for {self.detect_platform()}...")
226
+
227
+ # Download and extract toolchain package
228
+ self.cache.ensure_directories()
229
+
230
+ # Use downloader to handle download and extraction
231
+ archive_name = Path(platform_url).name
232
+ # Use a different path for the actual toolchain (not metadata)
233
+ toolchain_cache_dir = self.toolchain_path.parent / "bin"
234
+ toolchain_cache_dir.mkdir(parents=True, exist_ok=True)
235
+ archive_path = toolchain_cache_dir / archive_name
236
+
237
+ # Download if not cached
238
+ if not archive_path.exists():
239
+ self.downloader.download(platform_url, archive_path, show_progress=self.show_progress)
240
+ else:
241
+ if self.show_progress:
242
+ print("Using cached toolchain archive")
243
+
244
+ # Extract to toolchain directory
245
+ if self.show_progress:
246
+ print("Extracting toolchain...")
247
+
248
+ # Create temp extraction directory
249
+ temp_extract = toolchain_cache_dir / "temp_extract"
250
+ temp_extract.mkdir(parents=True, exist_ok=True)
251
+
252
+ self.downloader.extract_archive(archive_path, temp_extract, show_progress=self.show_progress)
253
+
254
+ # Find the toolchain directory in the extracted content
255
+ # Usually it's a subdirectory like "riscv32-esp-elf/" or "xtensa-esp32-elf/"
256
+ extracted_dirs = list(temp_extract.glob("*esp*"))
257
+ if not extracted_dirs:
258
+ # Maybe it extracted directly
259
+ extracted_dirs = [temp_extract]
260
+
261
+ source_dir = extracted_dirs[0]
262
+
263
+ # Move to final location (toolchain_path/bin)
264
+ final_bin_path = toolchain_cache_dir
265
+ if final_bin_path.exists() and final_bin_path != temp_extract:
266
+ # Remove old installation
267
+ import shutil
268
+
269
+ for item in final_bin_path.iterdir():
270
+ # Skip temp files and archives that might be locked by antivirus
271
+ if item.name != "temp_extract" and not item.name.endswith((".zip", ".tar", ".xz", ".gz", ".download", ".tmp")):
272
+ try:
273
+ if item.is_dir():
274
+ shutil.rmtree(item)
275
+ else:
276
+ item.unlink()
277
+ except (PermissionError, OSError):
278
+ # Ignore errors removing old files (might be locked by antivirus)
279
+ pass
280
+
281
+ # Copy contents from source_dir to final_bin_path
282
+ import shutil
283
+
284
+ for item in source_dir.iterdir():
285
+ dest = final_bin_path / item.name
286
+ if item.is_dir():
287
+ if dest.exists():
288
+ shutil.rmtree(dest)
289
+ shutil.copytree(item, dest)
290
+ else:
291
+ if dest.exists():
292
+ dest.unlink()
293
+ shutil.copy2(item, dest)
294
+
295
+ # Clean up temp directory
296
+ if temp_extract.exists():
297
+ import shutil
298
+
299
+ shutil.rmtree(temp_extract, ignore_errors=True)
300
+
301
+ if self.show_progress:
302
+ print(f"{self.toolchain_type} toolchain installed successfully")
303
+
304
+ return self.toolchain_path
305
+
306
+ except (DownloadError, ExtractionError) as e:
307
+ raise ToolchainErrorESP32(f"Failed to install {self.toolchain_type} toolchain: {e}")
308
+ except KeyboardInterrupt as ke:
309
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
310
+
311
+ handle_keyboard_interrupt_properly(ke)
312
+ raise # Never reached, but satisfies type checker
313
+ except Exception as e:
314
+ raise ToolchainErrorESP32(f"Unexpected error installing toolchain: {e}")
315
+
316
+ def is_installed(self) -> bool:
317
+ """Check if toolchain is already installed.
318
+
319
+ Returns:
320
+ True if toolchain directory exists with key binaries
321
+ """
322
+ if not self.toolchain_path.exists():
323
+ return False
324
+
325
+ # Verify essential toolchain binaries exist
326
+ return self.binary_finder.verify_binary_exists("gcc")
327
+
328
+ def get_bin_dir(self) -> Optional[Path]:
329
+ """Get path to toolchain bin directory.
330
+
331
+ Returns:
332
+ Path to bin directory containing compiler binaries, or None if not found
333
+ """
334
+ return self.binary_finder.find_bin_dir()
335
+
336
+ def _find_binary(self, binary_name: str) -> Optional[Path]:
337
+ """Find a binary in the toolchain bin directory.
338
+
339
+ Args:
340
+ binary_name: Name of the binary (e.g., "gcc", "g++")
341
+
342
+ Returns:
343
+ Path to binary or None if not found
344
+ """
345
+ return self.binary_finder.find_binary(binary_name)
346
+
347
+ def get_gcc_path(self) -> Optional[Path]:
348
+ """Get path to GCC compiler.
349
+
350
+ Returns:
351
+ Path to gcc binary or None if not found
352
+ """
353
+ return self.binary_finder.get_gcc_path()
354
+
355
+ def get_gxx_path(self) -> Optional[Path]:
356
+ """Get path to G++ compiler.
357
+
358
+ Returns:
359
+ Path to g++ binary or None if not found
360
+ """
361
+ return self.binary_finder.get_gxx_path()
362
+
363
+ def get_ar_path(self) -> Optional[Path]:
364
+ """Get path to archiver (ar).
365
+
366
+ Returns:
367
+ Path to ar binary or None if not found
368
+ """
369
+ return self.binary_finder.get_ar_path()
370
+
371
+ def get_objcopy_path(self) -> Optional[Path]:
372
+ """Get path to objcopy utility.
373
+
374
+ Returns:
375
+ Path to objcopy binary or None if not found
376
+ """
377
+ return self.binary_finder.get_objcopy_path()
378
+
379
+ def get_size_path(self) -> Optional[Path]:
380
+ """Get path to size utility.
381
+
382
+ Returns:
383
+ Path to size binary or None if not found
384
+ """
385
+ return self.binary_finder.get_size_path()
386
+
387
+ def get_objdump_path(self) -> Optional[Path]:
388
+ """Get path to objdump utility.
389
+
390
+ Returns:
391
+ Path to objdump binary or None if not found
392
+ """
393
+ return self.binary_finder.get_objdump_path()
394
+
395
+ def get_all_tool_paths(self) -> Dict[str, Optional[Path]]:
396
+ """Get paths to all common toolchain binaries.
397
+
398
+ Returns:
399
+ Dictionary mapping tool names to their paths
400
+ """
401
+ return self.binary_finder.get_common_tool_paths()
402
+
403
+ def get_all_tools(self) -> Dict[str, Path]:
404
+ """Get paths to all required tools.
405
+
406
+ Returns:
407
+ Dictionary mapping tool names to their paths
408
+
409
+ Raises:
410
+ ToolchainErrorESP32: If any required tool is not found
411
+ """
412
+ tools = self.get_all_tool_paths()
413
+
414
+ # Filter out None values and verify all tools exist
415
+ result = {}
416
+ for name, path in tools.items():
417
+ if path is None:
418
+ raise ToolchainErrorESP32(f"Required tool not found: {name}")
419
+ result[name] = path
420
+
421
+ return result
422
+
423
+ def get_bin_path(self) -> Optional[Path]:
424
+ """Get path to toolchain bin directory.
425
+
426
+ Returns:
427
+ Path to bin directory or None if not found
428
+ """
429
+ return self.get_bin_dir()
430
+
431
+ def verify_installation(self) -> bool:
432
+ """Verify that the toolchain is properly installed.
433
+
434
+ Returns:
435
+ True if all essential binaries are present
436
+
437
+ Raises:
438
+ ToolchainErrorESP32: If essential binaries are missing
439
+ """
440
+ try:
441
+ return self.binary_finder.verify_installation()
442
+ except KeyboardInterrupt as ke:
443
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
444
+
445
+ handle_keyboard_interrupt_properly(ke)
446
+ raise # Never reached, but satisfies type checker
447
+ except Exception as e:
448
+ raise ToolchainErrorESP32(str(e))
449
+
450
+ def get_toolchain_info(self) -> Dict[str, Any]:
451
+ """Get information about the installed toolchain.
452
+
453
+ Returns:
454
+ Dictionary with toolchain information
455
+ """
456
+ info = {
457
+ "type": self.toolchain_type,
458
+ "version": self.version,
459
+ "path": str(self.toolchain_path),
460
+ "url": self.toolchain_url,
461
+ "installed": self.is_installed(),
462
+ "binary_prefix": self.binary_prefix,
463
+ }
464
+
465
+ if self.is_installed():
466
+ info["bin_dir"] = str(self.get_bin_dir())
467
+ info["tools"] = {name: str(path) if path else None for name, path in self.get_all_tool_paths().items()}
468
+
469
+ return info
470
+
471
+ # Implement BasePackage interface
472
+ def ensure_package(self) -> Path:
473
+ """Ensure package is downloaded and extracted.
474
+
475
+ Returns:
476
+ Path to the extracted package directory
477
+
478
+ Raises:
479
+ PackageError: If download or extraction fails
480
+ """
481
+ return self.ensure_toolchain()
482
+
483
+ def get_package_info(self) -> Dict[str, Any]:
484
+ """Get information about the package.
485
+
486
+ Returns:
487
+ Dictionary with package metadata (version, path, etc.)
488
+ """
489
+ return self.get_toolchain_info()
@@ -0,0 +1,185 @@
1
+ """ESP32 Toolchain Metadata Parser.
2
+
3
+ This module handles downloading and parsing ESP32 toolchain metadata packages
4
+ to extract platform-specific toolchain URLs from tools.json.
5
+
6
+ Metadata Structure:
7
+ The metadata package contains a tools.json file with the following structure:
8
+ {
9
+ "tools": [
10
+ {
11
+ "name": "toolchain-riscv32-esp",
12
+ "versions": [
13
+ {
14
+ "win64": {"url": "...", "sha256": "..."},
15
+ "linux-amd64": {"url": "...", "sha256": "..."},
16
+ ...
17
+ }
18
+ ]
19
+ }
20
+ ]
21
+ }
22
+ """
23
+
24
+ import json
25
+ import shutil
26
+ from pathlib import Path
27
+ from typing import Optional
28
+
29
+ from .downloader import PackageDownloader
30
+
31
+
32
+ class MetadataParseError(Exception):
33
+ """Raised when metadata parsing fails."""
34
+
35
+ pass
36
+
37
+
38
+ class ToolchainMetadataParser:
39
+ """Parses ESP32 toolchain metadata to extract platform-specific URLs."""
40
+
41
+ def __init__(self, downloader: Optional[PackageDownloader] = None):
42
+ """Initialize the metadata parser.
43
+
44
+ Args:
45
+ downloader: Optional PackageDownloader instance. If not provided, creates a new one.
46
+ """
47
+ self.downloader = downloader or PackageDownloader()
48
+
49
+ def download_and_extract_metadata(
50
+ self,
51
+ metadata_url: str,
52
+ metadata_path: Path,
53
+ show_progress: bool = True,
54
+ ) -> Path:
55
+ """Download and extract metadata package.
56
+
57
+ Args:
58
+ metadata_url: URL to the metadata package (ZIP file)
59
+ metadata_path: Path where the metadata should be extracted
60
+ show_progress: Whether to show download/extraction progress
61
+
62
+ Returns:
63
+ Path to the extracted metadata directory
64
+
65
+ Raises:
66
+ MetadataParseError: If download or extraction fails
67
+ """
68
+ if metadata_path.exists():
69
+ return metadata_path
70
+
71
+ try:
72
+ if show_progress:
73
+ print("Downloading toolchain metadata...")
74
+
75
+ # Download metadata archive
76
+ archive_name = Path(metadata_url).name
77
+ archive_path = metadata_path.parent / archive_name
78
+
79
+ if not archive_path.exists():
80
+ archive_path.parent.mkdir(parents=True, exist_ok=True)
81
+ self.downloader.download(metadata_url, archive_path, show_progress=show_progress)
82
+
83
+ # Extract metadata to temp directory
84
+ temp_extract = metadata_path.parent / "temp_metadata"
85
+ temp_extract.mkdir(parents=True, exist_ok=True)
86
+
87
+ self.downloader.extract_archive(archive_path, temp_extract, show_progress=False)
88
+
89
+ # Move to final location
90
+ if metadata_path.exists():
91
+ shutil.rmtree(metadata_path)
92
+
93
+ temp_extract.rename(metadata_path)
94
+
95
+ return metadata_path
96
+
97
+ except KeyboardInterrupt as ke:
98
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
99
+
100
+ handle_keyboard_interrupt_properly(ke)
101
+ raise # Never reached, but satisfies type checker
102
+ except Exception as e:
103
+ raise MetadataParseError(f"Failed to download metadata: {e}")
104
+
105
+ def parse_tools_json(
106
+ self,
107
+ tools_json_path: Path,
108
+ toolchain_name: str,
109
+ platform: str,
110
+ ) -> str:
111
+ """Parse tools.json to extract platform-specific toolchain URL.
112
+
113
+ Args:
114
+ tools_json_path: Path to tools.json file
115
+ toolchain_name: Name of the toolchain (e.g., "toolchain-riscv32-esp")
116
+ platform: Platform identifier (e.g., "win64", "linux-amd64")
117
+
118
+ Returns:
119
+ URL to the platform-specific toolchain archive
120
+
121
+ Raises:
122
+ MetadataParseError: If parsing fails or platform/toolchain not found
123
+ """
124
+ if not tools_json_path.exists():
125
+ raise MetadataParseError(f"tools.json not found at {tools_json_path}")
126
+
127
+ try:
128
+ with open(tools_json_path, "r") as f:
129
+ tools_data = json.load(f)
130
+ except json.JSONDecodeError as e:
131
+ raise MetadataParseError(f"Invalid JSON in tools.json: {e}")
132
+
133
+ # Find the toolchain tool
134
+ tools = tools_data.get("tools", [])
135
+ for tool in tools:
136
+ if tool.get("name") == toolchain_name:
137
+ # Get versions
138
+ versions = tool.get("versions", [])
139
+ if not versions:
140
+ raise MetadataParseError(f"No versions found for {toolchain_name}")
141
+
142
+ # Use the first version (usually the recommended one)
143
+ version_info = versions[0]
144
+
145
+ # Get URL for the specified platform
146
+ if platform not in version_info:
147
+ available_platforms = list(version_info.keys())
148
+ raise MetadataParseError(f"Platform {platform} not supported for {toolchain_name}. Available platforms: {available_platforms}")
149
+
150
+ platform_info = version_info[platform]
151
+ return platform_info["url"]
152
+
153
+ raise MetadataParseError(f"Toolchain {toolchain_name} not found in tools.json")
154
+
155
+ def get_platform_url(
156
+ self,
157
+ metadata_url: str,
158
+ metadata_path: Path,
159
+ toolchain_name: str,
160
+ platform: str,
161
+ show_progress: bool = True,
162
+ ) -> str:
163
+ """Download metadata and extract platform-specific toolchain URL.
164
+
165
+ This is a convenience method that combines downloading, extracting, and parsing.
166
+
167
+ Args:
168
+ metadata_url: URL to the metadata package
169
+ metadata_path: Path where metadata should be extracted
170
+ toolchain_name: Name of the toolchain (e.g., "toolchain-riscv32-esp")
171
+ platform: Platform identifier (e.g., "win64", "linux-amd64")
172
+ show_progress: Whether to show progress messages
173
+
174
+ Returns:
175
+ URL to the platform-specific toolchain archive
176
+
177
+ Raises:
178
+ MetadataParseError: If any step fails
179
+ """
180
+ # Download and extract metadata
181
+ extracted_path = self.download_and_extract_metadata(metadata_url, metadata_path, show_progress)
182
+
183
+ # Parse tools.json
184
+ tools_json_path = extracted_path / "tools.json"
185
+ return self.parse_tools_json(tools_json_path, toolchain_name, platform)