diff --git a/backend/app/services/espidf_compiler.py b/backend/app/services/espidf_compiler.py index 48587c65..a380e6cd 100644 --- a/backend/app/services/espidf_compiler.py +++ b/backend/app/services/espidf_compiler.py @@ -454,32 +454,42 @@ class ESPIDFCompiler: found_any = True header_to_comp[header] = 'user_libs_all' - # Copy all source files flat into the merged component directory. - # First-writer wins for name conflicts (rare across Arduino libs). - for pattern in ('*.h', '*.cpp', '*.c', 'src/*.h', 'src/*.cpp', 'src/*.c'): - glob_root = src_root.parent if pattern.startswith('src/') else src_root - for f in glob_root.glob(pattern): - if not f.is_file(): - continue - if f.name not in seen_names: - shutil.copy2(f, comp_dir / f.name) - seen_names.add(f.name) - if f.suffix in ('.cpp', '.c') and f.name not in cpp_files: - cpp_files.append(f.name) + # Preserve directory structure while merging libraries. + # Skip non-buildable directories like examples, tests, docs. + lib_root = src_root.parent if src_root.name == 'src' else src_root + has_src_layout = (lib_root / 'src').is_dir() - # Also handle libraries that use an src/ subdirectory layout. - src_sub = (src_root / 'src') if (src_root / 'src').is_dir() else None - if src_sub is None and (src_root.parent / 'src').is_dir(): - src_sub = src_root.parent / 'src' - if src_sub: - for f in src_sub.glob('**/*'): - if not f.is_file() or f.suffix not in ('.h', '.cpp', '.c'): - continue - if f.name not in seen_names: - shutil.copy2(f, comp_dir / f.name) - seen_names.add(f.name) - if f.suffix in ('.cpp', '.c') and f.name not in cpp_files: - cpp_files.append(f.name) + excluded_dirs = { + '.git', '.github', '.vscode', '__pycache__', + 'docs', 'doc', 'example', 'examples', 'test', 'tests', + 'extras', 'ci', 'fuzz', 'fuzzing', 'benchmark', 'benchmarks', + } + + def _should_include(rel_path: Path) -> bool: + parts = rel_path.parts + if any(part.lower() in excluded_dirs for part in parts[:-1]): + return False + if rel_path.suffix not in ('.h', '.hpp', '.c', '.cpp'): + return False + if has_src_layout: + return parts[0] == 'src' or len(parts) == 1 + return len(parts) == 1 or parts[0].lower() == 'utility' + + for f in lib_root.rglob('*'): + if not f.is_file(): + continue + rel_path = f.relative_to(lib_root) + if not _should_include(rel_path): + continue + # Track file by its relative path to preserve structure + file_key = str(rel_path).replace('\\', '/') + if file_key not in seen_names: + dest = comp_dir / rel_path + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(f, dest) + seen_names.add(file_key) + if f.suffix in ('.cpp', '.c') and file_key not in cpp_files: + cpp_files.append(file_key) # Scan newly copied headers for transitive includes. for lib_file in comp_dir.glob('*.h'): @@ -497,13 +507,22 @@ class ESPIDFCompiler: return [], {} srcs_line = 'SRCS ' + ' '.join(f'"{f}"' for f in sorted(cpp_files)) if cpp_files else '' + + # Generate INCLUDE_DIRS from the directory structure of copied files + include_dirs: set[str] = {'.'} + for file_key in seen_names: + parent = str(Path(file_key).parent) + if parent and parent != '.': + include_dirs.add(parent) + + include_dirs_line = 'INCLUDE_DIRS ' + ' '.join(f'"{d}"' for d in sorted(include_dirs)) + cmake_content = ( '# Auto-generated by Velxio — all user libraries merged into one component.\n' - '# Single flat directory: every header sees every other header without\n' - '# cross-component REQUIRES propagation.\n' + '# Directory structure preserved for libraries like ArduinoJson with src/ layout.\n' 'idf_component_register(\n' f' {srcs_line}\n' - ' INCLUDE_DIRS "."\n' + f' {include_dirs_line}\n' f' REQUIRES {arduino_comp_name}\n' ')\n' ) @@ -567,47 +586,80 @@ class ESPIDFCompiler: comp_dir = user_libs_dir / safe_name comp_dir.mkdir(parents=True, exist_ok=True) - # Collect all source files — preserve subdirectory structure via INCLUDE_DIRS - # We copy files flat into the component root but add src/ as an include dir - cpp_files: list[str] = [] - seen_names: set[str] = set() + # Preserve the original library layout for actual buildable library code + # while skipping repo-only content such as examples, tests, and CI files. + lib_root = src_root.parent if src_root.name == 'src' else src_root + include_dirs: set[str] = {'.'} + cpp_files: set[str] = set() + copied_any = False - for pattern in ('*.h', '*.cpp', '*.c', 'src/*.h', 'src/*.cpp', 'src/*.c'): - for f in src_root.parent.glob(pattern) if pattern.startswith('src/') else src_root.glob(pattern): - if not f.is_file(): - continue - dest = comp_dir / f.name - if f.name not in seen_names: - shutil.copy2(f, dest) - seen_names.add(f.name) - if f.suffix in ('.cpp', '.c') and f.name not in cpp_files: - cpp_files.append(f.name) + has_src_layout = (lib_root / 'src').is_dir() + excluded_dirs = { + '.git', + '.github', + '.vscode', + '__pycache__', + 'docs', + 'doc', + 'example', + 'examples', + 'test', + 'tests', + 'extras', + 'ci', + 'fuzz', + 'fuzzing', + 'benchmark', + 'benchmarks', + } - # Also copy from src/ subdirectory if present (e.g. Adafruit libraries) - src_sub = src_root / 'src' if (src_root / 'src').is_dir() else None - if src_sub is None and (src_root.parent / 'src').is_dir(): - src_sub = src_root.parent / 'src' - if src_sub: - for f in src_sub.glob('**/*'): - if not f.is_file() or f.suffix not in ('.h', '.cpp', '.c'): - continue - if f.name not in seen_names: - shutil.copy2(f, comp_dir / f.name) - seen_names.add(f.name) - if f.suffix in ('.cpp', '.c') and f.name not in cpp_files: - cpp_files.append(f.name) + def should_include(relative_path: Path) -> bool: + parts = relative_path.parts + if any(part.lower() in excluded_dirs for part in parts[:-1]): + return False + if relative_path.suffix not in ('.h', '.hpp', '.c', '.cpp'): + return False + + if has_src_layout: + return parts[0] == 'src' or len(parts) == 1 + + return len(parts) == 1 or parts[0].lower() == 'utility' + + for f in lib_root.rglob('*'): + if not f.is_file(): + continue + rel_path = f.relative_to(lib_root) + if not should_include(rel_path): + continue + dest = comp_dir / rel_path + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(f, dest) + copied_any = True + include_dirs.add(str(rel_path.parent).replace('\\', '/')) + if f.suffix in ('.cpp', '.c'): + cpp_files.add(str(rel_path).replace('\\', '/')) + + if not copied_any: + raise ValueError(f'No buildable source files found in library {lib_dir_name}') # Generate CMakeLists.txt for this component + include_dirs.discard('.') + ordered_include_dirs = ['.'] + sorted(d for d in include_dirs if d and d != '.') + if cpp_files: srcs_line = 'SRCS ' + ' '.join(f'"{f}"' for f in sorted(cpp_files)) else: srcs_line = '# header-only library' + include_dirs_line = 'INCLUDE_DIRS ' + ' '.join( + f'"{include_dir}"' for include_dir in ordered_include_dirs + ) + cmake_content = ( f'# Auto-generated by Velxio for library: {lib_dir_name}\n' f'idf_component_register(\n' f' {srcs_line}\n' - f' INCLUDE_DIRS "."\n' + f' {include_dirs_line}\n' f' REQUIRES {arduino_comp_name}\n' f')\n' )