#!/usr/bin/env python3 """ Export Header Fixer v3 - Improved version This script fixes incorrect export header includes in migrated Qt6 code. It properly maps source files to their CMake targets by analyzing SRCS variables. """ import os import re import sys from pathlib import Path def find_cmake_files(root_dir): """Find all CMakeLists.txt files""" cmake_files = [] for root, dirs, files in os.walk(root_dir): if 'CMakeLists.txt' in files: cmake_files.append(os.path.join(root, 'CMakeLists.txt')) return cmake_files def parse_cmake_file(cmake_file, root_dir=None): """ Parse CMakeLists.txt and extract target information. Returns dict: {source_file: target_name} If root_dir is provided, keys are relative paths, otherwise just filenames. """ target_mapping = {} cmake_dir = os.path.dirname(cmake_file) with open(cmake_file, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() # Remove comments content = re.sub(r'#.*', '', content) # Join lines for multi-line commands content = re.sub(r'\\\n', ' ', content) # Find all NGL_ADD_LIBRARY2 calls with their SRCS variables # Pattern: SET( ) followed by NGL_ADD_LIBRARY2(... ...) # First, find all SET commands that define source file lists src_vars = {} set_pattern = r'SET\s*\(\s*(SRCS\w*)\s+((?:[^)]+))\)' for match in re.finditer(set_pattern, content, re.IGNORECASE): var_name = match.group(1) files_str = match.group(2) # Extract file names (remove .cpp, .h extensions for matching) files = re.findall(r'([\w_-]+\.(?:cpp|h|cc|cxx))', files_str) src_vars[var_name] = files # Find all NGL_ADD_LIBRARY2 and NGL_ADD_LIBRARY calls # Pattern 1: NGL_ADD_LIBRARY2([targets...] TARGET_NAME type ${SRCS_VAR}) lib_pattern = r'NGL_ADD_LIBRARY2?\s*\(([^)]+)\)' for match in re.finditer(lib_pattern, content, re.IGNORECASE): args = match.group(1).strip() # Split into tokens tokens = args.split() # Find SHARED or MODULE to identify library type lib_type_idx = -1 for i, token in enumerate(tokens): if token.upper() in ('SHARED', 'MODULE', 'STATIC'): lib_type_idx = i break if lib_type_idx < 1: continue # Target name is right before SHARED/MODULE target_name = tokens[lib_type_idx - 1] # Find SRCS variable after SHARED/MODULE src_var = None for i in range(lib_type_idx + 1, len(tokens)): token = tokens[i] if token.startswith('${') and token.endswith('}'): src_var = token[2:-1] # Remove ${ and } break elif token.endswith('_SRCS') or token.endswith('_HDRS'): src_var = token break # Map source files to target if src_var and src_var in src_vars: for src_file in src_vars[src_var]: if root_dir: # Store with relative path full_path = os.path.join(cmake_dir, src_file) rel_path = os.path.relpath(full_path, root_dir) target_mapping[rel_path] = target_name # Always store filename-only mapping too (will be overwritten by last occurrence) target_mapping[src_file] = target_name return target_mapping def find_export_header_includes(file_path): """Find export header includes in a file""" includes = [] with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: for line_num, line in enumerate(f, 1): # Match both <...> and "..." includes with _export.h match = re.search(r'#include\s*[<"]([^>"\s]+_export\.h)[>"]', line) if match: includes.append((line_num, line.strip(), match.group(1))) return includes def get_expected_export_header(target_name): """Get expected export header name for a target""" return f"{target_name.lower()}_export.h" def fix_export_header(file_path, old_include, new_include): """Replace old export header include with new one""" with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() # Replace the include - handle both <...> and "..." old_pattern = re.escape(old_include) # Match both angle brackets and quotes content = re.sub( rf'#include\s*[<"]{old_pattern}[>"]', f'#include "{new_include}"', content ) with open(file_path, 'w', encoding='utf-8') as f: f.write(content) def main(): if len(sys.argv) < 2: print("Usage: fix_export_headers_v3.py ") sys.exit(1) root_dir = sys.argv[1] print(f"Scanning directory: {root_dir}") print("=" * 60) # Build target mapping from all CMakeLists.txt files cmake_files = find_cmake_files(root_dir) print(f"Found {len(cmake_files)} CMakeLists.txt files") target_mapping = {} for cmake_file in cmake_files: file_mapping = parse_cmake_file(cmake_file, root_dir) target_mapping.update(file_mapping) print(f"Mapped {len(target_mapping)} source files to targets") print() # Find all header files and check for export header includes fixed = 0 skipped = 0 errors = 0 for root, dirs, files in os.walk(root_dir): for file in files: if file.endswith(('.h', '.hpp', '.hxx')): file_path = os.path.join(root, file) rel_path = os.path.relpath(file_path, root_dir) # Get just the filename for mapping filename = os.path.basename(file_path) includes = find_export_header_includes(file_path) for line_num, line, old_include in includes: # Skip common header files that appear in many targets # They should use their own target-specific export header if filename in ('logger.h', 'settings.h', 'initialization.h'): # These files need special handling - look only in local CMakeLists.txt cmake_file = os.path.join(root, 'CMakeLists.txt') if os.path.exists(cmake_file): dir_mapping = parse_cmake_file(cmake_file, root_dir) # Try relative path first if rel_path in dir_mapping: target_name = dir_mapping[rel_path] elif filename in dir_mapping: target_name = dir_mapping[filename] else: # Not found in local CMakeLists.txt - skip skipped += 1 continue else: # No CMakeLists.txt in directory - skip skipped += 1 continue else: # For other files, use the standard lookup target_name = None # Try exact relative path match first (most precise) if rel_path in target_mapping: target_name = target_mapping[rel_path] # Try filename only (for unique files) elif filename in target_mapping: target_name = target_mapping[filename] # Try to find by parsing local CMakeLists.txt else: cmake_file = os.path.join(root, 'CMakeLists.txt') if os.path.exists(cmake_file): dir_mapping = parse_cmake_file(cmake_file, root_dir) # Try relative path in local mapping if rel_path in dir_mapping: target_name = dir_mapping[rel_path] # Try filename in local mapping elif filename in dir_mapping: target_name = dir_mapping[filename] if target_name: expected_header = get_expected_export_header(target_name) if old_include != expected_header: print(f"FIX: {rel_path}") print(f" Old: {old_include}") print(f" New: {expected_header}") fix_export_header(file_path, old_include, expected_header) fixed += 1 else: skipped += 1 else: # File not in mapping - might be in a SRCS list we didn't parse # or might be a header-only file skipped += 1 print() print("=" * 60) print("Summary:") print(f" Fixed: {fixed}") print(f" Skipped: {skipped}") print(f" Errors: {errors}") if __name__ == '__main__': main()