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