debugVarTool/Src/scan_vars.py
Razvalyaev ae2c90160e +библиотека .c/.h переписана под универсальные дефайны intX_t uintX_t
+сделано задание размера короткого имени
+ добавлена бета поддержка stm:+
   - парс переменных из файла преокта Keil или makefile CubeIDE
   - запись в utf-8 для STM, вместо cp1251 для TMS
   - другой размер int (32 бита, вместо 16 бит) для STM
2025-07-17 09:18:03 +03:00

948 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# build command
# pyinstaller --onefile scan_vars.py --add-binary "F:\Work\Projects\TMS\TMS_new_bus\Src\DebugTools/build/libclang.dll;." --distpath . --workpath ./build --specpath ./build
# start script
# scan_vars.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 lxml.etree as ET
from xml.dom import minidom
from makefile_parser import parse_project
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, global_defs):
optional_printf(PRINT_STATUS, "Starting analysis of variables across files...")
index = clang.cindex.Index.create()
define_args = [f"-D{d}" for d in global_defs]
args = [f"-I{inc}" for inc in include_dirs] + define_args
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
if 'Drivers' in node.location.file.name:
return
if 'uint' in node.spelling:
a = 1
# Проверяем, является ли тип указателем на функцию
# Признак: в типе есть '(' и ')' и '*', например: "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 try_guess_std_include():
# Популярные места, где может лежать stdint.h
guesses = [
r"C:\Keil_v5\ARM\ARMCLANG\include",
r"C:\Program Files (x86)\GNU Arm Embedded Toolchain",
r"C:\Program Files (x86)\Arm GNU Toolchain"
]
found = []
for base in guesses:
for root, dirs, files in os.walk(base):
if "stdint.h" in files:
found.append(root)
return found
def analyze_typedefs_and_structs_across_files(c_files, include_dirs, global_defs):
optional_printf(PRINT_STATUS, "Starting analysis of typedefs and structs across files...")
index = clang.cindex.Index.create()
define_args = [f"-D{d}" for d in global_defs]
extra_std_include_dirs = try_guess_std_include()
args = [f"-I{inc}" for inc in include_dirs] + extra_std_include_dirs + define_args
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', 't_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', 't_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', 't_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, global_defs = parse_project(makefile_path, proj_path)
vars, includes, externs = analyze_variables_across_files(c_files, h_files, include_dirs, global_defs)
typedefs, structs = analyze_typedefs_and_structs_across_files(c_files, include_dirs, global_defs)
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, global_defs = parse_project(makefile_path, proj_path)
vars, includes, externs = analyze_variables_across_files(c_files, h_files, include_dirs, global_defs)
typedefs, structs = analyze_typedefs_and_structs_across_files(c_files, include_dirs, global_defs)
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")