#!/usr/bin/env python3 """ Repariert Export-Header-Includes in Qt6 migrierten Dateien. Nur für SHARED/MODULE Libraries - STATIC Libraries brauchen keine Export-Header. """ import os import re import sys from pathlib import Path from collections import defaultdict def parse_cmake_file(cmake_path, root_dir): """ Parse CMakeLists.txt und extrahiere Target-Informationen. Returns: targets: dict mapping target_name -> {'type': 'SHARED'|'STATIC'|'MODULE', 'sources': [files]} src_to_target: dict mapping source_file (relative path) -> target_name """ targets = {} src_to_target = {} if not os.path.exists(cmake_path): return targets, src_to_target with open(cmake_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() # Remove comments content = re.sub(r'#[^\n]*', '', content) # Parse SET() variables first set_vars = {} for match in re.finditer(r'SET\s*\(\s*(\w+)\s+([^)]+)\)', content, re.IGNORECASE): var_name = match.group(1) var_value = match.group(2).strip() # Split on whitespace files = [f.strip() for f in var_value.split() if f.strip()] set_vars[var_name] = files # Parse NGL_ADD_LIBRARY and NGL_ADD_LIBRARY2 # Pattern: NGL_ADD_LIBRARY(... target_name SHARED|STATIC|MODULE ...) # or: NGL_ADD_LIBRARY2(target_name SHARED|STATIC|MODULE ...) for pattern in [r'NGL_ADD_LIBRARY\s*\(([^)]+)\)', r'NGL_ADD_LIBRARY2\s*\(([^)]+)\)']: for match in re.finditer(pattern, content, re.IGNORECASE | re.DOTALL): args_str = match.group(1) args = args_str.split() if len(args) < 3: continue # Skip flags like ALL, WS, AAR, SV, NO_TRANSLATION idx = 0 while idx < len(args) and args[idx].upper() in ['ALL', 'WS', 'AAR', 'SV', 'NO_TRANSLATION']: idx += 1 if idx >= len(args): continue target_name = args[idx].lower() # Target names are lowercase in export headers idx += 1 if idx >= len(args): continue lib_type = args[idx].upper() if lib_type not in ['SHARED', 'STATIC', 'MODULE']: continue idx += 1 # Rest are source files (may include variable references like ${VAR}) sources = [] for arg in args[idx:]: if arg.startswith('${'): # Variable reference - expand it var_name = arg[2:-1] # Remove ${ and } if var_name in set_vars: sources.extend(set_vars[var_name]) else: sources.append(arg) targets[target_name] = { 'type': lib_type, 'sources': [s for s in sources if not s.endswith('.h') or 'export' in s.lower()] } # Map source files to target (only .cpp/.h files, not export headers) cmake_dir = os.path.dirname(cmake_path) for src in sources: if src.endswith('_export.h'): continue # Skip export headers if os.path.splitext(src)[1] in ['.cpp', '.h', '.cc', '.cxx', '.hpp']: # Store relative path from root_dir abs_path = os.path.normpath(os.path.join(cmake_dir, src)) rel_path = os.path.relpath(abs_path, root_dir) src_to_target[rel_path] = target_name return targets, src_to_target def find_export_header_includes(content): """Find export header includes in file content.""" # Match both "..." and <...> styles pattern = r'#include\s*[<"]([^>"\s]+_export\.h)[>"]' return re.findall(pattern, content) def get_expected_export_header(target_name): """Get expected export header name for a target.""" return f"{target_name}_export.h" def fix_file(file_path, target_name, all_targets): """ Fix export header include in a file. Returns: (old_include, new_include, reason) or None if no fix needed """ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() current_includes = find_export_header_includes(content) if not current_includes: return None # No export header include found # Check if target is SHARED or MODULE (STATIC doesn't need export headers) if target_name not in all_targets: return None # Target not found target_info = all_targets[target_name] if target_info['type'] == 'STATIC': # STATIC libraries don't need export headers - remove the include return (current_includes[0], None, f"STATIC library '{target_name}' doesn't need export header") expected_header = get_expected_export_header(target_name) if len(current_includes) == 1 and current_includes[0] == expected_header: return None # Already correct # Fix needed old_include = current_includes[0] return (old_include, expected_header, f"Target '{target_name}' ({target_info['type']})") def main(): if len(sys.argv) < 2: print("Usage: fix_export_headers_v4.py ") sys.exit(1) base_dir = sys.argv[1] if not os.path.isdir(base_dir): print(f"Error: {base_dir} is not a directory") sys.exit(1) # Find all CMakeLists.txt files and parse them all_targets = {} all_src_to_target = {} for root, dirs, files in os.walk(base_dir): if 'CMakeLists.txt' in files: cmake_path = os.path.join(root, 'CMakeLists.txt') targets, src_to_target = parse_cmake_file(cmake_path, base_dir) all_targets.update(targets) all_src_to_target.update(src_to_target) print(f"Found {len(all_targets)} targets") shared_module_targets = [t for t, info in all_targets.items() if info['type'] in ['SHARED', 'MODULE']] print(f"SHARED/MODULE targets: {len(shared_module_targets)}") # Find all .h files with export header includes files_to_check = [] for root, dirs, files in os.walk(base_dir): for file in files: if file.endswith('.h'): files_to_check.append(os.path.join(root, file)) print(f"Checking {len(files_to_check)} header files...") fixed = [] skipped = [] errors = [] for file_path in files_to_check: rel_path = os.path.relpath(file_path, base_dir) # Check if file has export header includes with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() current_includes = find_export_header_includes(content) if not current_includes: skipped.append((file_path, "No export header include")) continue # Try to find target for this file target_name = None # Priority 1: Exact relative path match if rel_path in all_src_to_target: target_name = all_src_to_target[rel_path] # Priority 2: Filename match (for headers in same dir as CMakeLists.txt) if not target_name: filename = os.path.basename(file_path) for src_path, tgt in all_src_to_target.items(): if os.path.basename(src_path) == filename: target_name = tgt break # Priority 3: Parse local CMakeLists.txt if not target_name: cmake_path = os.path.join(os.path.dirname(file_path), 'CMakeLists.txt') if os.path.exists(cmake_path): local_targets, local_mapping = parse_cmake_file(cmake_path, base_dir) # Check if file is in any local target for tgt_name, tgt_info in local_targets.items(): for src in tgt_info['sources']: if src == os.path.basename(file_path): target_name = tgt_name break if not target_name: errors.append((file_path, "Could not determine target")) continue # Fix the file result = fix_file(file_path, target_name, all_targets) if result is None: skipped.append((file_path, "Already correct or STATIC")) continue old_include, new_include, reason = result if new_include is None: # Remove include for STATIC libraries # Find the line with the include and remove it lines = content.split('\n') new_lines = [] for line in lines: if f'#include' in line and old_include in line: continue # Skip this line new_lines.append(line) new_content = '\n'.join(new_lines) with open(file_path, 'w', encoding='utf-8') as f: f.write(new_content) fixed.append((file_path, old_include, "REMOVED", reason)) else: # Replace include # Use regex to handle both "..." and <...> pattern = rf'#include\s*[<"]{re.escape(old_include)}[>"]' replacement = f'#include "{new_include}"' new_content = re.sub(pattern, replacement, content) with open(file_path, 'w', encoding='utf-8') as f: f.write(new_content) fixed.append((file_path, old_include, new_include, reason)) # Print summary print("\n" + "="*80) print(f"Summary:") print(f" Fixed: {len(fixed)}") print(f" Skipped: {len(skipped)}") print(f" Errors: {len(errors)}") print("="*80) if fixed: print("\nFixed files:") for item in fixed[:20]: # Show first 20 if len(item) == 4: file_path, old, new, reason = item print(f" FIX: {os.path.relpath(file_path, base_dir)}") print(f" {old} -> {new}") print(f" ({reason})") if len(fixed) > 20: print(f" ... and {len(fixed) - 20} more") if errors: print("\nErrors:") for file_path, reason in errors[:20]: print(f" ERROR: {os.path.relpath(file_path, base_dir)} - {reason}") if len(errors) > 20: print(f" ... and {len(errors) - 20} more") if __name__ == '__main__': main()