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.
- fbuild/__init__.py +0 -0
- fbuild/assets/example.txt +1 -0
- fbuild/build/__init__.py +117 -0
- fbuild/build/archive_creator.py +186 -0
- fbuild/build/binary_generator.py +444 -0
- fbuild/build/build_component_factory.py +131 -0
- fbuild/build/build_state.py +325 -0
- fbuild/build/build_utils.py +98 -0
- fbuild/build/compilation_executor.py +422 -0
- fbuild/build/compiler.py +165 -0
- fbuild/build/compiler_avr.py +574 -0
- fbuild/build/configurable_compiler.py +612 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +186 -0
- fbuild/build/library_dependency_processor.py +185 -0
- fbuild/build/linker.py +708 -0
- fbuild/build/orchestrator.py +67 -0
- fbuild/build/orchestrator_avr.py +656 -0
- fbuild/build/orchestrator_esp32.py +797 -0
- fbuild/build/orchestrator_teensy.py +543 -0
- fbuild/build/source_compilation_orchestrator.py +220 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +566 -0
- fbuild/cli_utils.py +312 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +457 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +209 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +34 -0
- fbuild/daemon/client.py +929 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/daemon.py +474 -0
- fbuild/daemon/daemon_context.py +196 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/lock_manager.py +270 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +301 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +12 -0
- fbuild/daemon/processors/build_processor.py +157 -0
- fbuild/daemon/processors/deploy_processor.py +327 -0
- fbuild/daemon/processors/monitor_processor.py +146 -0
- fbuild/daemon/request_processor.py +401 -0
- fbuild/daemon/status_manager.py +216 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +17 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +314 -0
- fbuild/deploy/monitor.py +495 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/packages/__init__.py +53 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +249 -0
- fbuild/packages/downloader.py +366 -0
- fbuild/packages/framework_esp32.py +538 -0
- fbuild/packages/framework_teensy.py +346 -0
- fbuild/packages/github_utils.py +96 -0
- fbuild/packages/header_trampoline_cache.py +394 -0
- fbuild/packages/library_compiler.py +203 -0
- fbuild/packages/library_manager.py +549 -0
- fbuild/packages/library_manager_esp32.py +413 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +325 -0
- fbuild/packages/sdk_utils.py +231 -0
- fbuild/packages/toolchain.py +436 -0
- fbuild/packages/toolchain_binaries.py +196 -0
- fbuild/packages/toolchain_esp32.py +484 -0
- fbuild/packages/toolchain_metadata.py +185 -0
- fbuild/packages/toolchain_teensy.py +404 -0
- fbuild/platform_configs/esp32.json +150 -0
- fbuild/platform_configs/esp32c2.json +144 -0
- fbuild/platform_configs/esp32c3.json +143 -0
- fbuild/platform_configs/esp32c5.json +151 -0
- fbuild/platform_configs/esp32c6.json +151 -0
- fbuild/platform_configs/esp32p4.json +149 -0
- fbuild/platform_configs/esp32s3.json +151 -0
- fbuild/platform_configs/imxrt1062.json +56 -0
- fbuild-1.1.0.dist-info/METADATA +447 -0
- fbuild-1.1.0.dist-info/RECORD +93 -0
- fbuild-1.1.0.dist-info/WHEEL +5 -0
- fbuild-1.1.0.dist-info/entry_points.txt +5 -0
- fbuild-1.1.0.dist-info/licenses/LICENSE +21 -0
- fbuild-1.1.0.dist-info/top_level.txt +2 -0
- fbuild_lint/__init__.py +0 -0
- fbuild_lint/ruff_plugins/__init__.py +0 -0
- fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
|
@@ -0,0 +1,484 @@
|
|
|
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
|
+
if item.name != "temp_extract" and not item.name.endswith((".zip", ".tar", ".xz", ".gz")):
|
|
271
|
+
if item.is_dir():
|
|
272
|
+
shutil.rmtree(item)
|
|
273
|
+
else:
|
|
274
|
+
item.unlink()
|
|
275
|
+
|
|
276
|
+
# Copy contents from source_dir to final_bin_path
|
|
277
|
+
import shutil
|
|
278
|
+
|
|
279
|
+
for item in source_dir.iterdir():
|
|
280
|
+
dest = final_bin_path / item.name
|
|
281
|
+
if item.is_dir():
|
|
282
|
+
if dest.exists():
|
|
283
|
+
shutil.rmtree(dest)
|
|
284
|
+
shutil.copytree(item, dest)
|
|
285
|
+
else:
|
|
286
|
+
if dest.exists():
|
|
287
|
+
dest.unlink()
|
|
288
|
+
shutil.copy2(item, dest)
|
|
289
|
+
|
|
290
|
+
# Clean up temp directory
|
|
291
|
+
if temp_extract.exists():
|
|
292
|
+
import shutil
|
|
293
|
+
|
|
294
|
+
shutil.rmtree(temp_extract, ignore_errors=True)
|
|
295
|
+
|
|
296
|
+
if self.show_progress:
|
|
297
|
+
print(f"{self.toolchain_type} toolchain installed successfully")
|
|
298
|
+
|
|
299
|
+
return self.toolchain_path
|
|
300
|
+
|
|
301
|
+
except (DownloadError, ExtractionError) as e:
|
|
302
|
+
raise ToolchainErrorESP32(f"Failed to install {self.toolchain_type} toolchain: {e}")
|
|
303
|
+
except KeyboardInterrupt as ke:
|
|
304
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
305
|
+
|
|
306
|
+
handle_keyboard_interrupt_properly(ke)
|
|
307
|
+
raise # Never reached, but satisfies type checker
|
|
308
|
+
except Exception as e:
|
|
309
|
+
raise ToolchainErrorESP32(f"Unexpected error installing toolchain: {e}")
|
|
310
|
+
|
|
311
|
+
def is_installed(self) -> bool:
|
|
312
|
+
"""Check if toolchain is already installed.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
True if toolchain directory exists with key binaries
|
|
316
|
+
"""
|
|
317
|
+
if not self.toolchain_path.exists():
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
# Verify essential toolchain binaries exist
|
|
321
|
+
return self.binary_finder.verify_binary_exists("gcc")
|
|
322
|
+
|
|
323
|
+
def get_bin_dir(self) -> Optional[Path]:
|
|
324
|
+
"""Get path to toolchain bin directory.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Path to bin directory containing compiler binaries, or None if not found
|
|
328
|
+
"""
|
|
329
|
+
return self.binary_finder.find_bin_dir()
|
|
330
|
+
|
|
331
|
+
def _find_binary(self, binary_name: str) -> Optional[Path]:
|
|
332
|
+
"""Find a binary in the toolchain bin directory.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
binary_name: Name of the binary (e.g., "gcc", "g++")
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Path to binary or None if not found
|
|
339
|
+
"""
|
|
340
|
+
return self.binary_finder.find_binary(binary_name)
|
|
341
|
+
|
|
342
|
+
def get_gcc_path(self) -> Optional[Path]:
|
|
343
|
+
"""Get path to GCC compiler.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Path to gcc binary or None if not found
|
|
347
|
+
"""
|
|
348
|
+
return self.binary_finder.get_gcc_path()
|
|
349
|
+
|
|
350
|
+
def get_gxx_path(self) -> Optional[Path]:
|
|
351
|
+
"""Get path to G++ compiler.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Path to g++ binary or None if not found
|
|
355
|
+
"""
|
|
356
|
+
return self.binary_finder.get_gxx_path()
|
|
357
|
+
|
|
358
|
+
def get_ar_path(self) -> Optional[Path]:
|
|
359
|
+
"""Get path to archiver (ar).
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Path to ar binary or None if not found
|
|
363
|
+
"""
|
|
364
|
+
return self.binary_finder.get_ar_path()
|
|
365
|
+
|
|
366
|
+
def get_objcopy_path(self) -> Optional[Path]:
|
|
367
|
+
"""Get path to objcopy utility.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Path to objcopy binary or None if not found
|
|
371
|
+
"""
|
|
372
|
+
return self.binary_finder.get_objcopy_path()
|
|
373
|
+
|
|
374
|
+
def get_size_path(self) -> Optional[Path]:
|
|
375
|
+
"""Get path to size utility.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Path to size binary or None if not found
|
|
379
|
+
"""
|
|
380
|
+
return self.binary_finder.get_size_path()
|
|
381
|
+
|
|
382
|
+
def get_objdump_path(self) -> Optional[Path]:
|
|
383
|
+
"""Get path to objdump utility.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Path to objdump binary or None if not found
|
|
387
|
+
"""
|
|
388
|
+
return self.binary_finder.get_objdump_path()
|
|
389
|
+
|
|
390
|
+
def get_all_tool_paths(self) -> Dict[str, Optional[Path]]:
|
|
391
|
+
"""Get paths to all common toolchain binaries.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Dictionary mapping tool names to their paths
|
|
395
|
+
"""
|
|
396
|
+
return self.binary_finder.get_common_tool_paths()
|
|
397
|
+
|
|
398
|
+
def get_all_tools(self) -> Dict[str, Path]:
|
|
399
|
+
"""Get paths to all required tools.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Dictionary mapping tool names to their paths
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
ToolchainErrorESP32: If any required tool is not found
|
|
406
|
+
"""
|
|
407
|
+
tools = self.get_all_tool_paths()
|
|
408
|
+
|
|
409
|
+
# Filter out None values and verify all tools exist
|
|
410
|
+
result = {}
|
|
411
|
+
for name, path in tools.items():
|
|
412
|
+
if path is None:
|
|
413
|
+
raise ToolchainErrorESP32(f"Required tool not found: {name}")
|
|
414
|
+
result[name] = path
|
|
415
|
+
|
|
416
|
+
return result
|
|
417
|
+
|
|
418
|
+
def get_bin_path(self) -> Optional[Path]:
|
|
419
|
+
"""Get path to toolchain bin directory.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Path to bin directory or None if not found
|
|
423
|
+
"""
|
|
424
|
+
return self.get_bin_dir()
|
|
425
|
+
|
|
426
|
+
def verify_installation(self) -> bool:
|
|
427
|
+
"""Verify that the toolchain is properly installed.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
True if all essential binaries are present
|
|
431
|
+
|
|
432
|
+
Raises:
|
|
433
|
+
ToolchainErrorESP32: If essential binaries are missing
|
|
434
|
+
"""
|
|
435
|
+
try:
|
|
436
|
+
return self.binary_finder.verify_installation()
|
|
437
|
+
except KeyboardInterrupt as ke:
|
|
438
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
439
|
+
|
|
440
|
+
handle_keyboard_interrupt_properly(ke)
|
|
441
|
+
raise # Never reached, but satisfies type checker
|
|
442
|
+
except Exception as e:
|
|
443
|
+
raise ToolchainErrorESP32(str(e))
|
|
444
|
+
|
|
445
|
+
def get_toolchain_info(self) -> Dict[str, Any]:
|
|
446
|
+
"""Get information about the installed toolchain.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Dictionary with toolchain information
|
|
450
|
+
"""
|
|
451
|
+
info = {
|
|
452
|
+
"type": self.toolchain_type,
|
|
453
|
+
"version": self.version,
|
|
454
|
+
"path": str(self.toolchain_path),
|
|
455
|
+
"url": self.toolchain_url,
|
|
456
|
+
"installed": self.is_installed(),
|
|
457
|
+
"binary_prefix": self.binary_prefix,
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if self.is_installed():
|
|
461
|
+
info["bin_dir"] = str(self.get_bin_dir())
|
|
462
|
+
info["tools"] = {name: str(path) if path else None for name, path in self.get_all_tool_paths().items()}
|
|
463
|
+
|
|
464
|
+
return info
|
|
465
|
+
|
|
466
|
+
# Implement BasePackage interface
|
|
467
|
+
def ensure_package(self) -> Path:
|
|
468
|
+
"""Ensure package is downloaded and extracted.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Path to the extracted package directory
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
PackageError: If download or extraction fails
|
|
475
|
+
"""
|
|
476
|
+
return self.ensure_toolchain()
|
|
477
|
+
|
|
478
|
+
def get_package_info(self) -> Dict[str, Any]:
|
|
479
|
+
"""Get information about the package.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Dictionary with package metadata (version, path, etc.)
|
|
483
|
+
"""
|
|
484
|
+
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)
|