#!/usr/bin/env python3 """Транзакционная debug-сборка: ошибочная линковка не публикует старый ihx.""" from __future__ import annotations import fcntl import hashlib import json import os from pathlib import Path import shutil import subprocess import sys import tempfile from sdbg.build import digest, write_json ROOT = Path(__file__).resolve().parents[1] def metadata(args): work, exe = map(Path, args[:2]) layout = dict(zip(('mode', 'code', 'data', 'load', 'entry', 'stack'), args[2:])) units = [json.loads(p.read_text()) for p in sorted(work.glob('*.sdbg-unit.json'))] if not units: raise ValueError('Нет отладочных единиц трансляции') inputs = {} snapshots = work / 'sources' snapshots.mkdir() dependencies = {} for unit in units + [json.loads(p.read_text()) for p in work.glob('*.sdbg-input.json')]: for name, expected in unit['dependency_hashes'].items(): if name in dependencies and dependencies[name] != expected: raise ValueError('Зависимость изменилась между TU: ' + name) dependencies[name] = expected for name, expected in dependencies.items(): source = Path(name) sha = digest(source) if sha != expected: raise ValueError('Зависимость изменилась после компиляции: ' + name) snapshot = 'sources/' + sha + source.suffix shutil.copyfile(source, work / snapshot) inputs[name] = {'sha256': sha, 'snapshot': snapshot} artifacts = {str(p.relative_to(work)): digest(p) for p in work.rglob('*') if p.is_file()} write_json(work / 'manifest.json', { 'schema_version': 1, 'build_id': digest(exe), 'executable_sha256': digest(exe), 'executable': exe.name, 'layout': layout, 'units': units, 'sources': inputs, 'artifacts': artifacts, 'sdcc': subprocess.check_output([str(ROOT/'third_party/sdcc/bin/sdcc'), '--version'], text=True).strip(), 'limitations': ['Нет карты библиотек', 'Нет location ranges локальных', 'Только оффлайновая карта; образ в MAME не проверен'], }) def run(wrapper, args): if '-o' not in args or args.index('-o') + 1 >= len(args): raise ValueError('Требуется -o FILE') if '--src-debug' in args and '--src-debug-file' in args: raise ValueError('--src-debug и --src-debug-file взаимоисключающие') # Валидируем выбранные TU до запуска compiler. Значения иных опций не TU. sources, selected = [], [] i = 0 valued = {'-o', '-I', '-L', '-E', '-S', '--code-loc', '--data-loc', '--memory', '--memory-manual', '--stack-size', '-Wl', '--mkexe', '--max-allocs', '--gfx'} while i < len(args): arg = args[i] if arg in valued | {'--bank', '--w3', '--src-debug-file'}: if i + 1 == len(args): raise ValueError(arg + ' требует аргумент') value = args[i+1] if arg == '--bank': sources.append(Path(value.split('=', 1)[-1]).resolve()) if arg == '--w3': sources.append(Path(value).resolve()) if arg == '--src-debug-file': selected.append(Path(value).resolve()) i += 2 elif arg == '--bank-data' and i+1 < len(args) and args[i+1].isdigit(): i += 2 else: if not arg.startswith('-'): sources.append(Path(arg).resolve()) i += 1 if any(p not in sources or not p.is_file() for p in selected): raise ValueError('--src-debug-file должен указывать на входной TU') output = Path(args[args.index('-o')+1]).resolve() output.parent.mkdir(parents=True, exist_ok=True) final_work = output.parent / ('.sprinter-cc-' + output.stem) locks = output.parent / '.resource-stamps' locks.mkdir(exist_ok=True) with (locks / (final_work.name + '.lock')).open('a') as lock: fcntl.flock(lock, fcntl.LOCK_EX) keep_stage = bool(os.environ.get('SDBG_KEEP_FAILED_STAGE')) with tempfile.TemporaryDirectory(prefix='.sdbg-build-', dir=output.parent, delete=not keep_stage) as temporary: stage = Path(temporary) if keep_stage: print('sdbg: диагностическая stage ' + str(stage), file=sys.stderr) new_exe = stage / output.name command = list(args) command[command.index('-o')+1] = str(new_exe) env = dict(os.environ, SPRINTER_SDBG_ACTIVE='1', SPRINTER_PYTHON=sys.executable) subprocess.run([str(Path(wrapper).resolve()), *command], env=env, check=True) work = stage / final_work.name manifest = json.loads((work/'manifest.json').read_text()) manifest['command'] = [str(Path(wrapper).resolve()), *args] manifest['working_directory'] = str(Path.cwd()) manifest['selected_sources'] = [str(p) for p in selected or sources] manifest['input_sources'] = [str(p) for p in sources] manifest['executable_path'] = str(output) identity = {'executable': manifest['executable_sha256'], 'sources': {p: v['sha256'] for p,v in manifest['sources'].items()}, 'command': manifest['command'], 'sdcc': manifest['sdcc']} manifest['build_id'] = hashlib.sha256(json.dumps(identity, sort_keys=True).encode()).hexdigest() write_json(work/'manifest.json', manifest) # Проверяем пакет до публикации. Старые exe/work переживают любой # сбой compiler/linker/парсера; manifest не создаётся из stale ihx. from sdbg.model import DebugMap model = DebugMap(work) index = work/(output.stem+'.sdbg.json') write_json(index, {'schema_version': 1, 'build_id': manifest['build_id'], 'functions': model.functions, 'variables': model.variables, 'instructions': list(model.instructions.values()), 'markers': model.markers, 'logpoints': model.logpoints}) manifest['artifacts'][index.name] = digest(index) write_json(work/'manifest.json', manifest) backup = stage/'previous-work' if final_work.exists(): final_work.rename(backup) try: work.rename(final_work) os.replace(new_exe, output) except BaseException: if final_work.exists(): shutil.rmtree(final_work) if backup.exists(): backup.rename(final_work) raise print('sdbg: пакет ' + str(final_work)) if __name__ == '__main__': try: if sys.argv[1:2] == ['--metadata']: metadata(sys.argv[2:]) else: run(sys.argv[1], sys.argv[2:]) except subprocess.CalledProcessError as error: print(f'sdbg: сборка не прошла (код {error.returncode}); предыдущий пакет сохранён', file=sys.stderr) sys.exit(1) except (ValueError, OSError) as error: print('sdbg: ' + str(error), file=sys.stderr) sys.exit(1)