120 lines
10 KiB
Python
120 lines
10 KiB
Python
"""Convert a CCS C2000 OUT to SCI8 HEX/BIN and seal its pre-build identity.
|
||
|
||
Uses only the standard library, TI hex2000 and the shared identity module.
|
||
No network, SETGUI, or project-specific paths are required.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
from pathlib import Path
|
||
import re
|
||
import subprocess
|
||
import sys # Запускает проектный сборщик тем же Python, которым выполняется общий postbuild.
|
||
import tempfile
|
||
|
||
from identity import archive_build, seal, verify
|
||
|
||
|
||
def build_flash_variant(compiler: Path, build_dir: Path, source: Path, stage: Path) -> tuple: # Возвращает файлы дополнительного Flash-образа только для проектов с явным сборщиком.
|
||
builder = build_dir.resolve().parent / "build_flash.py" # Соглашение CCS: Debug/Release расположены рядом с проектным сценарием сборки Flash.
|
||
if not builder.is_file(): # Отсутствие сценария оставляет прежний выпуск EEPROM без дополнительных требований к проекту.
|
||
return () # Пустой список не добавляет Flash-файлы в архив обычного проекта.
|
||
image = stage / (source.stem + "_flash.out") # Отдельное имя и временный каталог исключают подмену RAM-образа и использование устаревшего Flash-файла.
|
||
subprocess.run([sys.executable, str(builder), "--postbuild", "--compiler", str(compiler.resolve()), "--build-dir", str(build_dir.resolve()), "--output", str(image)], cwd=build_dir, check=True) # Режим postbuild использует уже собранные объекты и запрещает повторный запуск Make.
|
||
files = (image, image.with_suffix(".map")) # Карта линковки сопровождает COFF и позволяет проверить адрес аппаратной точки входа.
|
||
for file in files: # Оба результата должны быть выпущены текущим вызовом сборщика.
|
||
if not file.is_file() or not file.stat().st_size: # Успешный код процесса без непустого результата не считается успешной сборкой.
|
||
raise ValueError(f"Flash builder produced no file: {file}") # Ошибка останавливает postbuild до архивирования или экспорта неполного выпуска.
|
||
return files # Передаёт проверенные файлы вызывающему шагу для подписи и сохранения.
|
||
|
||
|
||
def postbuild(compiler: Path, build_dir: Path, artifact: str,
|
||
metadata: Path, output: Path, export: Path = None) -> Path:
|
||
# CCS may include the configured output directory in this macro.
|
||
# Resolve the input separately; output filenames must use only its basename.
|
||
artifact_path = Path(artifact)
|
||
if not artifact or artifact_path.name in ("", ".", ".."):
|
||
raise ValueError("artifact must name an OUT file without its extension")
|
||
artifact = artifact_path.name
|
||
if any(c in artifact for c in '\\/:*?"<>|'):
|
||
raise ValueError("invalid artifact file base name")
|
||
source_base = (artifact_path if artifact_path.is_absolute()
|
||
else build_dir / artifact_path)
|
||
source = Path(str(source_base) + ".out").resolve()
|
||
tool = compiler.resolve() / "bin" / "hex2000.exe"
|
||
if not tool.is_file():
|
||
raise FileNotFoundError(f"C2000 Hex Utility not found: {tool}")
|
||
if not source.is_file():
|
||
raise FileNotFoundError(f"compiled OUT not found: {source}")
|
||
metadata = metadata.resolve()
|
||
settings = json.loads(metadata.read_text(encoding="utf-8"))
|
||
build_id = settings["build_id"]
|
||
if not isinstance(build_id, str) or not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", build_id):
|
||
raise ValueError("invalid pre-build identity; run precompile before building")
|
||
if build_id == "NOGIT000":
|
||
print("[firmware-build] Git identity unavailable: using NOGIT000")
|
||
output = output.resolve()
|
||
output.mkdir(parents=True, exist_ok=True)
|
||
# Keep failed conversions and incomplete manifests out of the final folder.
|
||
with tempfile.TemporaryDirectory(prefix=".firmware-", dir=output) as temporary:
|
||
stage = Path(temporary)
|
||
hex_image = stage / (artifact + ".hex")
|
||
bin_image = stage / (artifact + "_" + build_id + ".bin")
|
||
flash_files = build_flash_variant(compiler, build_dir, source, stage) # Сначала собирает дополнительный Flash-вариант; его ошибка не допускает выпуск нового BIN.
|
||
for image, mode in ((hex_image, "-i"), (bin_image, "-b")):
|
||
subprocess.run([str(tool), str(source), "-boot", "-sci8",
|
||
"-map", str(image.with_suffix(image.suffix + ".map")),
|
||
"-o", str(image), mode], check=True)
|
||
if not image.is_file() or not image.stat().st_size:
|
||
raise ValueError(f"hex2000 produced no image: {image}")
|
||
seal(bin_image, metadata)
|
||
verify(bin_image, bin_image.with_suffix(bin_image.suffix + ".manifest.json"))
|
||
if flash_files: # Проекты со сборщиком получают независимую проверку точных байтов Flash-образа.
|
||
seal(flash_files[0], metadata) # Использует тот же паспорт prebuild для второго варианта этой сборки.
|
||
verify(flash_files[0], flash_files[0].with_suffix(".out.manifest.json")) # Проверяет размер и SHA-256 перед сохранением результатов.
|
||
serial_image = stage / (artifact + "_flash_rs.bin") # Отдельный SCI8 BIN содержит Flash-адреса и предназначен только для новой команды записи по RS.
|
||
serial_map = serial_image.with_suffix('.bin.map') # Карта конверсии позволяет отличить Flash-адреса от EEPROM/RAM BIN.
|
||
subprocess.run([str(tool), str(flash_files[0]), '-boot', '-sci8', '-map', str(serial_map), '-o', str(serial_image), '-b'], check=True) # Конвертирует именно Flash COFF с загрузочными адресами секций.
|
||
if not serial_image.is_file() or not serial_image.stat().st_size: # Пустая конверсия не должна попасть в выпуск либо каталог GUI.
|
||
raise ValueError(f'hex2000 produced no Flash RS image: {serial_image}') # Передаёт ошибку конверсии общему postcompile.
|
||
seal(serial_image, metadata) # Привязывает RS BIN к той же версии, ревизии и времени текущего prebuild.
|
||
verify(serial_image, serial_image.with_suffix('.bin.manifest.json')) # Проверяет точные байты перед копированием в Bin и архив.
|
||
flash_files += (serial_image, serial_map, serial_image.with_suffix('.bin.manifest.json'), serial_image.with_suffix('.bin.sha256')) # Включает транспортный BIN, карту и контрольные файлы в общий выпуск Flash.
|
||
for file in flash_files: # Сохраняет COFF и карту также рядом с обычным OUT для выбора в CCS.
|
||
destination = source.parent / file.name # Каталог исходного OUT задаёт место назначения без путей конкретного изделия в templates.
|
||
destination.write_bytes(file.read_bytes()) # Копирует только результаты успешного текущего вызова, сохраняя исходный RAM-образ.
|
||
for item in stage.iterdir():
|
||
item.replace(output / item.name)
|
||
result = output / bin_image.name # BIN остаётся основным образом EEPROM и возвращаемым результатом общего postbuild.
|
||
flash_archive = tuple(output / file.name for file in flash_files) # После переноса из временного каталога берёт актуальные Flash-файлы из firmware.
|
||
if flash_files: # Добавляет сопроводительные файлы только при выполненной сборке Flash.
|
||
flash_archive += (output / (flash_files[0].name + ".manifest.json"), output / (flash_files[0].name + ".sha256")) # Архив и GUI получают отдельный паспорт и контрольную сумму Flash COFF.
|
||
print(f"[firmware-build] Flash OUT: {source.parent / flash_files[0].name}") # Показывает пользователю CCS конкретный файл для прошивки внутренней Flash через JTAG.
|
||
archive_build(result, metadata, output, ( # Сохраняет файлы текущей сборки в архиве и передаёт весь архив в каталог GUI.
|
||
source, # Копирует исходный COFF-файл OUT с символами отладки под его именем для загрузки через JTAG из GUI.
|
||
output / hex_image.name,
|
||
output / (hex_image.name + ".map"),
|
||
output / (bin_image.name + ".map"), *flash_archive), export=export) # Экспортирует EEPROM и Flash в одной папке текущего выпуска.
|
||
print(f"[firmware-build] BIN: {result}")
|
||
print(f"[firmware-build] Manifest: {result}.manifest.json")
|
||
print(f"[firmware-build] SHA-256: {result}.sha256")
|
||
return result
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
for name in ("compiler", "build-dir", "metadata", "output"):
|
||
parser.add_argument("--" + name, type=Path, required=True)
|
||
parser.add_argument("--artifact", required=True)
|
||
parser.add_argument("--export", type=Path)
|
||
try:
|
||
postbuild(**vars(parser.parse_args()))
|
||
except (OSError, ValueError, KeyError, TypeError, subprocess.CalledProcessError) as error:
|
||
parser.exit(1, f"[firmware-build] ERROR: {error}\n")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|