"""Сборочные операции sdbg: уникальные debug-символы без изменения инструкций.""" from __future__ import annotations import hashlib import json import re import subprocess from pathlib import Path from .macros import anchor_symbols, extract def digest(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def write_json(path: Path, data) -> None: temporary = path.with_suffix(path.suffix + '.tmp') temporary.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n') temporary.replace(path) def normalize(assembly: str, adb: str, module: str): """Меняет только имена debug-записей; сохраняет число строк .asm.""" match = re.search(r'^\s*\.module\s+(\S+)', assembly, re.M) if not match: raise ValueError('В asm отсутствует .module') old = match.group(1) # F/XF/L связывают статики, функции и локальные с единицей трансляции. def rename(text): text = re.sub(r'\b(X?F)' + re.escape(old) + r'(?=\$)', lambda m: m[1] + module, text) return re.sub(r'\bL' + re.escape(old) + r'(?=[.$])', 'L' + module, text) # Не трогаем .ascii и инструкции: строковый литерал может содержать # текст, похожий на debug-символ. Переименовываем только определения # и объявления символов, сгенерированные SDCC. symbol_line = re.compile(r'^\s*(?:\.globl\s+|(?:X?F|L)' + re.escape(old) + r'[$.])') assembly = ''.join(rename(line) if symbol_line.match(line) else line for line in assembly.splitlines(keepends=True)) adb = rename(adb) assembly = re.sub(r'(?m)^(\s*\.module\s+)\S+', lambda m: m[1] + module, assembly) adb = re.sub(r'(?m)^M:.*$', 'M:' + module, adb) markers, active, output = {}, {}, [] for number, line in enumerate(assembly.splitlines(keepends=True), 1): definition = re.match(r'\s*(C\$[^\s=]+)\s*=\s*\.', line) if definition: original = definition[1] parts = original.split('$') if len(parts) != 5: raise ValueError('Неизвестная форма CDB: ' + original) unique = f'C${module}_{len(markers)}${parts[2]}${parts[3]}${parts[4]}' active[original] = unique markers[unique] = {'original': original, 'file': parts[1], 'line': int(parts[2]), 'asm_line': number} line = line.replace(original, unique) elif re.match(r'\s*\.globl\s+C\$', line): original = line.split()[1] if original not in active: raise ValueError('CDB .globl без определения: ' + original) line = line.replace(original, active[original]) output.append(line) return ''.join(output), adb, markers def compile_unit(sdcc: Path, assembler: Path, source: Path, output: Path, flags, debug=True): """-S и те же опции ассемблера, которые SDCC 4.5 использует с --debug.""" source = source.resolve() asm, adb = output.with_suffix('.asm'), output.with_suffix('.adb') compile_flags = [*flags, '-DSPRINTER_SDBG_ANCHORS=1'] if debug else flags # SDCC -E кодирует внутренний разделитель строк inline asm байтом 0x87; # для списка #line нужны только ASCII-пути. Само описание логов извлекаем # отдельным metadata-проходом без asm-развёртки. preprocessed = subprocess.check_output([str(sdcc), *compile_flags, '-E', str(source)], text=True, errors='replace') dependencies = {str(source)} for filename in re.findall(r'^#(?:line)?\s*\d+\s+"([^"]+)"', preprocessed, re.M): path = Path(filename).resolve() if path.is_file(): dependencies.add(str(path)) hashes = {name: digest(Path(name)) for name in dependencies} if not debug: subprocess.run([str(sdcc), *flags, '-c', '-o', str(output), str(source)], check=True) if any(digest(Path(name)) != sha for name, sha in hashes.items()): raise ValueError('Исходники изменились во время компиляции') write_json(output.with_suffix('.sdbg-input.json'), {'dependency_hashes': hashes}) return metadata = subprocess.check_output([str(sdcc), *compile_flags, '-DSPRINTER_SDBG_METADATA=1', '-E', str(source)], text=True) macros = extract(metadata, dependencies) subprocess.run([str(sdcc), *compile_flags, '--debug', '-S', '-o', str(asm), str(source)], check=True) if any(digest(Path(name)) != sha for name, sha in hashes.items()): raise ValueError('Исходники изменились во время компиляции') original = asm.read_text() # Включаем output stem: один исходник может собираться в разные банки. module = 'sdbg_' + hashlib.sha256((str(source) + ':' + output.stem).encode()).hexdigest()[:16] assembly, descriptions, markers = normalize(original, adb.read_text(), module) assembly, log_macros = anchor_symbols(assembly, module, macros) asm.write_text(assembly) adb.write_text(descriptions) subprocess.run([str(assembler), '-plosgffwy', str(output), str(asm)], check=True) # Комментарии SDCC сохраняют фактические пути строк, включая заголовки. sources = {} for name in dependencies: sources.setdefault(Path(name).name, set()).add(name) for filename in re.findall(r'^;(.+?):\d+:', original, re.M): path = Path(filename).resolve() if path.is_file(): sources.setdefault(path.name, set()).add(str(path)) write_json(output.with_suffix('.sdbg-unit.json'), { 'module': module, 'source': str(source), 'source_sha256': digest(source), 'asm': asm.name, 'adb': adb.name, 'object': output.name, 'flags': compile_flags, 'markers': markers, 'log_macros': log_macros, 'dependency_hashes': hashes, 'sources': {k: sorted(v) for k, v in sources.items()}, })