# 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', 'int'), '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', 'int'), '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', 'int') 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_elem = ET.SubElement(root, "structs") for struct_name, fields in sorted(structs.items()): create_struct_element(structs_elem, struct_name, fields) # 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")