fbuild 1.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. fbuild/__init__.py +390 -0
  2. fbuild/assets/example.txt +1 -0
  3. fbuild/build/__init__.py +117 -0
  4. fbuild/build/archive_creator.py +186 -0
  5. fbuild/build/binary_generator.py +444 -0
  6. fbuild/build/build_component_factory.py +131 -0
  7. fbuild/build/build_info_generator.py +624 -0
  8. fbuild/build/build_state.py +325 -0
  9. fbuild/build/build_utils.py +93 -0
  10. fbuild/build/compilation_executor.py +422 -0
  11. fbuild/build/compiler.py +165 -0
  12. fbuild/build/compiler_avr.py +574 -0
  13. fbuild/build/configurable_compiler.py +664 -0
  14. fbuild/build/configurable_linker.py +637 -0
  15. fbuild/build/flag_builder.py +214 -0
  16. fbuild/build/library_dependency_processor.py +185 -0
  17. fbuild/build/linker.py +708 -0
  18. fbuild/build/orchestrator.py +67 -0
  19. fbuild/build/orchestrator_avr.py +651 -0
  20. fbuild/build/orchestrator_esp32.py +878 -0
  21. fbuild/build/orchestrator_rp2040.py +719 -0
  22. fbuild/build/orchestrator_stm32.py +696 -0
  23. fbuild/build/orchestrator_teensy.py +580 -0
  24. fbuild/build/source_compilation_orchestrator.py +218 -0
  25. fbuild/build/source_scanner.py +516 -0
  26. fbuild/cli.py +717 -0
  27. fbuild/cli_utils.py +314 -0
  28. fbuild/config/__init__.py +16 -0
  29. fbuild/config/board_config.py +542 -0
  30. fbuild/config/board_loader.py +92 -0
  31. fbuild/config/ini_parser.py +369 -0
  32. fbuild/config/mcu_specs.py +88 -0
  33. fbuild/daemon/__init__.py +42 -0
  34. fbuild/daemon/async_client.py +531 -0
  35. fbuild/daemon/client.py +1505 -0
  36. fbuild/daemon/compilation_queue.py +293 -0
  37. fbuild/daemon/configuration_lock.py +865 -0
  38. fbuild/daemon/daemon.py +585 -0
  39. fbuild/daemon/daemon_context.py +293 -0
  40. fbuild/daemon/error_collector.py +263 -0
  41. fbuild/daemon/file_cache.py +332 -0
  42. fbuild/daemon/firmware_ledger.py +546 -0
  43. fbuild/daemon/lock_manager.py +508 -0
  44. fbuild/daemon/logging_utils.py +149 -0
  45. fbuild/daemon/messages.py +957 -0
  46. fbuild/daemon/operation_registry.py +288 -0
  47. fbuild/daemon/port_state_manager.py +249 -0
  48. fbuild/daemon/process_tracker.py +366 -0
  49. fbuild/daemon/processors/__init__.py +18 -0
  50. fbuild/daemon/processors/build_processor.py +248 -0
  51. fbuild/daemon/processors/deploy_processor.py +664 -0
  52. fbuild/daemon/processors/install_deps_processor.py +431 -0
  53. fbuild/daemon/processors/locking_processor.py +777 -0
  54. fbuild/daemon/processors/monitor_processor.py +285 -0
  55. fbuild/daemon/request_processor.py +457 -0
  56. fbuild/daemon/shared_serial.py +819 -0
  57. fbuild/daemon/status_manager.py +238 -0
  58. fbuild/daemon/subprocess_manager.py +316 -0
  59. fbuild/deploy/__init__.py +21 -0
  60. fbuild/deploy/deployer.py +67 -0
  61. fbuild/deploy/deployer_esp32.py +310 -0
  62. fbuild/deploy/docker_utils.py +315 -0
  63. fbuild/deploy/monitor.py +519 -0
  64. fbuild/deploy/qemu_runner.py +603 -0
  65. fbuild/interrupt_utils.py +34 -0
  66. fbuild/ledger/__init__.py +52 -0
  67. fbuild/ledger/board_ledger.py +560 -0
  68. fbuild/output.py +352 -0
  69. fbuild/packages/__init__.py +66 -0
  70. fbuild/packages/archive_utils.py +1098 -0
  71. fbuild/packages/arduino_core.py +412 -0
  72. fbuild/packages/cache.py +256 -0
  73. fbuild/packages/concurrent_manager.py +510 -0
  74. fbuild/packages/downloader.py +518 -0
  75. fbuild/packages/fingerprint.py +423 -0
  76. fbuild/packages/framework_esp32.py +538 -0
  77. fbuild/packages/framework_rp2040.py +349 -0
  78. fbuild/packages/framework_stm32.py +459 -0
  79. fbuild/packages/framework_teensy.py +346 -0
  80. fbuild/packages/github_utils.py +96 -0
  81. fbuild/packages/header_trampoline_cache.py +394 -0
  82. fbuild/packages/library_compiler.py +203 -0
  83. fbuild/packages/library_manager.py +549 -0
  84. fbuild/packages/library_manager_esp32.py +725 -0
  85. fbuild/packages/package.py +163 -0
  86. fbuild/packages/platform_esp32.py +383 -0
  87. fbuild/packages/platform_rp2040.py +400 -0
  88. fbuild/packages/platform_stm32.py +581 -0
  89. fbuild/packages/platform_teensy.py +312 -0
  90. fbuild/packages/platform_utils.py +131 -0
  91. fbuild/packages/platformio_registry.py +369 -0
  92. fbuild/packages/sdk_utils.py +231 -0
  93. fbuild/packages/toolchain.py +436 -0
  94. fbuild/packages/toolchain_binaries.py +196 -0
  95. fbuild/packages/toolchain_esp32.py +489 -0
  96. fbuild/packages/toolchain_metadata.py +185 -0
  97. fbuild/packages/toolchain_rp2040.py +436 -0
  98. fbuild/packages/toolchain_stm32.py +417 -0
  99. fbuild/packages/toolchain_teensy.py +404 -0
  100. fbuild/platform_configs/esp32.json +150 -0
  101. fbuild/platform_configs/esp32c2.json +144 -0
  102. fbuild/platform_configs/esp32c3.json +143 -0
  103. fbuild/platform_configs/esp32c5.json +151 -0
  104. fbuild/platform_configs/esp32c6.json +151 -0
  105. fbuild/platform_configs/esp32p4.json +149 -0
  106. fbuild/platform_configs/esp32s3.json +151 -0
  107. fbuild/platform_configs/imxrt1062.json +56 -0
  108. fbuild/platform_configs/rp2040.json +70 -0
  109. fbuild/platform_configs/rp2350.json +76 -0
  110. fbuild/platform_configs/stm32f1.json +59 -0
  111. fbuild/platform_configs/stm32f4.json +63 -0
  112. fbuild/py.typed +0 -0
  113. fbuild-1.2.8.dist-info/METADATA +468 -0
  114. fbuild-1.2.8.dist-info/RECORD +121 -0
  115. fbuild-1.2.8.dist-info/WHEEL +5 -0
  116. fbuild-1.2.8.dist-info/entry_points.txt +5 -0
  117. fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
  118. fbuild-1.2.8.dist-info/top_level.txt +2 -0
  119. fbuild_lint/__init__.py +0 -0
  120. fbuild_lint/ruff_plugins/__init__.py +0 -0
  121. fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
@@ -0,0 +1,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