import os import re from lxml import etree as ET def strip_single_line_comments(code): # Удалим // ... до конца строки return re.sub(r'//.*?$', '', code, flags=re.MULTILINE) def read_file_try_encodings(filepath): if not os.path.isfile(filepath): # Файл не существует — просто вернуть пустую строку или None return "", None for enc in ['utf-8', 'cp1251']: try: with open(filepath, 'r', encoding=enc) as f: content = f.read() content = strip_single_line_comments(content) return content, enc except UnicodeDecodeError: continue raise UnicodeDecodeError(f"Не удалось прочитать файл {filepath} с кодировками utf-8 и cp1251") def find_all_includes_recursive(c_files, include_dirs, processed_files=None): """ Рекурсивно ищет все include-файлы начиная с заданных c_files. Возвращает множество ПОЛНЫХ ПУТЕЙ к найденным include-файлам. include_dirs — список директорий, в которых ищем include-файлы. processed_files — множество уже обработанных файлов (для избежания циклов). """ if processed_files is None: processed_files = set() include_files = set() include_pattern = re.compile(r'#include\s+"([^"]+)"') for cfile in c_files: norm_path = os.path.normpath(cfile) if norm_path in processed_files: continue processed_files.add(norm_path) content, _ = read_file_try_encodings(cfile) if content is None: continue includes = include_pattern.findall(content) for inc in includes: # Ищем полный путь к include-файлу в include_dirs inc_full_path = None for dir_ in include_dirs: candidate = os.path.normpath(os.path.join(dir_, inc)) if os.path.isfile(candidate): inc_full_path = os.path.abspath(candidate) break if inc_full_path: include_files.add(inc_full_path) # Рекурсивный обход вложенных includes if inc_full_path not in processed_files: nested_includes = find_all_includes_recursive( [inc_full_path], include_dirs, processed_files ) include_files.update(nested_includes) return include_files def parse_objects_list(objects_list_path, project_root): c_files = [] include_dirs = set() if not os.path.isfile(objects_list_path): return c_files, include_dirs with open(objects_list_path, 'r', encoding='utf-8') as f: lines = f.readlines() for line in lines: line = line.strip().strip('"').replace("\\", "/") if line.endswith(".o"): c_file = re.sub(r"\.o$", ".c", line) abs_path = os.path.normpath(os.path.join(project_root, c_file)) if os.path.isfile(abs_path): if not any(x in abs_path for x in ["DebugTools", "v120", "v100"]): c_files.append(abs_path) include_dirs.add(os.path.dirname(abs_path)) return c_files, include_dirs def parse_uvprojx(uvprojx_path): import xml.etree.ElementTree as ET import os tree = ET.parse(uvprojx_path) root = tree.getroot() project_dir = os.path.dirname(os.path.abspath(uvprojx_path)) c_files = [] include_dirs = set() defines = set() # Найдём C-файлы и директории for file_elem in root.findall(".//FilePath"): file_path = file_elem.text if file_path: abs_path = os.path.normpath(os.path.join(project_dir, file_path)) if os.path.isfile(abs_path): if abs_path.endswith(".c"): c_files.append(abs_path) include_dirs.add(os.path.dirname(abs_path)) # Включаем IncludePath for inc_path_elem in root.findall(".//IncludePath"): path_text = inc_path_elem.text if path_text: paths = path_text.split(';') for p in paths: p = p.strip() if p: abs_inc_path = os.path.normpath(os.path.join(project_dir, p)) if os.path.isdir(abs_inc_path): include_dirs.add(abs_inc_path) # Добавим for define_elem in root.findall(".//Define"): def_text = define_elem.text if def_text: for d in def_text.split(','): d = d.strip() if d: defines.add(d) h_files = find_all_includes_recursive(c_files, include_dirs) return sorted(c_files), sorted(h_files), sorted(include_dirs), sorted(defines) def parse_makefile(makefile_path, proj_path): import os import re project_root = os.path.abspath(proj_path) c_files = [] include_dirs = set() defines = [] # Заглушка: нет define-параметров из Makefile with open(makefile_path, 'r', encoding='utf-8') as f: lines = f.readlines() raw_entries = [] collecting = False for line in lines: stripped = line.strip() if (("ORDERED_OBJS" in stripped or "C_SOURCES" in stripped) and ("+=" in stripped or "=" in stripped)): collecting = True if collecting: line_clean = stripped.rstrip("\\").strip() if line_clean: line_clean = re.sub(r"\$\([^)]+\)", "", line_clean) line_clean = re.sub(r"\$\{[^}]+\}", "", line_clean) raw_entries.append(line_clean) if not stripped.endswith("\\"): collecting = False for entry in raw_entries: for token in entry.split(): token = token.strip('"') if not token: continue token = token.replace("\\", "/") if token.endswith(".obj"): token = re.sub(r"\.obj$", ".c", token) elif token.endswith(".o"): token = re.sub(r"\.o$", ".c", token) if token.endswith(".c"): abs_path = os.path.normpath(os.path.join(project_root, token)) if os.path.isfile(abs_path): if not any(x in abs_path for x in ["DebugTools", "v120", "v100"]): c_files.append(abs_path) include_dirs.add(os.path.dirname(abs_path)) if not c_files: makefile_dir = os.path.dirname(os.path.abspath(makefile_path)) objects_list_path = os.path.join(makefile_dir, "objects.list") c_from_objects, inc_from_objects = parse_objects_list(objects_list_path, project_root) c_files.extend(c_from_objects) include_dirs.update(inc_from_objects) for line in lines: if "-I" in line or "C_INCLUDES" in line: matches = re.findall(r"-I\s*([^\s\\]+)", line) for match in matches: match = match.strip('"').replace("\\", "/") abs_include = os.path.normpath(os.path.join(project_root, match)) if os.path.isdir(abs_include): include_dirs.add(abs_include) # Добавляем пути с заменой 'Src' на 'Inc', если путь заканчивается на 'Src' additional_includes = set() for inc in include_dirs: if inc.endswith(os.sep + "Src") or inc.endswith("/Src"): inc_inc = inc[:-3] + "Inc" # заменяем 'Src' на 'Inc' if os.path.isdir(inc_inc): additional_includes.add(inc_inc) include_dirs.update(additional_includes) h_files = find_all_includes_recursive(c_files, include_dirs) return sorted(c_files), sorted(h_files), sorted(include_dirs), sorted(defines) def parse_project(project_file_path, project_root=None): """ Выбирает парсер в зависимости от расширения project_file_path: - для *.uvprojx и *.uvproj вызывается парсер Keil - для остальных - parse_makefile project_root нужен для parse_makefile, если не передан - берется из project_file_path """ ext = os.path.splitext(project_file_path)[1].lower() if ext in ['.uvprojx', '.uvproj']: # Парсим Keil проект return parse_uvprojx(project_file_path) else: # Парсим makefile if project_root is None: project_root = os.path.dirname(os.path.abspath(project_file_path)) return parse_makefile(project_file_path, project_root)