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,549 @@
1
+ """Library dependency management for fbuild.
2
+
3
+ This module handles downloading, extracting, and compiling external libraries
4
+ from lib_deps in platformio.ini. Libraries are cached locally in the build
5
+ directory for fast rebuilds and proper invalidation.
6
+ """
7
+
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional, Tuple
11
+ from urllib.parse import urlparse
12
+
13
+ from fbuild.packages.downloader import PackageDownloader
14
+ from fbuild.packages.github_utils import GitHubURLOptimizer
15
+ from fbuild.packages.library_compiler import LibraryCompilationError, LibraryCompiler
16
+
17
+
18
+ class LibraryError(Exception):
19
+ """Base exception for library management errors."""
20
+
21
+ pass
22
+
23
+
24
+ class LibraryInfo:
25
+ """Metadata about a downloaded and compiled library."""
26
+
27
+ def __init__(
28
+ self,
29
+ name: str,
30
+ url: str,
31
+ version: str,
32
+ commit_hash: Optional[str],
33
+ compiler: str,
34
+ compile_commands: List[str],
35
+ link_commands: List[str],
36
+ ):
37
+ """Initialize library info.
38
+
39
+ Args:
40
+ name: Library name
41
+ url: Source URL
42
+ version: Version or commit
43
+ commit_hash: Git commit hash if available
44
+ compiler: Compiler used
45
+ compile_commands: Commands used for compilation
46
+ link_commands: Commands/flags for linking
47
+ """
48
+ self.name = name
49
+ self.url = url
50
+ self.version = version
51
+ self.commit_hash = commit_hash
52
+ self.compiler = compiler
53
+ self.compile_commands = compile_commands
54
+ self.link_commands = link_commands
55
+
56
+ def to_dict(self) -> Dict:
57
+ """Convert to dictionary for JSON serialization."""
58
+ return {
59
+ "name": self.name,
60
+ "url": self.url,
61
+ "version": self.version,
62
+ "commit_hash": self.commit_hash,
63
+ "compiler": self.compiler,
64
+ "compile_commands": self.compile_commands,
65
+ "link_commands": self.link_commands,
66
+ }
67
+
68
+ @classmethod
69
+ def from_dict(cls, data: Dict) -> "LibraryInfo":
70
+ """Create from dictionary."""
71
+ return cls(
72
+ name=data["name"],
73
+ url=data["url"],
74
+ version=data["version"],
75
+ commit_hash=data.get("commit_hash"),
76
+ compiler=data["compiler"],
77
+ compile_commands=data["compile_commands"],
78
+ link_commands=data["link_commands"],
79
+ )
80
+
81
+ def save(self, path: Path) -> None:
82
+ """Save library info to JSON file.
83
+
84
+ Args:
85
+ path: Path to info.json file
86
+ """
87
+ with open(path, "w", encoding="utf-8") as f:
88
+ json.dump(self.to_dict(), f, indent=2)
89
+
90
+ @classmethod
91
+ def load(cls, path: Path) -> "LibraryInfo":
92
+ """Load library info from JSON file.
93
+
94
+ Args:
95
+ path: Path to info.json file
96
+
97
+ Returns:
98
+ LibraryInfo instance
99
+ """
100
+ with open(path, "r", encoding="utf-8") as f:
101
+ data = json.load(f)
102
+ return cls.from_dict(data)
103
+
104
+
105
+ class Library:
106
+ """Represents a downloaded and compiled library."""
107
+
108
+ def __init__(self, lib_dir: Path, name: str):
109
+ """Initialize library.
110
+
111
+ Args:
112
+ lib_dir: Root directory for the library (.fbuild/build/{mode}/libs/{name})
113
+ name: Library name
114
+ """
115
+ self.lib_dir = lib_dir
116
+ self.name = name
117
+ self.src_dir = lib_dir / "src"
118
+ self.info_file = lib_dir / "info.json"
119
+ self.archive_file = lib_dir / f"lib{name}.a"
120
+
121
+ @property
122
+ def exists(self) -> bool:
123
+ """Check if library is downloaded and compiled."""
124
+ return self.lib_dir.exists() and self.src_dir.exists() and self.archive_file.exists() and self.info_file.exists()
125
+
126
+ def get_info(self) -> Optional[LibraryInfo]:
127
+ """Load library info if available."""
128
+ if self.info_file.exists():
129
+ return LibraryInfo.load(self.info_file)
130
+ return None
131
+
132
+ def get_source_files(self) -> List[Path]:
133
+ """Find all source files (.c, .cpp) in the library.
134
+
135
+ Returns:
136
+ List of source file paths
137
+ """
138
+ if not self.src_dir.exists():
139
+ return []
140
+
141
+ sources = []
142
+
143
+ # Check for src/src/ structure (like FastLED)
144
+ src_src = self.src_dir / "src"
145
+ search_dir = src_src if (src_src.exists() and src_src.is_dir()) else self.src_dir
146
+
147
+ for pattern in ["**/*.c", "**/*.cpp", "**/*.cc", "**/*.cxx"]:
148
+ sources.extend(search_dir.glob(pattern))
149
+
150
+ return sources
151
+
152
+ def get_include_dirs(self) -> List[Path]:
153
+ """Get include directories for this library.
154
+
155
+ For Arduino libraries, the typical structure is:
156
+ - libname/src/ (contains headers and sources)
157
+ - or libname/ (root contains headers)
158
+
159
+ Returns:
160
+ List of include directory paths
161
+ """
162
+ include_dirs = []
163
+
164
+ if not self.src_dir.exists():
165
+ return include_dirs
166
+
167
+ # Check if there's a src subdirectory inside src_dir
168
+ # This happens with libraries like FastLED that have src/src/ structure
169
+ src_src = self.src_dir / "src"
170
+ if src_src.exists() and src_src.is_dir():
171
+ # Arduino library with src/src/ structure
172
+ # Add src/src/ as the main include path
173
+ include_dirs.append(src_src)
174
+ else:
175
+ # Standard Arduino library structure
176
+ # Add src/ as the include path
177
+ include_dirs.append(self.src_dir)
178
+
179
+ # Look for additional include directories
180
+ for name in ["include", "Include", "INCLUDE"]:
181
+ inc_dir = self.lib_dir / name
182
+ if inc_dir.exists():
183
+ include_dirs.append(inc_dir)
184
+
185
+ return include_dirs
186
+
187
+
188
+ class LibraryManager:
189
+ """Manages library dependencies for a build."""
190
+
191
+ def __init__(
192
+ self,
193
+ build_dir: Path,
194
+ mode: str = "release",
195
+ downloader: Optional[PackageDownloader] = None,
196
+ ):
197
+ """Initialize library manager.
198
+
199
+ Args:
200
+ build_dir: Build directory (.fbuild/build/{env_name})
201
+ mode: Build mode (release, quick, debug)
202
+ downloader: Optional custom downloader instance
203
+ """
204
+ self.build_dir = Path(build_dir)
205
+ self.mode = mode
206
+ self.libs_dir = self.build_dir / "libs"
207
+ self.downloader = downloader or PackageDownloader()
208
+
209
+ # Ensure libs directory exists
210
+ self.libs_dir.mkdir(parents=True, exist_ok=True)
211
+
212
+ def _extract_library_name(self, url: str) -> str:
213
+ """Extract library name from URL.
214
+
215
+ Args:
216
+ url: Library URL
217
+
218
+ Returns:
219
+ Library name in lowercase
220
+ """
221
+ # For GitHub URLs, use the repo name
222
+ if GitHubURLOptimizer.is_github_url(url):
223
+ parts = urlparse(url).path.strip("/").split("/")
224
+ if len(parts) >= 2:
225
+ name = parts[1]
226
+ # Remove .git suffix if present
227
+ if name.endswith(".git"):
228
+ name = name[:-4]
229
+ return name.lower()
230
+
231
+ # For other URLs, use the filename without extension
232
+ filename = Path(urlparse(url).path).name
233
+ # Remove common archive extensions
234
+ for ext in [".zip", ".tar.gz", ".tar.bz2", ".tar.xz"]:
235
+ if filename.endswith(ext):
236
+ filename = filename[: -len(ext)]
237
+ break
238
+
239
+ return filename.lower()
240
+
241
+ def get_library(self, name: str) -> Library:
242
+ """Get a Library instance for a given name.
243
+
244
+ Args:
245
+ name: Library name
246
+
247
+ Returns:
248
+ Library instance
249
+ """
250
+ lib_dir = self.libs_dir / name
251
+ return Library(lib_dir, name)
252
+
253
+ def download_library(self, url: str, show_progress: bool = True) -> Library:
254
+ """Download and extract a library from a URL.
255
+
256
+ Args:
257
+ url: Library source URL
258
+ show_progress: Whether to show download progress
259
+
260
+ Returns:
261
+ Library instance for the downloaded library
262
+
263
+ Raises:
264
+ LibraryError: If download or extraction fails
265
+ """
266
+ try:
267
+ # Optimize GitHub URLs
268
+ original_url = url
269
+ if GitHubURLOptimizer.is_github_url(url):
270
+ url = GitHubURLOptimizer.optimize_url(url)
271
+ if show_progress:
272
+ print(f"Optimized GitHub URL: {url}")
273
+
274
+ # Extract library name
275
+ lib_name = self._extract_library_name(original_url)
276
+ library = self.get_library(lib_name)
277
+
278
+ # Skip if already downloaded
279
+ if library.exists:
280
+ if show_progress:
281
+ print(f"Library '{lib_name}' already downloaded")
282
+ return library
283
+
284
+ # Create library directory
285
+ library.lib_dir.mkdir(parents=True, exist_ok=True)
286
+
287
+ # Download to temporary location
288
+ filename = Path(urlparse(url).path).name
289
+ temp_archive = library.lib_dir / filename
290
+
291
+ if show_progress:
292
+ print(f"Downloading library: {lib_name}")
293
+
294
+ self.downloader.download(url, temp_archive, show_progress=show_progress)
295
+
296
+ # Extract to src directory
297
+ if show_progress:
298
+ print(f"Extracting library: {lib_name}")
299
+
300
+ temp_extract = library.lib_dir / "_extract"
301
+ temp_extract.mkdir(exist_ok=True)
302
+
303
+ self.downloader.extract_archive(temp_archive, temp_extract, show_progress=show_progress)
304
+
305
+ # Find the actual source directory
306
+ # Archives often have a top-level directory like "FastLED-main"
307
+ extracted_contents = list(temp_extract.iterdir())
308
+
309
+ if len(extracted_contents) == 1 and extracted_contents[0].is_dir():
310
+ # Single directory - move its contents
311
+ source_root = extracted_contents[0]
312
+ else:
313
+ # Multiple items - use extract dir as root
314
+ source_root = temp_extract
315
+
316
+ # Move to src directory
317
+ if library.src_dir.exists():
318
+ import shutil
319
+
320
+ shutil.rmtree(library.src_dir)
321
+
322
+ source_root.rename(library.src_dir)
323
+
324
+ # Clean up
325
+ if temp_extract.exists():
326
+ import shutil
327
+
328
+ shutil.rmtree(temp_extract)
329
+ temp_archive.unlink()
330
+
331
+ # Create initial info.json
332
+ info = LibraryInfo(
333
+ name=lib_name,
334
+ url=original_url,
335
+ version="unknown",
336
+ commit_hash=None,
337
+ compiler="",
338
+ compile_commands=[],
339
+ link_commands=[],
340
+ )
341
+ info.save(library.info_file)
342
+
343
+ return library
344
+
345
+ except KeyboardInterrupt as ke:
346
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
347
+
348
+ handle_keyboard_interrupt_properly(ke)
349
+ raise # Never reached, but satisfies type checker
350
+ except Exception as e:
351
+ raise LibraryError(f"Failed to download library from {url}: {e}") from e
352
+
353
+ def needs_rebuild(self, library: Library, compiler_flags: List[str]) -> Tuple[bool, str]:
354
+ """Check if a library needs to be rebuilt.
355
+
356
+ Args:
357
+ library: Library to check
358
+ compiler_flags: Current compiler flags
359
+
360
+ Returns:
361
+ Tuple of (needs_rebuild, reason)
362
+ """
363
+ return LibraryCompiler.needs_rebuild(
364
+ library.archive_file,
365
+ library.info_file,
366
+ compiler_flags,
367
+ library.get_info,
368
+ )
369
+
370
+ def compile_library(
371
+ self,
372
+ library: Library,
373
+ compiler_path: Path,
374
+ mcu: str,
375
+ f_cpu: str,
376
+ defines: List[str],
377
+ include_paths: List[Path],
378
+ extra_flags: List[str],
379
+ show_progress: bool = True,
380
+ ) -> Path:
381
+ """Compile a library into a static archive (.a file).
382
+
383
+ Args:
384
+ library: Library to compile
385
+ compiler_path: Path to avr-gcc/avr-g++
386
+ mcu: MCU target (e.g., atmega328p)
387
+ f_cpu: CPU frequency (e.g., 16000000L)
388
+ defines: Preprocessor defines
389
+ include_paths: Include directories
390
+ extra_flags: Additional compiler flags
391
+ show_progress: Whether to show progress
392
+
393
+ Returns:
394
+ Path to compiled archive file
395
+
396
+ Raises:
397
+ LibraryError: If compilation fails
398
+ """
399
+ try:
400
+ # Get source files
401
+ sources = library.get_source_files()
402
+ if not sources:
403
+ raise LibraryError(f"No source files found in library '{library.name}'")
404
+
405
+ # Get library's own include directories
406
+ lib_includes = library.get_include_dirs()
407
+ all_includes = list(include_paths) + lib_includes
408
+
409
+ # Compile library using LibraryCompiler
410
+ archive_file, _, compile_commands = LibraryCompiler.compile_library(
411
+ library_name=library.name,
412
+ lib_dir=library.lib_dir,
413
+ source_files=sources,
414
+ include_dirs=all_includes,
415
+ compiler_path=compiler_path,
416
+ mcu=mcu,
417
+ f_cpu=f_cpu,
418
+ defines=defines,
419
+ extra_flags=extra_flags,
420
+ show_progress=show_progress,
421
+ )
422
+
423
+ # Update info.json with compile information
424
+ info = library.get_info() or LibraryInfo(
425
+ name=library.name,
426
+ url="",
427
+ version="unknown",
428
+ commit_hash=None,
429
+ compiler="",
430
+ compile_commands=[],
431
+ link_commands=[],
432
+ )
433
+
434
+ info.compiler = str(compiler_path)
435
+ info.compile_commands = compile_commands
436
+ info.link_commands = [str(archive_file)]
437
+ info.save(library.info_file)
438
+
439
+ return archive_file
440
+
441
+ except LibraryCompilationError as e:
442
+ raise LibraryError(str(e)) from e
443
+ except KeyboardInterrupt as ke:
444
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
445
+
446
+ handle_keyboard_interrupt_properly(ke)
447
+ raise # Never reached, but satisfies type checker
448
+ except Exception as e:
449
+ raise LibraryError(f"Failed to compile library '{library.name}': {e}") from e
450
+
451
+ def ensure_libraries(
452
+ self,
453
+ lib_deps: List[str],
454
+ compiler_path: Path,
455
+ mcu: str,
456
+ f_cpu: str,
457
+ defines: List[str],
458
+ include_paths: List[Path],
459
+ extra_flags: List[str],
460
+ show_progress: bool = True,
461
+ ) -> List[Library]:
462
+ """Ensure all library dependencies are downloaded and compiled.
463
+
464
+ Args:
465
+ lib_deps: List of library dependency URLs
466
+ compiler_path: Path to compiler
467
+ mcu: MCU target
468
+ f_cpu: CPU frequency
469
+ defines: Preprocessor defines
470
+ include_paths: Include directories
471
+ extra_flags: Additional compiler flags
472
+ show_progress: Whether to show progress
473
+
474
+ Returns:
475
+ List of compiled Library instances
476
+ """
477
+ libraries = []
478
+
479
+ for url in lib_deps:
480
+ # Download library
481
+ library = self.download_library(url, show_progress)
482
+
483
+ # Check if rebuild needed
484
+ needs_rebuild, reason = self.needs_rebuild(library, extra_flags)
485
+
486
+ if needs_rebuild:
487
+ if show_progress and reason:
488
+ print(f"Rebuilding library '{library.name}': {reason}")
489
+
490
+ self.compile_library(
491
+ library,
492
+ compiler_path,
493
+ mcu,
494
+ f_cpu,
495
+ defines,
496
+ include_paths,
497
+ extra_flags,
498
+ show_progress,
499
+ )
500
+
501
+ libraries.append(library)
502
+
503
+ return libraries
504
+
505
+ def get_library_archives(self) -> List[Path]:
506
+ """Get paths to all compiled library archives.
507
+
508
+ Returns:
509
+ List of .a archive file paths
510
+ """
511
+ archives = []
512
+ if self.libs_dir.exists():
513
+ for lib_dir in self.libs_dir.iterdir():
514
+ if lib_dir.is_dir():
515
+ archive = lib_dir / f"lib{lib_dir.name}.a"
516
+ if archive.exists():
517
+ archives.append(archive)
518
+ return archives
519
+
520
+ def get_library_objects(self) -> List[Path]:
521
+ """Get paths to all compiled library object files.
522
+
523
+ This is useful for LTO linking where object files work better than archives.
524
+
525
+ Returns:
526
+ List of .o object file paths from all libraries
527
+ """
528
+ objects = []
529
+ if self.libs_dir.exists():
530
+ for lib_dir in self.libs_dir.iterdir():
531
+ if lib_dir.is_dir():
532
+ # Find all .o files in the library directory
533
+ for obj_file in lib_dir.glob("*.o"):
534
+ objects.append(obj_file)
535
+ return objects
536
+
537
+ def get_library_include_paths(self) -> List[Path]:
538
+ """Get all include paths from downloaded libraries.
539
+
540
+ Returns:
541
+ List of include directory paths
542
+ """
543
+ include_paths = []
544
+ if self.libs_dir.exists():
545
+ for lib_dir in self.libs_dir.iterdir():
546
+ if lib_dir.is_dir():
547
+ library = Library(lib_dir, lib_dir.name)
548
+ include_paths.extend(library.get_include_dirs())
549
+ return include_paths