923 lines
38 KiB
Python
923 lines
38 KiB
Python
# build command
|
||
# pyinstaller --onefile scanVars.py --add-binary "F:\Work\Projects\TMS\TMS_new_bus\Src\DebugTools/build/libclang.dll;." --distpath . --workpath ./build --specpath ./build
|
||
# start script
|
||
# scanVars.exe F:\Work\Projects\TMS\TMS_new_bus\ F:\Work\Projects\TMS\TMS_new_bus\Debug\makefile
|
||
|
||
import os
|
||
import sys
|
||
import re
|
||
import clang.cindex
|
||
from clang import cindex
|
||
from clang.cindex import Config
|
||
import xml.etree.ElementTree as ET
|
||
from xml.dom import minidom
|
||
from parseMakefile import parse_makefile
|
||
from collections import deque
|
||
import argparse
|
||
import myXML
|
||
BITFIELD_WIDTHS = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}
|
||
|
||
def is_frozen():
|
||
# Для Nuitka --onefile
|
||
return getattr(sys, 'frozen', False)
|
||
|
||
def get_base_path():
|
||
if is_frozen():
|
||
# В Nuitka onefile распаковывается в папку с самим exe во временной директории
|
||
return os.path.dirname(sys.executable)
|
||
else:
|
||
# Режим разработки
|
||
return os.path.dirname(os.path.abspath(__file__))
|
||
|
||
def print_embedded_dlls():
|
||
# Папка временной распаковки для onefile приложений Nuitka/PyInstaller
|
||
base_path = get_base_path()
|
||
print(f"Scanning DLLs in temporary folder: {base_path}")
|
||
|
||
dlls = []
|
||
for root, dirs, files in os.walk(base_path):
|
||
for file in files:
|
||
if file.lower().endswith('.dll'):
|
||
full_path = os.path.join(root, file)
|
||
rel_path = os.path.relpath(full_path, base_path)
|
||
dlls.append(rel_path)
|
||
|
||
if dlls:
|
||
print("DLL files found:")
|
||
for d in dlls:
|
||
print(" -", d)
|
||
else:
|
||
print("No DLL files found in temporary folder.")
|
||
|
||
# Вызов при старте
|
||
print_embedded_dlls()
|
||
|
||
base_path = get_base_path()
|
||
print("Base path:", base_path)
|
||
|
||
# Указываем полный путь к libclang.dll внутри распакованного каталога
|
||
dll_path = os.path.join(base_path, "libclang.dll")
|
||
|
||
if os.path.exists(dll_path):
|
||
print("Loading libclang.dll from:", dll_path)
|
||
Config.set_library_file(dll_path)
|
||
else:
|
||
print("ERROR: libclang.dll not found at", dll_path)
|
||
|
||
|
||
index = cindex.Index.create()
|
||
PRINT_LEVEL = 2
|
||
|
||
PRINT_ERROR = 1 # 0 = ничего, 1 = ошибки, 2 = статус, 3 = отладка
|
||
PRINT_STATUS = 2 # 0 = ничего, 1 = ошибки, 2 = статус, 3 = отладка
|
||
PRINT_DEBUG = 3 # 0 = ничего, 1 = ошибки, 2 = статус, 3 = отладка
|
||
|
||
def optional_printf(level, msg):
|
||
"""
|
||
Выводит сообщение, если заданный уровень level меньше или равен текущему уровню DEBUG_LEVEL.
|
||
:param level: int — уровень важности сообщения # 0 = ничего, 1 = статус, 2 = подробно, 3 = отладка
|
||
:param msg: str — текст сообщения
|
||
"""
|
||
if level <= PRINT_LEVEL:
|
||
print(msg)
|
||
|
||
def get_canonical_typedef_file(var_type, include_dirs):
|
||
"""
|
||
Рекурсивно спускаемся к базовому типу, чтобы найти typedef-декларацию,
|
||
возвращаем файл заголовка, если он есть и в include_dirs.
|
||
"""
|
||
# unwrap array, pointer, typedef, etc, пока не дойдём до базового типа
|
||
t = var_type
|
||
while True:
|
||
# если массив, достаём тип элементов
|
||
if t.kind == clang.cindex.TypeKind.CONSTANTARRAY or t.kind == clang.cindex.TypeKind.INCOMPLETEARRAY:
|
||
t = t.element_type
|
||
continue
|
||
# если указатель — достаём тип, на который он указывает
|
||
if t.kind == clang.cindex.TypeKind.POINTER:
|
||
t = t.get_pointee()
|
||
continue
|
||
# если typedef — unwrap до underlying type
|
||
if t.get_declaration().kind == clang.cindex.CursorKind.TYPEDEF_DECL:
|
||
typedef_decl = t.get_declaration()
|
||
if typedef_decl and typedef_decl.location and typedef_decl.location.file:
|
||
typedef_header = str(typedef_decl.location.file)
|
||
# Проверяем, внутри ли include_dirs
|
||
path_abs = os.path.abspath(typedef_header)
|
||
for inc in include_dirs:
|
||
try:
|
||
inc_abs = os.path.abspath(inc)
|
||
if os.path.commonpath([path_abs, inc_abs]) == inc_abs and typedef_header.endswith('.h'):
|
||
return os.path.normpath(typedef_header)
|
||
except ValueError:
|
||
continue
|
||
# Если не нашли, пытаемся получить underlying type дальше
|
||
t = t.get_canonical() # underlying type без typedef-ов
|
||
continue
|
||
# Если дошли до типа без typedef-а — возвращаем None
|
||
break
|
||
return None
|
||
|
||
def analyze_variables_across_files(c_files, h_files, include_dirs):
|
||
optional_printf(PRINT_STATUS, "Starting analysis of variables across files...")
|
||
index = clang.cindex.Index.create()
|
||
args = [f"-I{inc}" for inc in include_dirs]
|
||
|
||
unique_vars = {} # имя переменной → словарь с инфой
|
||
h_files_needed = set()
|
||
vars_need_extern = {} # имя переменной → словарь без поля 'extern'
|
||
|
||
def is_inside_includes(path, include_dirs):
|
||
path_abs = os.path.abspath(path)
|
||
for inc in include_dirs:
|
||
try:
|
||
inc_abs = os.path.abspath(inc)
|
||
if os.path.commonpath([path_abs, inc_abs]) == inc_abs:
|
||
return True
|
||
except ValueError:
|
||
continue
|
||
return False
|
||
|
||
|
||
def parse_file(file_path):
|
||
optional_printf(PRINT_DEBUG, f"\tParsing file: {file_path}")
|
||
try:
|
||
tu = index.parse(file_path, args=args)
|
||
except Exception as e:
|
||
optional_printf(PRINT_ERROR, f"\t\tFailed to parse {file_path}: {e}")
|
||
return []
|
||
|
||
vars_in_file = []
|
||
|
||
def visit(node):
|
||
def is_system_var(var_name: str) -> bool:
|
||
# Проверяем, начинается ли имя с "_" и содержит заглавные буквы или служебные символы
|
||
return bool(re.match(r"^_[_A-Z]", var_name))
|
||
|
||
if node.kind == clang.cindex.CursorKind.VAR_DECL:
|
||
if node.semantic_parent.kind == clang.cindex.CursorKind.TRANSLATION_UNIT:
|
||
is_extern = (node.storage_class == clang.cindex.StorageClass.EXTERN)
|
||
is_static = (node.storage_class == clang.cindex.StorageClass.STATIC)
|
||
var_type = node.type.spelling
|
||
|
||
# Проверка, есть ли определение
|
||
definition = node.get_definition()
|
||
if is_extern and definition is None:
|
||
# Переменная extern без определения — игнорируем
|
||
return
|
||
|
||
if is_system_var(node.spelling):
|
||
return # игнорируем только явно известные служебные переменные
|
||
if node.spelling == 'HUGE': # еще одна служеюная, которую хз как выделять
|
||
return
|
||
|
||
# Проверяем, является ли тип указателем на функцию
|
||
# Признак: в типе есть '(' и ')' и '*', например: "void (*)(int)"
|
||
if "(" in var_type and "*" in var_type and ")" in var_type:
|
||
# Пропускаем указатели на функции
|
||
return
|
||
|
||
vars_in_file.append({
|
||
"name": node.spelling,
|
||
"type": var_type,
|
||
"extern": is_extern,
|
||
"static": is_static,
|
||
"file": file_path
|
||
})
|
||
|
||
# Если переменная extern и находится в .h — добавляем в includes
|
||
if is_extern and file_path.endswith('.h') and is_inside_includes(file_path):
|
||
h_files_needed.add(os.path.normpath(file_path))
|
||
|
||
# Добавляем файл с typedef, если есть
|
||
typedef_header = get_canonical_typedef_file(node.type, include_dirs)
|
||
if typedef_header:
|
||
h_files_needed.add(typedef_header)
|
||
|
||
|
||
|
||
for child in node.get_children():
|
||
visit(child)
|
||
|
||
visit(tu.cursor)
|
||
return vars_in_file
|
||
|
||
optional_printf(PRINT_STATUS, "Parsing header files (.h)...")
|
||
optional_printf(PRINT_STATUS, 'Progress: "Parsing variables from headers..."')
|
||
total_h = len(h_files)
|
||
for i, h in enumerate(h_files, 1):
|
||
vars_in_h = parse_file(h)
|
||
for v in vars_in_h:
|
||
name = v["name"]
|
||
if name not in unique_vars:
|
||
unique_vars[name] = {
|
||
"type": v["type"],
|
||
"extern": v["extern"],
|
||
"static": v["static"],
|
||
"file": v["file"]
|
||
}
|
||
optional_printf(PRINT_STATUS, f"Progress: {i}/{total_h}")
|
||
|
||
optional_printf(PRINT_STATUS, "Parsing source files (.c)...")
|
||
optional_printf(PRINT_STATUS, 'Progress: "Parsing variables from sources files..."')
|
||
total_c = len(c_files)
|
||
for i, c in enumerate(c_files, 1):
|
||
vars_in_c = parse_file(c)
|
||
for v in vars_in_c:
|
||
name = v["name"]
|
||
if name in unique_vars:
|
||
unique_vars[name].update({
|
||
"type": v["type"],
|
||
"extern": v["extern"],
|
||
"static": v["static"],
|
||
"file": v["file"]
|
||
})
|
||
else:
|
||
unique_vars[name] = {
|
||
"type": v["type"],
|
||
"extern": v["extern"],
|
||
"static": v["static"],
|
||
"file": v["file"]
|
||
}
|
||
optional_printf(PRINT_STATUS, f"Progress: {i}/{total_c}")
|
||
|
||
optional_printf(PRINT_STATUS, "Checking which variables need explicit extern declaration...")
|
||
optional_printf(PRINT_STATUS, 'Progress: "Checking extern declarations..."')
|
||
total_vars = len(unique_vars)
|
||
for i, (name, info) in enumerate(unique_vars.items(), 1):
|
||
if not info["extern"] and not info["static"] and info["file"].endswith('.c'):
|
||
extern_declared = False
|
||
for h in h_files_needed:
|
||
if h in unique_vars and unique_vars[h]["name"] == name and unique_vars[h]["extern"]:
|
||
extern_declared = True
|
||
break
|
||
if not extern_declared:
|
||
vars_need_extern[name] = {
|
||
"type": info["type"],
|
||
"file": info["file"]
|
||
}
|
||
optional_printf(PRINT_STATUS, f"Progress: {i}/{total_vars}")
|
||
|
||
optional_printf(PRINT_STATUS, "Analysis complete.")
|
||
optional_printf(PRINT_STATUS, f"\tTotal unique variables found: {len(unique_vars)}")
|
||
optional_printf(PRINT_STATUS, f"\tHeader files with extern variables and declarations: {len(h_files_needed)}")
|
||
optional_printf(PRINT_STATUS, f"\tVariables that need explicit extern declaration: {len(vars_need_extern)}\n")
|
||
|
||
return unique_vars, list(h_files_needed), vars_need_extern
|
||
|
||
|
||
def resolve_typedef(typedefs, typename):
|
||
"""
|
||
Рекурсивно раскрывает typedef, пока не дойдёт до "примитивного" типа.
|
||
Если typename нет в typedefs — возвращаем typename как есть.
|
||
"""
|
||
seen = set()
|
||
current = typename
|
||
while current in typedefs and current not in seen:
|
||
seen.add(current)
|
||
current = typedefs[current]
|
||
return current
|
||
|
||
|
||
def strip_ptr_and_array(typename):
|
||
"""
|
||
Убирает указатели и массивные скобки из типа,
|
||
чтобы найти базовый тип (например, для typedef или struct).
|
||
"""
|
||
if not isinstance(typename, str):
|
||
return typename
|
||
|
||
# Убираем [] и всё, что внутри скобок
|
||
typename = re.sub(r'\[.*?\]', '', typename)
|
||
|
||
# Убираем звёздочки и пробелы рядом
|
||
typename = typename.replace('*', '').strip()
|
||
|
||
return typename
|
||
|
||
def analyze_typedefs_and_struct(typedefs, structs):
|
||
optional_printf(PRINT_STATUS, "Resolving typedefs and expanding struct field types...")
|
||
|
||
def simplify_type_name(typename):
|
||
# Убираем ключевые слова типа "struct ", "union ", "enum " для поиска в typedefs и structs
|
||
if isinstance(typename, str):
|
||
for prefix in ("struct ", "union ", "enum "):
|
||
if typename.startswith(prefix):
|
||
return typename[len(prefix):]
|
||
return typename
|
||
|
||
def resolve_typedef_rec(typename, depth=0):
|
||
if depth > 50:
|
||
optional_printf(PRINT_ERROR, f"Possible typedef recursion limit reached on '{typename}'")
|
||
return typename
|
||
|
||
if not isinstance(typename, str):
|
||
return typename
|
||
|
||
simple_name = typename
|
||
|
||
if simple_name in typedefs:
|
||
underlying = typedefs[simple_name]
|
||
|
||
# Если раскрытие не меняет результат — считаем раскрытие завершённым
|
||
if normalize_type_name(underlying) == normalize_type_name(typename):
|
||
return underlying
|
||
|
||
return resolve_typedef_rec(underlying, depth + 1)
|
||
else:
|
||
return typename
|
||
|
||
|
||
def resolve_struct_fields(fields, depth=0):
|
||
if depth > 50:
|
||
optional_printf(PRINT_ERROR, f"Possible struct recursion limit reached")
|
||
return fields
|
||
|
||
if not isinstance(fields, dict):
|
||
return fields
|
||
|
||
resolved_fields = {}
|
||
|
||
for fname, ftype in fields.items():
|
||
base_type = strip_ptr_and_array(ftype)
|
||
original_type = ftype # Сохраняем оригинальный вид типа
|
||
|
||
if base_type in structs:
|
||
# Рекурсивно раскрываем вложенную структуру
|
||
nested = resolve_struct_fields(structs[base_type], depth + 1)
|
||
nested["__type__"] = original_type
|
||
resolved_fields[fname] = nested
|
||
else:
|
||
resolved_fields[fname] = original_type # сохраняем оригинал
|
||
|
||
return resolved_fields
|
||
|
||
|
||
""" # Сначала раскрываем typedef в именах структур и в полях
|
||
substituted_structs = {}
|
||
for sname, fields in structs.items():
|
||
resolved_sname = resolve_typedef_rec(sname) # раскрываем имя структуры
|
||
substituted_fields = {}
|
||
for fname, ftype in fields.items():
|
||
resolved_type = resolve_typedef_rec(ftype) # раскрываем тип поля
|
||
substituted_fields[fname] = resolved_type
|
||
substituted_structs[resolved_sname] = substituted_fields """
|
||
|
||
# Раскрываем typedef'ы
|
||
optional_printf(PRINT_STATUS, 'Progress: "Resolving typedefs..."')
|
||
total_typedefs = len(typedefs)
|
||
resolved_typedefs = {}
|
||
for i, tname in enumerate(typedefs, 1):
|
||
resolved = resolve_typedef_rec(tname)
|
||
resolved_typedefs[tname] = resolved
|
||
optional_printf(4, f"\tTypedef {tname} resolved")
|
||
optional_printf(PRINT_STATUS, f"Progress: {i}/{total_typedefs}")
|
||
|
||
# Теперь раскрываем вложенные структуры
|
||
optional_printf(PRINT_STATUS, 'Progress: "Resolving structs..."')
|
||
total_structs = len(structs)
|
||
resolved_structs = {}
|
||
for i, (sname, fields) in enumerate(structs.items(), 1):
|
||
if "(unnamed" in sname:
|
||
optional_printf(4, f" Skipping anonymous struct/union: {sname}")
|
||
continue
|
||
if sname == 'T_project':
|
||
a = 1
|
||
resolved_fields = resolve_struct_fields(fields)
|
||
resolved_structs[sname] = resolved_fields
|
||
optional_printf(PRINT_DEBUG, f"\tStruct {sname} resolved")
|
||
optional_printf(PRINT_STATUS, f"Progress: {i}/{total_structs}")
|
||
|
||
return resolved_typedefs, resolved_structs
|
||
|
||
def normalize_type_name(type_name: str) -> str:
|
||
# Приводим тип к виду "union (unnamed union at ...)" или "struct (unnamed struct at ...)"
|
||
m = re.match(r'^(union|struct) \((unnamed)( union| struct)? at .+\)$', type_name)
|
||
if m:
|
||
kind = m.group(1)
|
||
unnamed = m.group(2)
|
||
extra = m.group(3)
|
||
if extra is None:
|
||
type_name = f"{kind} ({unnamed} {kind} at {type_name.split(' at ')[1]}"
|
||
return type_name
|
||
|
||
def contains_anywhere_in_node(node, target: str) -> bool:
|
||
"""
|
||
Рекурсивно ищет target во всех строковых значениях текущего узла и его потомков.
|
||
"""
|
||
for attr in dir(node):
|
||
try:
|
||
val = getattr(node, attr)
|
||
if isinstance(val, str) and target in val:
|
||
return True
|
||
elif hasattr(val, 'spelling') and target in val.spelling:
|
||
return True
|
||
except Exception:
|
||
continue
|
||
|
||
for child in node.get_children():
|
||
if contains_anywhere_in_node(child, target):
|
||
return True
|
||
|
||
return False
|
||
|
||
|
||
def analyze_typedefs_and_structs_across_files(c_files, include_dirs):
|
||
optional_printf(PRINT_STATUS, "Starting analysis of typedefs and structs across files...")
|
||
index = clang.cindex.Index.create()
|
||
args = [f"-I{inc}" for inc in include_dirs]
|
||
|
||
unique_typedefs_raw = {}
|
||
unique_structs_raw = {}
|
||
|
||
def parse_file(file_path):
|
||
optional_printf(PRINT_DEBUG, f"\tParsing file: {file_path}")
|
||
try:
|
||
tu = index.parse(file_path, args=args)
|
||
except Exception as e:
|
||
optional_printf(PRINT_ERROR, f"\t\tFailed to parse {file_path}: {e}")
|
||
return {}, {}
|
||
|
||
typedefs = {}
|
||
structs = {}
|
||
|
||
def visit(node):
|
||
if node.kind == clang.cindex.CursorKind.TYPEDEF_DECL:
|
||
name = node.spelling
|
||
underlying = node.underlying_typedef_type.spelling
|
||
typedefs[name] = underlying
|
||
|
||
elif node.kind in (clang.cindex.CursorKind.STRUCT_DECL, clang.cindex.CursorKind.UNION_DECL):
|
||
prefix = "struct " if node.kind == clang.cindex.CursorKind.STRUCT_DECL else "union "
|
||
|
||
raw_name = node.spelling
|
||
normalized_name = normalize_type_name(raw_name)
|
||
|
||
# struct_name всегда с префиксом
|
||
if node.spelling and "unnamed" not in normalized_name:
|
||
struct_name = f"{prefix}{normalized_name}"
|
||
else:
|
||
struct_name = normalized_name
|
||
|
||
# Поиск typedef, соответствующего этой структуре
|
||
typedef_name_for_struct = None
|
||
for tname, underlying in typedefs.items():
|
||
if normalize_type_name(underlying) == struct_name:
|
||
typedef_name_for_struct = tname
|
||
break
|
||
|
||
# Если нашли typedef → заменим struct_name на него
|
||
final_name = typedef_name_for_struct if typedef_name_for_struct else struct_name
|
||
|
||
fields = {}
|
||
for c in node.get_children():
|
||
if c.kind == clang.cindex.CursorKind.FIELD_DECL:
|
||
ftype = c.type.spelling
|
||
bit_width = c.get_bitfield_width()
|
||
if bit_width > 0:
|
||
ftype += f" (bitfield:{bit_width})"
|
||
fields[c.spelling] = ftype
|
||
|
||
if fields:
|
||
structs[final_name] = fields
|
||
|
||
# Если это был struct с typedef, удалим старое имя (например, struct TS_project)
|
||
if typedef_name_for_struct and struct_name in structs:
|
||
del structs[struct_name]
|
||
|
||
for child in node.get_children():
|
||
visit(child)
|
||
|
||
|
||
|
||
|
||
visit(tu.cursor)
|
||
return typedefs, structs
|
||
|
||
optional_printf(PRINT_STATUS, 'Progress: "Resolving structs and typedefs..."')
|
||
total_files = len(c_files)
|
||
for i, c_file in enumerate(c_files, 1):
|
||
typedefs_in_file, structs_in_file = parse_file(c_file)
|
||
for name, underlying in typedefs_in_file.items():
|
||
if name not in unique_typedefs_raw:
|
||
unique_typedefs_raw[name] = underlying
|
||
for sname, fields in structs_in_file.items():
|
||
if sname not in unique_structs_raw:
|
||
unique_structs_raw[sname] = fields
|
||
optional_printf(PRINT_STATUS, f"Progress: {i}/{total_files}")
|
||
|
||
# Теперь раскроем typedef и структуры, учитывая вложения
|
||
resolved_typedefs, resolved_structs = analyze_typedefs_and_struct(unique_typedefs_raw, unique_structs_raw)
|
||
|
||
optional_printf(PRINT_STATUS, "Analysis complete.")
|
||
optional_printf(PRINT_STATUS, f"\tTotal unique typedefs found: {len(resolved_typedefs)}")
|
||
optional_printf(PRINT_STATUS, f"\tTotal unique structs found: {len(resolved_structs)}\n")
|
||
|
||
return resolved_typedefs, resolved_structs
|
||
|
||
|
||
def read_vars_from_xml(xml_path):
|
||
xml_full_path = os.path.normpath(xml_path)
|
||
vars_data = {}
|
||
|
||
if not os.path.exists(xml_full_path):
|
||
return vars_data # пусто, если файла нет
|
||
|
||
root, tree = myXML.safe_parse_xml(xml_full_path)
|
||
if root is None:
|
||
return vars_data
|
||
|
||
vars_elem = root.find('variables')
|
||
if vars_elem is None:
|
||
return vars_data
|
||
|
||
for var_elem in vars_elem.findall('var'):
|
||
name = var_elem.get('name')
|
||
if not name:
|
||
continue
|
||
|
||
def get_bool(tag, default='false'):
|
||
return var_elem.findtext(tag, default).lower() == 'true'
|
||
|
||
vars_data[name] = {
|
||
'show_var': get_bool('show_var'),
|
||
'enable': get_bool('enable'),
|
||
'shortname': var_elem.findtext('shortname', name),
|
||
'pt_type': var_elem.findtext('pt_type', ''),
|
||
'iq_type': var_elem.findtext('iq_type', ''),
|
||
'return_type': var_elem.findtext('return_type', 'pt_iq_none'),
|
||
'type': var_elem.findtext('type', 'unknown'),
|
||
'file': var_elem.findtext('file', ''),
|
||
'extern': get_bool('extern'),
|
||
'static': get_bool('static'),
|
||
}
|
||
|
||
return vars_data
|
||
|
||
def make_relative_if_possible(path, base):
|
||
if not path:
|
||
return ''
|
||
if not os.path.isabs(path):
|
||
return path # уже относительный
|
||
try:
|
||
rel = os.path.relpath(path, base)
|
||
return rel
|
||
except ValueError as e:
|
||
print(f"[WARNING] relpath error between '{path}' and '{base}': {e}")
|
||
return path # оставляем абсолютным
|
||
|
||
def generate_xml_output(proj_path, xml_path, unique_vars, h_files_needed, vars_need_extern, structs_xml_path=None, makefile_path=None):
|
||
xml_full_path = os.path.normpath(xml_path)
|
||
|
||
# Проверяем, существует ли файл, только тогда читаем из него
|
||
existing_vars_data = {}
|
||
if os.path.isfile(xml_full_path):
|
||
existing_vars_data = read_vars_from_xml(xml_full_path)
|
||
|
||
# --- Новый блок: формируем атрибуты корневого тега с относительными путями ---
|
||
proj_path = os.path.abspath(proj_path)
|
||
analysis_attrs = {
|
||
"proj_path": proj_path.replace("\\", "/")
|
||
}
|
||
|
||
if makefile_path:
|
||
rel_makefile = myXML.make_relative_path(makefile_path, proj_path)
|
||
if not os.path.isabs(rel_makefile):
|
||
analysis_attrs["makefile_path"] = rel_makefile.replace("\\", "/")
|
||
|
||
if structs_xml_path:
|
||
rel_struct = myXML.make_relative_path(structs_xml_path, proj_path)
|
||
if not os.path.isabs(rel_struct):
|
||
analysis_attrs["structs_path"] = rel_struct.replace("\\", "/")
|
||
|
||
|
||
root = ET.Element("analysis", attrib=analysis_attrs)
|
||
|
||
vars_elem = ET.SubElement(root, "variables")
|
||
|
||
# Объединяем старые и новые переменные
|
||
combined_vars = {}
|
||
if existing_vars_data:
|
||
combined_vars.update(existing_vars_data)
|
||
|
||
for name, info in unique_vars.items():
|
||
if name not in combined_vars:
|
||
combined_vars[name] = {
|
||
'show_var': info.get('enable', False),
|
||
'enable': info.get('enable', False),
|
||
'shortname': info.get('shortname', name),
|
||
'pt_type': info.get('pt_type', ''),
|
||
'iq_type': info.get('iq_type', ''),
|
||
'return_type': info.get('return_type', 'pt_iq_none'),
|
||
'type': info.get('type', 'unknown'),
|
||
'file': info.get('file', ''),
|
||
'extern': info.get('extern', False),
|
||
'static': info.get('static', False),
|
||
}
|
||
else:
|
||
# При необходимости можно обновить поля, например:
|
||
# combined_vars[name].update(info)
|
||
pass
|
||
|
||
# Записываем переменные с новыми полями
|
||
for name, info in combined_vars.items():
|
||
var_elem = ET.SubElement(vars_elem, "var", name=name)
|
||
ET.SubElement(var_elem, "show_var").text = str(info.get('show_var', False)).lower()
|
||
ET.SubElement(var_elem, "enable").text = str(info.get('enable', False)).lower()
|
||
ET.SubElement(var_elem, "shortname").text = info.get('shortname', name)
|
||
ET.SubElement(var_elem, "pt_type").text = info.get('pt_type', '')
|
||
ET.SubElement(var_elem, "iq_type").text = info.get('iq_type', '')
|
||
ET.SubElement(var_elem, "return_type").text = info.get('return_type', 'pt_iq_none')
|
||
|
||
ET.SubElement(var_elem, "type").text = info.get('type', 'unknown')
|
||
rel_file = make_relative_if_possible(info.get('file', ''), proj_path).replace("\\", "/")
|
||
ET.SubElement(var_elem, "file").text = rel_file.replace("\\", "/") if rel_file else ''
|
||
ET.SubElement(var_elem, "extern").text = str(info.get('extern', False)).lower()
|
||
ET.SubElement(var_elem, "static").text = str(info.get('static', False)).lower()
|
||
|
||
# Секция includes (файлы)
|
||
includes_elem = ET.SubElement(root, "includes")
|
||
for path in h_files_needed:
|
||
rel_path = os.path.relpath(path, proj_path)
|
||
includes_elem_file = ET.SubElement(includes_elem, "file")
|
||
includes_elem_file.text = rel_path.replace("\\", "/")
|
||
|
||
# Секция externs (переменные с extern)
|
||
externs_elem = ET.SubElement(root, "externs")
|
||
for name, info in vars_need_extern.items():
|
||
var_elem = ET.SubElement(externs_elem, "var", name=name)
|
||
ET.SubElement(var_elem, "type").text = info.get("type", "unknown")
|
||
rel_file = os.path.relpath(info.get("file", ""), proj_path)
|
||
ET.SubElement(var_elem, "file").text = rel_file.replace("\\", "/")
|
||
|
||
# Форматирование с отступами
|
||
myXML.fwrite(root, xml_full_path)
|
||
|
||
optional_printf(PRINT_STATUS, f"[XML] Variables saved to {xml_full_path}")
|
||
|
||
|
||
def write_typedefs_and_structs_to_xml(proj_path, xml_path, typedefs, structs):
|
||
def create_struct_element(parent_elem, struct_name, fields):
|
||
struct_elem = ET.SubElement(parent_elem, "struct", name=struct_name)
|
||
|
||
for field_name, field_type in fields.items():
|
||
if isinstance(field_type, dict):
|
||
# Вложенная структура
|
||
nested_elem = ET.SubElement(struct_elem, "field", name=field_name)
|
||
# Сохраняем оригинальный тип (например: T_innerStruct[4], T_innerStruct*)
|
||
if "__type__" in field_type:
|
||
nested_elem.set("type", field_type["__type__"])
|
||
else:
|
||
nested_elem.set("type", "anonymous")
|
||
|
||
# Рекурсивно добавляем поля вложенной структуры
|
||
create_struct_element(nested_elem, field_name, {
|
||
k: v for k, v in field_type.items() if k != "__type__"
|
||
})
|
||
else:
|
||
# Примитивное поле
|
||
ET.SubElement(struct_elem, "field", name=field_name, type=field_type)
|
||
|
||
# Полный путь к xml файлу
|
||
xml_full_path = os.path.normpath(xml_path)
|
||
root = ET.Element("analysis")
|
||
|
||
# <structs>
|
||
structs_elem = ET.SubElement(root, "structs")
|
||
for struct_name, fields in sorted(structs.items()):
|
||
create_struct_element(structs_elem, struct_name, fields)
|
||
|
||
# <typedefs>
|
||
typedefs_elem = ET.SubElement(root, "typedefs")
|
||
for name, underlying in sorted(typedefs.items()):
|
||
ET.SubElement(typedefs_elem, "typedef", name=name, type=underlying)
|
||
|
||
# Преобразуем в красиво отформатированную XML-строку
|
||
myXML.fwrite(root, xml_full_path)
|
||
|
||
print(f"[XML] Typedefs and structs saved to: {xml_full_path}")
|
||
|
||
def topo_sort(graph):
|
||
indegree = {}
|
||
for node in graph:
|
||
indegree.setdefault(node, 0)
|
||
for neigh in graph[node]:
|
||
indegree[neigh] = indegree.get(neigh, 0) + 1
|
||
|
||
queue = deque([node for node in indegree if indegree[node] == 0])
|
||
sorted_list = []
|
||
|
||
while queue:
|
||
node = queue.popleft()
|
||
sorted_list.append(node)
|
||
for neigh in graph.get(node, []):
|
||
indegree[neigh] -= 1
|
||
if indegree[neigh] == 0:
|
||
queue.append(neigh)
|
||
|
||
if len(sorted_list) != len(indegree):
|
||
print("Warning: include graph has cycles or disconnected components.")
|
||
return sorted_list
|
||
|
||
def get_sorted_headers(c_files, h_files, include_dirs):
|
||
index = clang.cindex.Index.create()
|
||
args = [f"-I{inc}" for inc in include_dirs]
|
||
|
||
# Собираем граф зависимостей для заголовочных файлов
|
||
include_graph = {}
|
||
|
||
# Проходим по всем исходникам и заголовкам, чтобы получить полный граф
|
||
all_files_to_parse = set(c_files) | set(h_files)
|
||
|
||
for f in all_files_to_parse:
|
||
try:
|
||
tu = index.parse(f, args=args)
|
||
except Exception as e:
|
||
print(f"Failed to parse {f}: {e}")
|
||
continue
|
||
for include in tu.get_includes():
|
||
inc_file = str(include.include)
|
||
src_file = str(include.source)
|
||
if not inc_file or not src_file:
|
||
continue
|
||
# Фокусируемся только на заголовочных файлах из списка h_files
|
||
if src_file not in include_graph:
|
||
include_graph[src_file] = set()
|
||
include_graph[src_file].add(inc_file)
|
||
|
||
# Оставляем только заголовочные файлы из h_files, чтобы получить их зависимости
|
||
h_files_set = set(h_files)
|
||
filtered_graph = {}
|
||
for src, incs in include_graph.items():
|
||
if src in h_files_set:
|
||
# Оставляем зависимости, которые тоже из h_files
|
||
filtered_graph[src] = set(filter(lambda x: x in h_files_set, incs))
|
||
|
||
# Теперь топологическая сортировка заголовков
|
||
sorted_h_files = topo_sort(filtered_graph)
|
||
|
||
# В случае если какие-то h_files не попали в граф (нет зависимостей) — добавим их в конец
|
||
missing_headers = h_files_set - set(sorted_h_files)
|
||
sorted_h_files.extend(sorted(missing_headers))
|
||
|
||
return sorted_h_files
|
||
|
||
|
||
def build_include_graph(tu):
|
||
# Возвращает dict: ключ — файл, значение — set файлов, которые он включает
|
||
graph = {}
|
||
for include in tu.get_includes():
|
||
included_file = str(include.include)
|
||
including_file = str(include.source)
|
||
if including_file not in graph:
|
||
graph[including_file] = set()
|
||
graph[including_file].add(included_file)
|
||
return graph
|
||
|
||
def topo_sort(graph):
|
||
from collections import deque
|
||
|
||
indegree = {}
|
||
for node in graph:
|
||
indegree.setdefault(node, 0)
|
||
for neigh in graph[node]:
|
||
indegree[neigh] = indegree.get(neigh, 0) + 1
|
||
|
||
queue = deque([node for node in indegree if indegree[node] == 0])
|
||
sorted_list = []
|
||
|
||
while queue:
|
||
node = queue.popleft()
|
||
sorted_list.append(node)
|
||
for neigh in graph.get(node, []):
|
||
indegree[neigh] -= 1
|
||
if indegree[neigh] == 0:
|
||
queue.append(neigh)
|
||
|
||
if len(sorted_list) != len(indegree):
|
||
# Цикл или недостающие файлы
|
||
print("Warning: include graph has cycles or disconnected components.")
|
||
return sorted_list
|
||
|
||
|
||
|
||
def main():
|
||
sys.stdout.reconfigure(line_buffering=True)
|
||
global PRINT_LEVEL
|
||
|
||
parser = argparse.ArgumentParser(
|
||
description="Analyze C project variables, typedefs, and structs using Clang.",
|
||
epilog="""\
|
||
Usage example:
|
||
%(prog)s /path/to/project /path/to/Makefile /absolute/path/to/output_vars.xml
|
||
""",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
add_help=False
|
||
)
|
||
parser.add_argument("proj_path", help="Absolute path to the project root directory")
|
||
parser.add_argument("makefile_path", help="Absolute path to the makefile to parse")
|
||
parser.add_argument("output_xml", help="Absolute path to output XML file for variables")
|
||
|
||
parser.add_argument("-h", "--help", action="store_true", help="Show this help message and exit")
|
||
parser.add_argument("-v", "--verbose", type=int, choices=range(0,6), default=2,
|
||
help="Set verbosity level from 0 (quiet) to 5 (most detailed), default=2")
|
||
|
||
if "-h" in sys.argv or "--help" in sys.argv:
|
||
parser.print_help()
|
||
print("\nUsage example:")
|
||
print(f" {os.path.basename(sys.argv[0])} /path/to/project /path/to/Makefile /absolute/path/to/output_vars.xml")
|
||
sys.exit(0)
|
||
|
||
if len(sys.argv) < 4:
|
||
print("Error: insufficient arguments.\n")
|
||
print("Usage example:")
|
||
print(f" {os.path.basename(sys.argv[0])} /path/to/project /path/to/Makefile /absolute/path/to/output_vars.xml")
|
||
sys.exit(1)
|
||
|
||
args = parser.parse_args()
|
||
|
||
PRINT_LEVEL = args.verbose
|
||
|
||
proj_path = os.path.normpath(args.proj_path)
|
||
makefile_path = os.path.normpath(args.makefile_path)
|
||
output_xml = os.path.normpath(args.output_xml)
|
||
|
||
# Проверка абсолютности путей
|
||
for path, name in [(proj_path, "Project path"), (makefile_path, "Makefile path"), (output_xml, "Output XML path")]:
|
||
if not os.path.isabs(path):
|
||
print(f"Error: {name} '{path}' is not an absolute path.")
|
||
sys.exit(1)
|
||
|
||
if not os.path.isdir(proj_path):
|
||
print(f"Error: Project path '{proj_path}' is not a directory or does not exist.")
|
||
sys.exit(1)
|
||
if not os.path.isfile(makefile_path):
|
||
print(f"Error: Makefile path '{makefile_path}' does not exist.")
|
||
sys.exit(1)
|
||
|
||
c_files, h_files, include_dirs = parse_makefile(makefile_path, proj_path)
|
||
|
||
vars, includes, externs = analyze_variables_across_files(c_files, h_files, include_dirs)
|
||
typedefs, structs = analyze_typedefs_and_structs_across_files(c_files, include_dirs)
|
||
|
||
vars = dict(sorted(vars.items()))
|
||
includes = get_sorted_headers(c_files, includes, include_dirs)
|
||
externs = dict(sorted(externs.items()))
|
||
typedefs = dict(sorted(typedefs.items()))
|
||
structs = dict(sorted(structs.items()))
|
||
# Определяем путь к файлу с структурами рядом с output_xml
|
||
structs_xml = os.path.join(os.path.dirname(output_xml), "structs.xml")
|
||
|
||
# Записываем структуры в structs_xml
|
||
write_typedefs_and_structs_to_xml(proj_path, structs_xml, typedefs, structs)
|
||
|
||
# Передаем путь к structs.xml относительно proj_path в vars.xml
|
||
# Модифицируем generate_xml_output так, чтобы принимать и путь к structs.xml (относительный)
|
||
generate_xml_output(proj_path, output_xml, vars, includes, externs, structs_xml, makefile_path)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|
||
|
||
def run_scan(proj_path, makefile_path, output_xml, verbose=2):
|
||
global PRINT_LEVEL
|
||
PRINT_LEVEL = verbose
|
||
|
||
proj_path = os.path.normpath(proj_path)
|
||
makefile_path = os.path.normpath(makefile_path)
|
||
output_xml = os.path.normpath(output_xml)
|
||
|
||
# Проверка абсолютности путей
|
||
for path, name in [(proj_path, "Project path"), (makefile_path, "Makefile path"), (output_xml, "Output XML path")]:
|
||
if not os.path.isabs(path):
|
||
raise ValueError(f"{name} '{path}' is not an absolute path.")
|
||
|
||
if not os.path.isdir(proj_path):
|
||
raise FileNotFoundError(f"Project path '{proj_path}' is not a directory or does not exist.")
|
||
if not os.path.isfile(makefile_path):
|
||
raise FileNotFoundError(f"Makefile path '{makefile_path}' does not exist.")
|
||
|
||
c_files, h_files, include_dirs = parse_makefile(makefile_path, proj_path)
|
||
|
||
vars, includes, externs = analyze_variables_across_files(c_files, h_files, include_dirs)
|
||
typedefs, structs = analyze_typedefs_and_structs_across_files(c_files, include_dirs)
|
||
|
||
vars = dict(sorted(vars.items()))
|
||
includes = get_sorted_headers(c_files, includes, include_dirs)
|
||
externs = dict(sorted(externs.items()))
|
||
typedefs = dict(sorted(typedefs.items()))
|
||
structs = dict(sorted(structs.items()))
|
||
|
||
print('Progress: "Writting XML..."')
|
||
print("Progress: 0/2")
|
||
print("[XML] Creating structs.xml...")
|
||
structs_xml = os.path.join(os.path.dirname(output_xml), "structs.xml")
|
||
write_typedefs_and_structs_to_xml(proj_path, structs_xml, typedefs, structs)
|
||
print("Progress: 1/2")
|
||
|
||
print("[XML] Creating vars.xml...")
|
||
generate_xml_output(proj_path, output_xml, vars, includes, externs, structs_xml, makefile_path)
|
||
print('Progress: "Done"')
|
||
print("Progress: 2/2")
|