144 lines
7.4 KiB
Python
144 lines
7.4 KiB
Python
#!/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)
|