242 lines
8.8 KiB
Python
242 lines
8.8 KiB
Python
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)
|
||
|
||
# Добавим <Define>
|
||
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) |