Files
qt6-migration-pipeline/fix_export_headers_v4.py
T
torstenandCopilot 342ffd3e5c Update submodule to latest BUILD_RTI fixes
- HandlerMinelayerData: BUILD_RTI Guards für RTI-Abhängigkeiten
- svcTabCommon: Export-Header-Fix via Fallback-Target
- TrainingAreaBase: Stub-Header für BUILD_RTI=OFF
- ddsfts: Include-Pfad und Dependency-Fixes
- BUILD_RTI=OFF Compilation vollständig unterstützt

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: 01M0JNZA586SACX1PRZ2DJAE5S
2026-08-21 19:30:20 +02:00

296 lines
11 KiB
Python

#!/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 <base_dir>")
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()