Copy FastLED library to build directory with proper library.json structure.
(project_root: Path, build_dir: Path)
| 343 | |
| 344 | |
| 345 | def copy_fastled_library(project_root: Path, build_dir: Path) -> bool: |
| 346 | """Copy FastLED library to build directory with proper library.json structure.""" |
| 347 | lib_dir = build_dir / "lib" / "FastLED" |
| 348 | lib_parent = build_dir / "lib" |
| 349 | |
| 350 | # Copy src/ directory into lib/FastLED using dirsync for better caching |
| 351 | fastled_src_path = project_root / "src" |
| 352 | try: |
| 353 | # Create lib directory if it doesn't exist |
| 354 | lib_parent.mkdir(parents=True, exist_ok=True) |
| 355 | |
| 356 | # Try to remove existing FastLED directory if it exists |
| 357 | # On Windows, files may be locked by antivirus or explorer, so handle gracefully |
| 358 | if lib_dir.exists(): |
| 359 | if lib_dir.is_symlink(): |
| 360 | lib_dir.unlink() |
| 361 | else: |
| 362 | # On Windows, add retry logic for rmtree since files may still be locked |
| 363 | # This can happen when antivirus software or explorer.exe still has handles open |
| 364 | max_rmtree_retries = 5 if sys.platform == "win32" else 1 |
| 365 | rmtree_delay_ms = 100 if sys.platform == "win32" else 0 |
| 366 | |
| 367 | for rmtree_attempt in range(max_rmtree_retries): |
| 368 | try: |
| 369 | shutil.rmtree(lib_dir) |
| 370 | break # Success |
| 371 | except OSError as e: |
| 372 | if ( |
| 373 | sys.platform == "win32" |
| 374 | and rmtree_attempt < max_rmtree_retries - 1 |
| 375 | ): |
| 376 | delay_seconds = rmtree_delay_ms / 1000.0 |
| 377 | time.sleep(delay_seconds) |
| 378 | elif rmtree_attempt == max_rmtree_retries - 1: |
| 379 | # On last attempt failure, just warn and continue with update instead |
| 380 | print( |
| 381 | f"Warning: Could not remove old FastLED library, will update existing: {e}" |
| 382 | ) |
| 383 | break |
| 384 | |
| 385 | # Ensure target directory exists for dirsync |
| 386 | lib_dir.mkdir(parents=True, exist_ok=True) |
| 387 | |
| 388 | # Use dirsync.sync for efficient incremental synchronization |
| 389 | # Only pass content=True in Docker environments (content-based comparison) |
| 390 | # On other systems, use default behavior (timestamp/size) by omitting the parameter |
| 391 | # On Windows, add retry logic for file locking issues |
| 392 | max_retries = 3 if sys.platform == "win32" else 1 |
| 393 | retry_delay = 0.5 # seconds |
| 394 | |
| 395 | for attempt in range(max_retries): |
| 396 | try: |
| 397 | _is_docker = os.environ.get("DOCKER_CONTAINER", "") != "" |
| 398 | _scandir_sync( |
| 399 | str(fastled_src_path), |
| 400 | str(lib_dir), |
| 401 | purge=True, |
| 402 | content=_is_docker, |
no test coverage detected