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.
- fbuild/__init__.py +390 -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_info_generator.py +624 -0
- fbuild/build/build_state.py +325 -0
- fbuild/build/build_utils.py +93 -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 +664 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +214 -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 +651 -0
- fbuild/build/orchestrator_esp32.py +878 -0
- fbuild/build/orchestrator_rp2040.py +719 -0
- fbuild/build/orchestrator_stm32.py +696 -0
- fbuild/build/orchestrator_teensy.py +580 -0
- fbuild/build/source_compilation_orchestrator.py +218 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +717 -0
- fbuild/cli_utils.py +314 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +542 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +369 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +42 -0
- fbuild/daemon/async_client.py +531 -0
- fbuild/daemon/client.py +1505 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/configuration_lock.py +865 -0
- fbuild/daemon/daemon.py +585 -0
- fbuild/daemon/daemon_context.py +293 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/firmware_ledger.py +546 -0
- fbuild/daemon/lock_manager.py +508 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +957 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/port_state_manager.py +249 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +18 -0
- fbuild/daemon/processors/build_processor.py +248 -0
- fbuild/daemon/processors/deploy_processor.py +664 -0
- fbuild/daemon/processors/install_deps_processor.py +431 -0
- fbuild/daemon/processors/locking_processor.py +777 -0
- fbuild/daemon/processors/monitor_processor.py +285 -0
- fbuild/daemon/request_processor.py +457 -0
- fbuild/daemon/shared_serial.py +819 -0
- fbuild/daemon/status_manager.py +238 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +21 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +310 -0
- fbuild/deploy/docker_utils.py +315 -0
- fbuild/deploy/monitor.py +519 -0
- fbuild/deploy/qemu_runner.py +603 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/ledger/__init__.py +52 -0
- fbuild/ledger/board_ledger.py +560 -0
- fbuild/output.py +352 -0
- fbuild/packages/__init__.py +66 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +256 -0
- fbuild/packages/concurrent_manager.py +510 -0
- fbuild/packages/downloader.py +518 -0
- fbuild/packages/fingerprint.py +423 -0
- fbuild/packages/framework_esp32.py +538 -0
- fbuild/packages/framework_rp2040.py +349 -0
- fbuild/packages/framework_stm32.py +459 -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 +725 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_rp2040.py +400 -0
- fbuild/packages/platform_stm32.py +581 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +369 -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 +489 -0
- fbuild/packages/toolchain_metadata.py +185 -0
- fbuild/packages/toolchain_rp2040.py +436 -0
- fbuild/packages/toolchain_stm32.py +417 -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/platform_configs/rp2040.json +70 -0
- fbuild/platform_configs/rp2350.json +76 -0
- fbuild/platform_configs/stm32f1.json +59 -0
- fbuild/platform_configs/stm32f4.json +63 -0
- fbuild/py.typed +0 -0
- fbuild-1.2.8.dist-info/METADATA +468 -0
- fbuild-1.2.8.dist-info/RECORD +121 -0
- fbuild-1.2.8.dist-info/WHEEL +5 -0
- fbuild-1.2.8.dist-info/entry_points.txt +5 -0
- fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
- fbuild-1.2.8.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,725 @@
|
|
|
1
|
+
"""ESP32 library dependency management.
|
|
2
|
+
|
|
3
|
+
This module handles downloading and compiling external libraries for ESP32 builds.
|
|
4
|
+
It uses the PlatformIO registry to resolve and download libraries, then compiles
|
|
5
|
+
them with the ESP32 toolchain.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import platform
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
from fbuild.packages.platformio_registry import (
|
|
16
|
+
LibrarySpec,
|
|
17
|
+
PlatformIORegistry,
|
|
18
|
+
RegistryError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LibraryErrorESP32(Exception):
|
|
25
|
+
"""Exception for ESP32 library management errors."""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LibraryESP32:
|
|
31
|
+
"""Represents a downloaded and compiled ESP32 library."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, lib_dir: Path, name: str):
|
|
34
|
+
"""Initialize ESP32 library.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
lib_dir: Root directory for the library
|
|
38
|
+
name: Library name
|
|
39
|
+
"""
|
|
40
|
+
self.lib_dir = lib_dir
|
|
41
|
+
self.name = name
|
|
42
|
+
self.src_dir = lib_dir / "src"
|
|
43
|
+
self.info_file = lib_dir / "library.json"
|
|
44
|
+
self.archive_file = lib_dir / f"lib{name}.a"
|
|
45
|
+
self.build_info_file = lib_dir / "build_info.json"
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def exists(self) -> bool:
|
|
49
|
+
"""Check if library is downloaded and compiled."""
|
|
50
|
+
return self.lib_dir.exists() and self.src_dir.exists() and self.info_file.exists()
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def is_compiled(self) -> bool:
|
|
54
|
+
"""Check if library is compiled."""
|
|
55
|
+
return self.archive_file.exists() and self.build_info_file.exists()
|
|
56
|
+
|
|
57
|
+
def get_source_files(self) -> List[Path]:
|
|
58
|
+
"""Find all source files (.c, .cpp, .cc, .cxx) in the library.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
List of source file paths
|
|
62
|
+
"""
|
|
63
|
+
if not self.src_dir.exists():
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
sources = []
|
|
67
|
+
|
|
68
|
+
# Check for src/src/ structure (some libraries have this)
|
|
69
|
+
src_src = self.src_dir / "src"
|
|
70
|
+
search_dir = src_src if (src_src.exists() and src_src.is_dir()) else self.src_dir
|
|
71
|
+
|
|
72
|
+
# Find all source files recursively
|
|
73
|
+
for pattern in ["**/*.c", "**/*.cpp", "**/*.cc", "**/*.cxx"]:
|
|
74
|
+
for path in search_dir.glob(pattern):
|
|
75
|
+
# Skip examples and tests (check relative path only)
|
|
76
|
+
rel_path = str(path.relative_to(search_dir)).lower()
|
|
77
|
+
if "example" not in rel_path and "test" not in rel_path:
|
|
78
|
+
sources.append(path)
|
|
79
|
+
|
|
80
|
+
return sources
|
|
81
|
+
|
|
82
|
+
def get_include_dirs(self) -> List[Path]:
|
|
83
|
+
"""Get include directories for this library.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
List of include directory paths
|
|
87
|
+
"""
|
|
88
|
+
include_dirs = []
|
|
89
|
+
|
|
90
|
+
if not self.src_dir.exists():
|
|
91
|
+
logger.warning(f"[INCLUDE_DEBUG] src_dir does not exist: {self.src_dir}")
|
|
92
|
+
return include_dirs
|
|
93
|
+
|
|
94
|
+
# Check for src/src/ structure
|
|
95
|
+
src_src = self.src_dir / "src"
|
|
96
|
+
if src_src.exists() and src_src.is_dir():
|
|
97
|
+
logger.debug(f"[INCLUDE_DEBUG] Found nested src/, adding: {src_src}")
|
|
98
|
+
include_dirs.append(src_src)
|
|
99
|
+
else:
|
|
100
|
+
logger.debug(f"[INCLUDE_DEBUG] No nested src/, adding base: {self.src_dir}")
|
|
101
|
+
include_dirs.append(self.src_dir)
|
|
102
|
+
|
|
103
|
+
# Look for additional include directories
|
|
104
|
+
for name in ["include", "Include", "INCLUDE"]:
|
|
105
|
+
inc_dir = self.lib_dir / name
|
|
106
|
+
if inc_dir.exists():
|
|
107
|
+
logger.debug(f"[INCLUDE_DEBUG] Found additional include dir: {inc_dir}")
|
|
108
|
+
include_dirs.append(inc_dir)
|
|
109
|
+
|
|
110
|
+
logger.debug(f"[INCLUDE_DEBUG] Final include_dirs for {self.name}: {include_dirs}")
|
|
111
|
+
return include_dirs
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class LibraryManagerESP32:
|
|
115
|
+
"""Manages ESP32 library dependencies."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, build_dir: Path, registry: Optional[PlatformIORegistry] = None, project_dir: Optional[Path] = None):
|
|
118
|
+
"""Initialize library manager.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
build_dir: Build directory (.fbuild/build/{board})
|
|
122
|
+
registry: Optional registry client
|
|
123
|
+
project_dir: Optional project directory (for resolving relative local library paths)
|
|
124
|
+
"""
|
|
125
|
+
self.build_dir = Path(build_dir)
|
|
126
|
+
self.libs_dir = self.build_dir / "libs"
|
|
127
|
+
self.registry = registry or PlatformIORegistry()
|
|
128
|
+
self.project_dir = Path(project_dir) if project_dir else None
|
|
129
|
+
|
|
130
|
+
# Ensure libs directory exists
|
|
131
|
+
self.libs_dir.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
|
|
133
|
+
def _sanitize_name(self, name: str) -> str:
|
|
134
|
+
"""Sanitize library name for filesystem.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
name: Library name
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Sanitized name
|
|
141
|
+
"""
|
|
142
|
+
return name.lower().replace("/", "_").replace("@", "_")
|
|
143
|
+
|
|
144
|
+
def _find_toolchain_compilers(self, toolchain_path: Path) -> tuple[Path, Path]:
|
|
145
|
+
"""Find GCC and G++ compilers in the toolchain directory.
|
|
146
|
+
|
|
147
|
+
ESP32 uses different toolchains depending on the MCU architecture:
|
|
148
|
+
- Xtensa (ESP32, ESP32-S2, ESP32-S3): xtensa-esp32-elf-gcc, xtensa-esp32-elf-g++
|
|
149
|
+
- RISC-V (ESP32-C3, C6, H2): riscv32-esp-elf-gcc, riscv32-esp-elf-g++
|
|
150
|
+
|
|
151
|
+
This method auto-detects which toolchain is available.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
toolchain_path: Path to toolchain bin directory
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Tuple of (gcc_path, gxx_path)
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
LibraryErrorESP32: If no suitable compiler is found
|
|
161
|
+
"""
|
|
162
|
+
# Check for Xtensa toolchain first (more common for ESP32)
|
|
163
|
+
exe_suffix = ".exe" if platform.system() == "Windows" else ""
|
|
164
|
+
|
|
165
|
+
# Xtensa toolchain (ESP32, S2, S3)
|
|
166
|
+
xtensa_gcc = toolchain_path / f"xtensa-esp32-elf-gcc{exe_suffix}"
|
|
167
|
+
xtensa_gxx = toolchain_path / f"xtensa-esp32-elf-g++{exe_suffix}"
|
|
168
|
+
|
|
169
|
+
if xtensa_gcc.exists() and xtensa_gxx.exists():
|
|
170
|
+
logger.debug(f"[TOOLCHAIN] Using Xtensa toolchain: {xtensa_gcc}")
|
|
171
|
+
return xtensa_gcc, xtensa_gxx
|
|
172
|
+
|
|
173
|
+
# RISC-V toolchain (ESP32-C3, C6, H2)
|
|
174
|
+
riscv_gcc = toolchain_path / f"riscv32-esp-elf-gcc{exe_suffix}"
|
|
175
|
+
riscv_gxx = toolchain_path / f"riscv32-esp-elf-g++{exe_suffix}"
|
|
176
|
+
|
|
177
|
+
if riscv_gcc.exists() and riscv_gxx.exists():
|
|
178
|
+
logger.debug(f"[TOOLCHAIN] Using RISC-V toolchain: {riscv_gcc}")
|
|
179
|
+
return riscv_gcc, riscv_gxx
|
|
180
|
+
|
|
181
|
+
# Fallback: try to find any gcc/g++ pattern
|
|
182
|
+
gcc_files = list(toolchain_path.glob(f"*-gcc{exe_suffix}"))
|
|
183
|
+
gxx_files = list(toolchain_path.glob(f"*-g++{exe_suffix}"))
|
|
184
|
+
|
|
185
|
+
if gcc_files and gxx_files:
|
|
186
|
+
logger.debug(f"[TOOLCHAIN] Using discovered toolchain: {gcc_files[0]}")
|
|
187
|
+
return gcc_files[0], gxx_files[0]
|
|
188
|
+
|
|
189
|
+
raise LibraryErrorESP32(f"No suitable ESP32 toolchain found in {toolchain_path}. Expected xtensa-esp32-elf-gcc or riscv32-esp-elf-gcc")
|
|
190
|
+
|
|
191
|
+
def _find_toolchain_ar(self, toolchain_path: Path) -> Path:
|
|
192
|
+
"""Find ar archiver in the toolchain directory.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
toolchain_path: Path to toolchain bin directory
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Path to ar binary
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
LibraryErrorESP32: If no suitable ar is found
|
|
202
|
+
"""
|
|
203
|
+
exe_suffix = ".exe" if platform.system() == "Windows" else ""
|
|
204
|
+
|
|
205
|
+
# Check for Xtensa ar
|
|
206
|
+
xtensa_ar = toolchain_path / f"xtensa-esp32-elf-ar{exe_suffix}"
|
|
207
|
+
if xtensa_ar.exists():
|
|
208
|
+
return xtensa_ar
|
|
209
|
+
|
|
210
|
+
# Check for RISC-V ar
|
|
211
|
+
riscv_ar = toolchain_path / f"riscv32-esp-elf-ar{exe_suffix}"
|
|
212
|
+
if riscv_ar.exists():
|
|
213
|
+
return riscv_ar
|
|
214
|
+
|
|
215
|
+
# Fallback: try to find any ar pattern
|
|
216
|
+
ar_files = list(toolchain_path.glob(f"*-ar{exe_suffix}"))
|
|
217
|
+
if ar_files:
|
|
218
|
+
return ar_files[0]
|
|
219
|
+
|
|
220
|
+
raise LibraryErrorESP32(f"No suitable ar archiver found in {toolchain_path}. Expected xtensa-esp32-elf-ar or riscv32-esp-elf-ar")
|
|
221
|
+
|
|
222
|
+
def get_library(self, spec: LibrarySpec) -> LibraryESP32:
|
|
223
|
+
"""Get a library instance for a specification.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
spec: Library specification
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
LibraryESP32 instance
|
|
230
|
+
"""
|
|
231
|
+
lib_name = self._sanitize_name(spec.name)
|
|
232
|
+
lib_dir = self.libs_dir / lib_name
|
|
233
|
+
return LibraryESP32(lib_dir, lib_name)
|
|
234
|
+
|
|
235
|
+
def _handle_local_library(self, spec: LibrarySpec, show_progress: bool = True) -> LibraryESP32:
|
|
236
|
+
"""Handle a local library specification (file:// or relative path).
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
spec: Library specification with is_local=True
|
|
240
|
+
show_progress: Whether to show progress
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
LibraryESP32 instance
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
LibraryErrorESP32: If local library setup fails
|
|
247
|
+
"""
|
|
248
|
+
import logging
|
|
249
|
+
|
|
250
|
+
logger = logging.getLogger(__name__)
|
|
251
|
+
|
|
252
|
+
logger.debug(f"[LOCAL_LIB] Step 1: Starting _handle_local_library for spec: {spec}")
|
|
253
|
+
|
|
254
|
+
if not spec.local_path:
|
|
255
|
+
raise LibraryErrorESP32(f"Local library spec has no path: {spec}")
|
|
256
|
+
|
|
257
|
+
logger.debug(f"[LOCAL_LIB] Step 2: spec.local_path = {spec.local_path}")
|
|
258
|
+
|
|
259
|
+
library = self.get_library(spec)
|
|
260
|
+
logger.debug(f"[LOCAL_LIB] Step 3: Created library instance, lib_dir = {library.lib_dir}")
|
|
261
|
+
|
|
262
|
+
# Skip if already set up
|
|
263
|
+
if library.exists:
|
|
264
|
+
logger.debug("[LOCAL_LIB] Step 4: Library already exists, returning early")
|
|
265
|
+
if show_progress:
|
|
266
|
+
print(f"Local library '{spec.name}' already set up")
|
|
267
|
+
return library
|
|
268
|
+
|
|
269
|
+
logger.debug("[LOCAL_LIB] Step 5: Library doesn't exist, need to set up")
|
|
270
|
+
|
|
271
|
+
# Resolve the local path (relative to project directory if available, otherwise cwd)
|
|
272
|
+
local_path = spec.local_path
|
|
273
|
+
logger.debug(f"[LOCAL_LIB] Step 6: local_path before absolute check: {local_path}, is_absolute={local_path.is_absolute()}")
|
|
274
|
+
|
|
275
|
+
if not local_path.is_absolute():
|
|
276
|
+
# Make absolute relative to project directory (where platformio.ini is)
|
|
277
|
+
# If project_dir not set, fall back to current working directory
|
|
278
|
+
base_dir = self.project_dir if self.project_dir else Path.cwd()
|
|
279
|
+
logger.debug(f"[LOCAL_LIB] Step 7: Converting to absolute, base_dir = {base_dir}")
|
|
280
|
+
local_path = base_dir / local_path
|
|
281
|
+
logger.debug(f"[LOCAL_LIB] Step 8: After joining: local_path = {local_path}")
|
|
282
|
+
|
|
283
|
+
# Normalize path (resolve .. and .)
|
|
284
|
+
logger.debug(f"[LOCAL_LIB] Step 9: Before resolve(), local_path = {local_path}")
|
|
285
|
+
local_path = local_path.resolve()
|
|
286
|
+
logger.debug(f"[LOCAL_LIB] Step 10: After resolve(), local_path = {local_path}")
|
|
287
|
+
|
|
288
|
+
# Verify library exists
|
|
289
|
+
logger.debug(f"[LOCAL_LIB] Step 11: Checking if local_path exists: {local_path}")
|
|
290
|
+
if not local_path.exists():
|
|
291
|
+
raise LibraryErrorESP32(f"Local library path does not exist: {local_path}")
|
|
292
|
+
|
|
293
|
+
logger.debug("[LOCAL_LIB] Step 12: Path exists, checking if directory")
|
|
294
|
+
if not local_path.is_dir():
|
|
295
|
+
raise LibraryErrorESP32(f"Local library path is not a directory: {local_path}")
|
|
296
|
+
|
|
297
|
+
logger.debug("[LOCAL_LIB] Step 13: Checking for library.json or library.properties")
|
|
298
|
+
# Look for library.json (Arduino library metadata)
|
|
299
|
+
library_json = local_path / "library.json"
|
|
300
|
+
library_properties = local_path / "library.properties"
|
|
301
|
+
|
|
302
|
+
if not library_json.exists() and not library_properties.exists():
|
|
303
|
+
# Check if there's a src subdirectory (common Arduino structure)
|
|
304
|
+
logger.debug("[LOCAL_LIB] Step 14: No metadata files, checking for src/ directory")
|
|
305
|
+
src_dir = local_path / "src"
|
|
306
|
+
if not src_dir.exists():
|
|
307
|
+
raise LibraryErrorESP32(f"Local library has no library.json, library.properties, or src/ directory: {local_path}")
|
|
308
|
+
|
|
309
|
+
logger.debug("[LOCAL_LIB] Step 15: Library structure validated")
|
|
310
|
+
if show_progress:
|
|
311
|
+
print(f"Setting up local library '{spec.name}' from {local_path}")
|
|
312
|
+
|
|
313
|
+
# Create library directory structure
|
|
314
|
+
logger.debug(f"[LOCAL_LIB] Step 16: Creating library directory: {library.lib_dir}")
|
|
315
|
+
library.lib_dir.mkdir(parents=True, exist_ok=True)
|
|
316
|
+
|
|
317
|
+
# Create a symlink or copy to the source directory
|
|
318
|
+
# On Windows, force copy instead of symlink due to MSYS/cross-compiler incompatibility
|
|
319
|
+
# MSYS creates /c/Users/... symlinks that ESP32 cross-compiler can't follow when
|
|
320
|
+
# include paths use Windows format (C:/Users/...)
|
|
321
|
+
import os
|
|
322
|
+
import shutil
|
|
323
|
+
|
|
324
|
+
is_windows = platform.system() == "Windows"
|
|
325
|
+
logger.debug(f"[LOCAL_LIB] Step 17: Platform detected: {platform.system()} (is_windows={is_windows})")
|
|
326
|
+
|
|
327
|
+
# Check if local_path has a src/ subdirectory (Arduino library structure)
|
|
328
|
+
# If so, copy from local_path/src instead of local_path to avoid path duplication
|
|
329
|
+
source_path = local_path / "src" if (local_path / "src").is_dir() else local_path
|
|
330
|
+
logger.debug(f"[LOCAL_LIB] Step 17.5: Source path for copy/symlink: {source_path}")
|
|
331
|
+
|
|
332
|
+
if is_windows:
|
|
333
|
+
# Windows: Always copy to avoid MSYS symlink issues with ESP32 cross-compiler
|
|
334
|
+
logger.debug("[LOCAL_LIB] Step 18: Windows detected, forcing copy (no symlink)")
|
|
335
|
+
if show_progress:
|
|
336
|
+
print(f" Copying library files from {source_path}...")
|
|
337
|
+
|
|
338
|
+
if library.src_dir.exists():
|
|
339
|
+
logger.debug("[LOCAL_LIB] Step 19: Removing existing src_dir before copy")
|
|
340
|
+
shutil.rmtree(library.src_dir)
|
|
341
|
+
|
|
342
|
+
# Define ignore function to exclude build artifacts and version control
|
|
343
|
+
# This prevents recursive .fbuild directories and other unnecessary files
|
|
344
|
+
def ignore_build_artifacts(directory: str, contents: list[str]) -> list[str]:
|
|
345
|
+
ignored = []
|
|
346
|
+
for name in contents:
|
|
347
|
+
if name in {".fbuild", ".pio", ".git", ".venv", "__pycache__", ".pytest_cache", "node_modules", ".cache", "build", ".build", ".vscode", ".idea"}:
|
|
348
|
+
ignored.append(name)
|
|
349
|
+
logger.debug(f"[LOCAL_LIB] Ignoring: {os.path.join(directory, name)}")
|
|
350
|
+
return ignored
|
|
351
|
+
|
|
352
|
+
logger.debug("[LOCAL_LIB] Step 20: Calling shutil.copytree() with symlinks=False and ignore filter")
|
|
353
|
+
# symlinks=False: Dereference any symlinks in source tree (important for nested dependencies)
|
|
354
|
+
# ignore: Skip build artifacts, version control, and cache directories
|
|
355
|
+
shutil.copytree(source_path, library.src_dir, symlinks=False, ignore=ignore_build_artifacts)
|
|
356
|
+
logger.debug("[LOCAL_LIB] Step 21: Copy completed successfully")
|
|
357
|
+
if show_progress:
|
|
358
|
+
print(f" Copied library files to {library.src_dir}")
|
|
359
|
+
else:
|
|
360
|
+
# Unix: Use symlink for efficiency (actual files stay in original location)
|
|
361
|
+
logger.debug(f"[LOCAL_LIB] Step 18: Unix platform, attempting symlink from {library.src_dir} to {source_path}")
|
|
362
|
+
try:
|
|
363
|
+
# Try to create symlink first (faster, no disk space duplication)
|
|
364
|
+
if library.src_dir.exists():
|
|
365
|
+
logger.debug("[LOCAL_LIB] Step 19: Removing existing src_dir")
|
|
366
|
+
library.src_dir.unlink()
|
|
367
|
+
logger.debug("[LOCAL_LIB] Step 20: Calling os.symlink()")
|
|
368
|
+
os.symlink(str(source_path), str(library.src_dir), target_is_directory=True)
|
|
369
|
+
logger.debug("[LOCAL_LIB] Step 21: Symlink created successfully")
|
|
370
|
+
if show_progress:
|
|
371
|
+
print(f" Created symlink to {source_path}")
|
|
372
|
+
except OSError as e:
|
|
373
|
+
# Symlink failed (maybe no permissions), fall back to copying
|
|
374
|
+
logger.debug(f"[LOCAL_LIB] Step 22: Symlink failed with error: {e}, falling back to copy")
|
|
375
|
+
if show_progress:
|
|
376
|
+
print(" Symlink failed, copying library files...")
|
|
377
|
+
|
|
378
|
+
if library.src_dir.exists():
|
|
379
|
+
logger.debug("[LOCAL_LIB] Step 23: Removing existing src_dir before copy")
|
|
380
|
+
shutil.rmtree(library.src_dir)
|
|
381
|
+
|
|
382
|
+
# Define ignore function to exclude build artifacts and version control
|
|
383
|
+
def ignore_build_artifacts(directory: str, contents: list[str]) -> list[str]:
|
|
384
|
+
ignored = []
|
|
385
|
+
for name in contents:
|
|
386
|
+
if name in {".fbuild", ".pio", ".git", ".venv", "__pycache__", ".pytest_cache", "node_modules", ".cache", "build", ".build", ".vscode", ".idea"}:
|
|
387
|
+
ignored.append(name)
|
|
388
|
+
logger.debug(f"[LOCAL_LIB] Ignoring: {os.path.join(directory, name)}")
|
|
389
|
+
return ignored
|
|
390
|
+
|
|
391
|
+
logger.debug("[LOCAL_LIB] Step 24: Calling shutil.copytree() with symlinks=False and ignore filter")
|
|
392
|
+
shutil.copytree(source_path, library.src_dir, symlinks=False, ignore=ignore_build_artifacts)
|
|
393
|
+
logger.debug("[LOCAL_LIB] Step 25: Copy completed successfully")
|
|
394
|
+
if show_progress:
|
|
395
|
+
print(f" Copied library files to {library.src_dir}")
|
|
396
|
+
|
|
397
|
+
# Create library.json metadata
|
|
398
|
+
import json
|
|
399
|
+
|
|
400
|
+
logger.debug("[LOCAL_LIB] Step 25: Creating library.json metadata")
|
|
401
|
+
|
|
402
|
+
lib_name = spec.name
|
|
403
|
+
lib_version = "local"
|
|
404
|
+
|
|
405
|
+
# Try to read version from existing metadata
|
|
406
|
+
logger.debug(f"[LOCAL_LIB] Step 26: Checking if source library.json exists: {library_json}")
|
|
407
|
+
if library_json.exists():
|
|
408
|
+
logger.debug("[LOCAL_LIB] Step 27: Reading metadata from source library.json")
|
|
409
|
+
try:
|
|
410
|
+
with open(library_json, "r", encoding="utf-8") as f:
|
|
411
|
+
metadata = json.load(f)
|
|
412
|
+
lib_name = metadata.get("name", spec.name)
|
|
413
|
+
lib_version = metadata.get("version", "local")
|
|
414
|
+
logger.debug(f"[LOCAL_LIB] Step 28: Read metadata: name={lib_name}, version={lib_version}")
|
|
415
|
+
except KeyboardInterrupt as ke:
|
|
416
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
417
|
+
|
|
418
|
+
handle_keyboard_interrupt_properly(ke)
|
|
419
|
+
raise # Never reached
|
|
420
|
+
except Exception as e:
|
|
421
|
+
logger.debug(f"[LOCAL_LIB] Step 29: Failed to read metadata: {e}, using defaults")
|
|
422
|
+
pass # Use defaults
|
|
423
|
+
|
|
424
|
+
# Save library info in the expected location
|
|
425
|
+
info_file = library.lib_dir / "library.json"
|
|
426
|
+
logger.debug(f"[LOCAL_LIB] Step 30: Writing library info to: {info_file}")
|
|
427
|
+
with open(info_file, "w", encoding="utf-8") as f:
|
|
428
|
+
json.dump(
|
|
429
|
+
{
|
|
430
|
+
"name": lib_name,
|
|
431
|
+
"owner": "local",
|
|
432
|
+
"version": lib_version,
|
|
433
|
+
"local_path": str(local_path),
|
|
434
|
+
"is_local": True,
|
|
435
|
+
},
|
|
436
|
+
f,
|
|
437
|
+
indent=2,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
logger.debug("[LOCAL_LIB] Step 31: Local library setup completed successfully")
|
|
441
|
+
if show_progress:
|
|
442
|
+
print(f"Local library '{spec.name}' set up successfully")
|
|
443
|
+
|
|
444
|
+
return library
|
|
445
|
+
|
|
446
|
+
def download_library(self, spec: LibrarySpec, show_progress: bool = True) -> LibraryESP32:
|
|
447
|
+
"""Download a library from PlatformIO registry or set up local library.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
spec: Library specification
|
|
451
|
+
show_progress: Whether to show progress
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
LibraryESP32 instance
|
|
455
|
+
|
|
456
|
+
Raises:
|
|
457
|
+
LibraryErrorESP32: If download or setup fails
|
|
458
|
+
"""
|
|
459
|
+
try:
|
|
460
|
+
# Check if this is a local library first
|
|
461
|
+
if spec.is_local:
|
|
462
|
+
return self._handle_local_library(spec, show_progress)
|
|
463
|
+
|
|
464
|
+
# Remote library - use existing registry logic
|
|
465
|
+
library = self.get_library(spec)
|
|
466
|
+
|
|
467
|
+
# Skip if already downloaded
|
|
468
|
+
if library.exists:
|
|
469
|
+
if show_progress:
|
|
470
|
+
print(f"Library '{spec.name}' already downloaded")
|
|
471
|
+
return library
|
|
472
|
+
|
|
473
|
+
# Download from registry
|
|
474
|
+
self.registry.download_library(spec, library.lib_dir, show_progress=show_progress)
|
|
475
|
+
|
|
476
|
+
return library
|
|
477
|
+
|
|
478
|
+
except RegistryError as e:
|
|
479
|
+
raise LibraryErrorESP32(f"Failed to download library {spec}: {e}") from e
|
|
480
|
+
|
|
481
|
+
def needs_rebuild(self, library: LibraryESP32, compiler_flags: List[str]) -> tuple[bool, str]:
|
|
482
|
+
"""Check if a library needs to be rebuilt.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
library: Library to check
|
|
486
|
+
compiler_flags: Current compiler flags
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Tuple of (needs_rebuild, reason)
|
|
490
|
+
"""
|
|
491
|
+
if not library.archive_file.exists():
|
|
492
|
+
return True, "Archive not found"
|
|
493
|
+
|
|
494
|
+
if not library.build_info_file.exists():
|
|
495
|
+
return True, "Build info missing"
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
with open(library.build_info_file, "r", encoding="utf-8") as f:
|
|
499
|
+
build_info = json.load(f)
|
|
500
|
+
|
|
501
|
+
# Check if compiler flags changed
|
|
502
|
+
stored_flags = build_info.get("compiler_flags", [])
|
|
503
|
+
if stored_flags != compiler_flags:
|
|
504
|
+
return True, "Compiler flags changed"
|
|
505
|
+
|
|
506
|
+
except KeyboardInterrupt as ke:
|
|
507
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
508
|
+
|
|
509
|
+
handle_keyboard_interrupt_properly(ke)
|
|
510
|
+
raise # Never reached, but satisfies type checker
|
|
511
|
+
except Exception:
|
|
512
|
+
return True, "Could not load build info"
|
|
513
|
+
|
|
514
|
+
return False, ""
|
|
515
|
+
|
|
516
|
+
def compile_library(
|
|
517
|
+
self,
|
|
518
|
+
library: LibraryESP32,
|
|
519
|
+
toolchain_path: Path,
|
|
520
|
+
compiler_flags: List[str],
|
|
521
|
+
include_paths: List[Path],
|
|
522
|
+
show_progress: bool = True,
|
|
523
|
+
) -> Path:
|
|
524
|
+
"""Compile a library into a static archive.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
library: Library to compile
|
|
528
|
+
toolchain_path: Path to toolchain bin directory
|
|
529
|
+
compiler_flags: Compiler flags
|
|
530
|
+
include_paths: Include directories
|
|
531
|
+
show_progress: Whether to show progress
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
Path to compiled archive file
|
|
535
|
+
|
|
536
|
+
Raises:
|
|
537
|
+
LibraryErrorESP32: If compilation fails
|
|
538
|
+
"""
|
|
539
|
+
try:
|
|
540
|
+
if show_progress:
|
|
541
|
+
print(f"Compiling library: {library.name}")
|
|
542
|
+
|
|
543
|
+
# Get source files
|
|
544
|
+
sources = library.get_source_files()
|
|
545
|
+
if not sources:
|
|
546
|
+
raise LibraryErrorESP32(f"No source files found in library '{library.name}'")
|
|
547
|
+
|
|
548
|
+
if show_progress:
|
|
549
|
+
print(f" Found {len(sources)} source file(s)")
|
|
550
|
+
|
|
551
|
+
# Get library's own include directories
|
|
552
|
+
lib_includes = library.get_include_dirs()
|
|
553
|
+
all_includes = list(include_paths) + lib_includes
|
|
554
|
+
|
|
555
|
+
# Compile each source file
|
|
556
|
+
object_files = []
|
|
557
|
+
|
|
558
|
+
# Auto-detect toolchain prefix from available binaries
|
|
559
|
+
# ESP32/S2/S3 use xtensa, C3/C6/H2 use RISC-V
|
|
560
|
+
gcc_path, gxx_path = self._find_toolchain_compilers(toolchain_path)
|
|
561
|
+
|
|
562
|
+
# Create response file for include paths (avoid Windows command line length limit)
|
|
563
|
+
include_flags = [f"-I{str(inc).replace(chr(92), '/')}" for inc in all_includes]
|
|
564
|
+
response_file = library.lib_dir / "includes.rsp"
|
|
565
|
+
with open(response_file, "w") as f:
|
|
566
|
+
f.write("\n".join(include_flags))
|
|
567
|
+
|
|
568
|
+
for source in sources:
|
|
569
|
+
# Determine compiler based on extension
|
|
570
|
+
if source.suffix in [".cpp", ".cc", ".cxx"]:
|
|
571
|
+
compiler = gxx_path
|
|
572
|
+
else:
|
|
573
|
+
compiler = gcc_path
|
|
574
|
+
|
|
575
|
+
# Output object file - maintain directory structure relative to src_dir
|
|
576
|
+
# This prevents name collisions and keeps .d files organized
|
|
577
|
+
rel_path = source.relative_to(library.src_dir)
|
|
578
|
+
obj_file = library.src_dir / rel_path.with_suffix(".o")
|
|
579
|
+
|
|
580
|
+
# Ensure output directory exists
|
|
581
|
+
obj_file.parent.mkdir(parents=True, exist_ok=True)
|
|
582
|
+
|
|
583
|
+
# Build compile command
|
|
584
|
+
cmd = [str(compiler), "-c"]
|
|
585
|
+
cmd.extend(compiler_flags)
|
|
586
|
+
|
|
587
|
+
# Use response file for include paths
|
|
588
|
+
cmd.append(f"@{response_file}")
|
|
589
|
+
# Add source and output
|
|
590
|
+
cmd.extend(["-o", str(obj_file), str(source)])
|
|
591
|
+
|
|
592
|
+
# Compile
|
|
593
|
+
if show_progress:
|
|
594
|
+
print(f" Compiling {source.name}...")
|
|
595
|
+
|
|
596
|
+
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
|
|
597
|
+
|
|
598
|
+
if result.returncode != 0:
|
|
599
|
+
raise LibraryErrorESP32(f"Failed to compile {source.name}:\n{result.stderr}")
|
|
600
|
+
|
|
601
|
+
object_files.append(obj_file)
|
|
602
|
+
|
|
603
|
+
# Create static archive using ar
|
|
604
|
+
ar_path = self._find_toolchain_ar(toolchain_path)
|
|
605
|
+
|
|
606
|
+
if show_progress:
|
|
607
|
+
print(f" Creating archive: {library.archive_file.name}")
|
|
608
|
+
|
|
609
|
+
# Remove old archive if exists
|
|
610
|
+
if library.archive_file.exists():
|
|
611
|
+
library.archive_file.unlink()
|
|
612
|
+
|
|
613
|
+
# Create new archive
|
|
614
|
+
cmd = [str(ar_path), "rcs", str(library.archive_file)]
|
|
615
|
+
cmd.extend([str(obj) for obj in object_files])
|
|
616
|
+
|
|
617
|
+
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
|
|
618
|
+
|
|
619
|
+
if result.returncode != 0:
|
|
620
|
+
raise LibraryErrorESP32(f"Failed to create archive for {library.name}:\n{result.stderr}")
|
|
621
|
+
|
|
622
|
+
# Save build info
|
|
623
|
+
build_info = {
|
|
624
|
+
"compiler_flags": compiler_flags,
|
|
625
|
+
"source_count": len(sources),
|
|
626
|
+
"object_files": [str(obj) for obj in object_files],
|
|
627
|
+
}
|
|
628
|
+
with open(library.build_info_file, "w", encoding="utf-8") as f:
|
|
629
|
+
json.dump(build_info, f, indent=2)
|
|
630
|
+
|
|
631
|
+
if show_progress:
|
|
632
|
+
print(f"Library '{library.name}' compiled successfully")
|
|
633
|
+
|
|
634
|
+
return library.archive_file
|
|
635
|
+
|
|
636
|
+
except subprocess.CalledProcessError as e:
|
|
637
|
+
raise LibraryErrorESP32(f"Compilation failed for library '{library.name}': {e}") from e
|
|
638
|
+
except KeyboardInterrupt as ke:
|
|
639
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
640
|
+
|
|
641
|
+
handle_keyboard_interrupt_properly(ke)
|
|
642
|
+
raise # Never reached, but satisfies type checker
|
|
643
|
+
except Exception as e:
|
|
644
|
+
raise LibraryErrorESP32(f"Failed to compile library '{library.name}': {e}") from e
|
|
645
|
+
|
|
646
|
+
def ensure_libraries(
|
|
647
|
+
self,
|
|
648
|
+
lib_specs: List[str],
|
|
649
|
+
toolchain_path: Path,
|
|
650
|
+
compiler_flags: List[str],
|
|
651
|
+
include_paths: List[Path],
|
|
652
|
+
show_progress: bool = True,
|
|
653
|
+
) -> List[LibraryESP32]:
|
|
654
|
+
"""Ensure all library dependencies are downloaded and compiled.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
lib_specs: List of library specification strings
|
|
658
|
+
toolchain_path: Path to toolchain bin directory
|
|
659
|
+
compiler_flags: Compiler flags
|
|
660
|
+
include_paths: Include directories
|
|
661
|
+
show_progress: Whether to show progress
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
List of compiled LibraryESP32 instances
|
|
665
|
+
"""
|
|
666
|
+
libraries = []
|
|
667
|
+
|
|
668
|
+
for spec_str in lib_specs:
|
|
669
|
+
# Parse library specification
|
|
670
|
+
spec = LibrarySpec.parse(spec_str)
|
|
671
|
+
|
|
672
|
+
# Download library
|
|
673
|
+
library = self.download_library(spec, show_progress)
|
|
674
|
+
|
|
675
|
+
# Check if rebuild needed
|
|
676
|
+
needs_rebuild, reason = self.needs_rebuild(library, compiler_flags)
|
|
677
|
+
|
|
678
|
+
if needs_rebuild:
|
|
679
|
+
if show_progress and reason:
|
|
680
|
+
print(f"Rebuilding library '{library.name}': {reason}")
|
|
681
|
+
|
|
682
|
+
self.compile_library(
|
|
683
|
+
library,
|
|
684
|
+
toolchain_path,
|
|
685
|
+
compiler_flags,
|
|
686
|
+
include_paths,
|
|
687
|
+
show_progress,
|
|
688
|
+
)
|
|
689
|
+
else:
|
|
690
|
+
if show_progress:
|
|
691
|
+
print(f"Library '{library.name}' is up to date")
|
|
692
|
+
|
|
693
|
+
libraries.append(library)
|
|
694
|
+
|
|
695
|
+
return libraries
|
|
696
|
+
|
|
697
|
+
def get_library_archives(self) -> List[Path]:
|
|
698
|
+
"""Get paths to all compiled library archives.
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
List of .a archive file paths
|
|
702
|
+
"""
|
|
703
|
+
archives = []
|
|
704
|
+
if self.libs_dir.exists():
|
|
705
|
+
for lib_dir in self.libs_dir.iterdir():
|
|
706
|
+
if lib_dir.is_dir():
|
|
707
|
+
archive = lib_dir / f"lib{lib_dir.name}.a"
|
|
708
|
+
if archive.exists():
|
|
709
|
+
archives.append(archive)
|
|
710
|
+
return archives
|
|
711
|
+
|
|
712
|
+
def get_library_include_paths(self) -> List[Path]:
|
|
713
|
+
"""Get all include paths from downloaded libraries.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
List of include directory paths
|
|
717
|
+
"""
|
|
718
|
+
include_paths = []
|
|
719
|
+
if self.libs_dir.exists():
|
|
720
|
+
for lib_dir in self.libs_dir.iterdir():
|
|
721
|
+
if lib_dir.is_dir():
|
|
722
|
+
library = LibraryESP32(lib_dir, lib_dir.name)
|
|
723
|
+
if library.exists:
|
|
724
|
+
include_paths.extend(library.get_include_dirs())
|
|
725
|
+
return include_paths
|