Files
Balsam_167/UKSSTMS320F28335/build_identity.py
Andrey dce9dd41a4 Использовать общий итератор версии прошивки
Убрать повторное увеличение версии из проектной обёртки.
Проверять прямое подключение templates и вызов через обёртку в CCS.
Сохранить текущий номер прошивки 1.0.5 и уточнить документацию.
2026-10-19 12:02:08 +03:00

125 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Подготавливает версию и выпускает проверенный C2000 SCI8 по настройкам JSON.""" # Один переносимый сценарий обслуживает оба шага CCS через BAT.
import argparse # Разбирает команды prepare и build без зависимости от установленной IDE.
from datetime import datetime # Восстанавливает регистры даты из времени UTC, зафиксированного до компиляции.
import hashlib # Проверяет SHA-256 точных байтов BIN перед переносом в постоянные каталоги.
import json # Хранит изделие, счётчик версии, пути и предел EEPROM в одном проектном файле.
from pathlib import Path # Разрешает относительные пути от JSON, а не от рабочего каталога CCS.
import shutil # Переносит только прошедшие проверку файлы текущей сборки.
import struct # Читает загрузочные секции SCI8 как 16-битные слова C28x.
import subprocess # Запускает публичные BAT общей библиотеки и передаёт ошибки вызывающей сборке.
import tempfile # Изолирует результаты конверсии до проверки паспорта и контрольных файлов; счётчик теперь сохраняет общий генератор templates.
def read_settings(config): # Проверяет настройки до изменения счётчика и запуска общих шагов.
settings = json.loads(config.read_text(encoding="utf-8-sig")) # Поддерживает UTF-8 как с BOM, так и без него.
version = settings.get("version") # Единственным сохраняемым источником версии является проектный JSON.
if not isinstance(version, list) or len(version) != 3 or any(type(value) is not int or not 0 <= value <= limit for value, limit in zip(version, (255, 255, 999))): # Контракт firmware-info v1 допускает major/minor 0..255 и patch 0..999.
raise ValueError("version must be [major 0..255, minor 0..255, patch 0..999]") # Не допускает неверную версию в счётчик или заголовок компилятора.
for field in ("product", "target", "templates"): # Требует идентификацию изделия и явно выбранную общую библиотеку.
if not isinstance(settings.get(field), str) or not settings[field].strip(): # Не принимает отсутствующее или пустое значение за пригодную настройку.
raise ValueError(f"missing {field} in {config}") # Указывает поле, которое нужно заполнить при переносе проекта.
if type(settings.get("increment_version", False)) is not bool: # Переключатель итератора допускает только настоящие JSON true/false.
raise ValueError("increment_version must be true or false") # Не трактует строку false как включённый счётчик Python.
limit = settings.get("max_image_bytes") # Предел образа задаёт плата; отсутствие поля отключает только ограничение размера.
if limit is not None and (type(limit) is not int or limit < 24): # Минимальная таблица SCI8 занимает 22 байта заголовка и два байта терминатора.
raise ValueError("max_image_bytes must be an integer >= 24") # Не принимает отрицательный либо дробный размер EEPROM.
if "copy_to" in settings and (not isinstance(settings["copy_to"], str) or not settings["copy_to"].strip()): # Дополнительный каталог копирования должен быть явно задан непустым путём.
raise ValueError("copy_to must be a nonempty path or omitted") # Для отключения копирования достаточно удалить поле из JSON.
return settings # Возвращает проверенные настройки без скрытых значений конкретного изделия.
def config_path(config, value): # Делает размещение templates и дополнительного выпуска независимым от текущего каталога.
return (config.parent / value).resolve() # Абсолютный путь остаётся абсолютным, относительный отсчитывается от JSON.
def prepare(config, build_dir): # Передаёт резервирование версии и генерацию паспорта штатному precompile templates без повторного увеличения номера.
settings = read_settings(config) # Сначала проверяет настройки, чтобы ошибочный JSON не занимал номер сборки.
script = config_path(config, settings["templates"]) / "tools/firmware-build/ccs/precompile.bat" # Выбирает публичную точку входа общей зависимости из JSON.
if not script.is_file(): raise FileNotFoundError(script) # Отсутствие templates останавливает шаг до изменения счётчика.
output = build_dir / "generated" # Каждая конфигурация CCS получает отдельные заголовок и метаданные.
subprocess.run(["cmd.exe", "/d", "/c", "call", str(script), "--identity", str(config.parent), str(config), str(output)], check=True) # Общий precompile сам увеличивает версию при increment_version=true и создаёт единый паспорт для компиляции; ошибка останавливает сборку.
return json.loads((output / "firmware_identity.json").read_text(encoding="utf-8")) # Возвращает фактический паспорт без повторного вычисления времени или Build ID.
def verify_embedded(image, metadata, max_image_bytes=None): # Проверяет общий паспорт firmware-info v1 независимо от имени платы и проекта.
data = image.read_bytes() # Проверяет именно октеты транспортного BIN, предназначенного для загрузки.
if data[:2] != b"\xaa\x08" or len(data) < 24: # Ключ загрузчика C2000 SCI8 равен 0x08AA; минимальная таблица содержит терминатор.
raise ValueError("Invalid SCI8 header or missing terminator") # Не допускает выпуск произвольного файла под видом загрузочного BIN.
if max_image_bytes is not None and len(data) > max_image_bytes: # Сравнивает размер в байтах с пределом из настроек конкретной платы.
raise ValueError(f"SCI8 exceeds max_image_bytes={max_image_bytes}") # Сохраняет защиту области параметров EEPROM при переносе между изделиями.
stamp = datetime.fromisoformat(metadata["built_at_utc"]) # Берёт время именно того prebuild, который подготовил заголовок.
build_id = metadata["build_id"] # Восемь ASCII-символов занимают четыре регистра контракта.
words = [1, *metadata["version"], stamp.year, stamp.month * 256 + stamp.day, stamp.hour * 256 + stamp.minute, stamp.second] # Восстанавливает первые восемь регистров; множитель 256 упаковывает два октета в слово.
words += [ord(build_id[index]) * 256 + ord(build_id[index + 1]) for index in range(0, 8, 2)] # Упаковывает символы Build ID в том же сетевом порядке, что обработчик устройства.
expected = struct.pack("<12H", *words) # SCI8 хранит двенадцать 16-битных регистров младшим октетом вперёд.
offset, found = 22, 0 # Пропускает 22 байта ключа, настроек загрузчика и точки входа C2000.
while offset + 2 <= len(data): # Обрабатывает конечную таблицу без чтения за пределами BIN.
count, = struct.unpack_from("<H", data, offset) # Размер секции задан количеством 16-битных слов.
offset += 2 # Пропускает поле размера перед адресом секции или терминатором.
if not count: break # Нулевая длина завершает таблицу и не содержит адреса назначения.
end = offset + 4 + count * 2 # После четырёх байтов адреса идут count слов полезной нагрузки.
if end > len(data): raise ValueError("Truncated SCI8 section") # Не принимает усечённую секцию даже при совпавшем паспорте в её начале.
payload = data[offset + 4:end] # Проверяет только загружаемые слова, исключая адресные поля.
found += sum(payload[index:index + 24] == expected for index in range(0, len(payload) - 23, 2)) # Ищет паспорт из 24 байтов только на границе слова C28x.
offset = end # Переходит к следующей непересекающейся секции.
if offset != len(data) or count or found != 1: # Требует терминатор в конце файла и ровно один паспорт текущей компиляции.
raise ValueError(f"SCI8 does not contain exactly one matching prebuild identity: {found}") # Отклоняет старый OUT, чужую версию, дубликат и лишний хвост.
def verify_package(image, metadata): # Проверяет согласованность BIN и контрольных файлов до публикации.
manifest = json.loads(image.with_suffix(".bin.manifest.json").read_text(encoding="utf-8")) # Читает manifest, созданный общим postcompile для текущего файла.
data = image.read_bytes() # Вычисляет контрольные значения по точным байтам результата конверсии.
digest = hashlib.sha256(data).hexdigest() # SHA-256 служит проверкой целостности конкретного транспортного файла.
if manifest.get("file") != image.name or manifest.get("size_bytes") != len(data) or manifest.get("sha256") != digest: # Связывает имя, размер и содержимое BIN с manifest.
raise ValueError("BIN does not match its manifest") # Не переносит повреждённый выпуск в постоянный каталог.
if any(manifest.get(field) != metadata[field] for field in ("product", "target", "version", "build_id", "built_at_utc")): # Проверяет, что manifest относится к тому же паспорту prebuild.
raise ValueError("Manifest does not match prebuild identity") # Запрещает контрольные файлы от другой сборки.
if image.with_suffix(".bin.sha256").read_text(encoding="utf-8").strip() != f"{digest} {image.name}": # Общий формат SHA-256 содержит хеш и точное имя BIN через два пробела.
raise ValueError("Invalid BIN SHA-256 sidecar") # Не допускает рассогласованный комплект файлов для загрузки.
def build(compiler, build_dir, artifact, config): # Конвертирует OUT общей библиотекой и публикует только проверенную сборку.
settings = read_settings(config) # Берёт ограничения и пути из того же JSON, что использовался перед компиляцией.
script = config_path(config, settings["templates"]) / "tools/firmware-build/ccs/postcompile.bat" # Не содержит пути к конкретному репозиторию templates.
metadata_path = build_dir / "generated/firmware_identity.json" # Выбирает паспорт текущей конфигурации Debug или Release.
metadata = json.loads(metadata_path.read_text(encoding="utf-8")) # Отсутствующий prebuild останавливает выпуск до конверсии.
if any(metadata.get(field) != settings[field] for field in ("product", "target", "version")): # Сравнивает изделие, контроллер и сохраняемую версию с текущей конфигурацией.
raise ValueError("Configuration changed: run prebuild and rebuild before publishing") # Не выпускает старый OUT после изменения настроек или версии.
output = build_dir / "firmware" # Сохраняет стандартную структуру templates внутри BuildDirectory.
with tempfile.TemporaryDirectory(prefix=".identity-release-", dir=build_dir) as directory: # Все результаты до проверки остаются во временном каталоге этой сборки.
stage = Path(directory) # Изолирует новые результаты от предыдущих архивов и каталога копирования.
subprocess.run(["cmd.exe", "/d", "/c", "call", str(script), "--identity", str(compiler), str(build_dir), artifact, str(metadata_path), str(stage)], check=True) # Общий BAT формирует SCI8, manifest, SHA-256 и архив; ненулевой код прерывает выпуск.
staged_image = stage / (Path(artifact).name + "_" + metadata["build_id"] + ".bin") # Находит текущий BIN без поиска по времени среди старых файлов.
verify_embedded(staged_image, metadata, settings.get("max_image_bytes")) # Проверяет паспорт и проектный предел EEPROM перед постоянной публикацией.
verify_package(staged_image, metadata) # Проверяет SHA-256 и соответствие сопроводительных файлов до копирования.
output.mkdir(parents=True, exist_ok=True) # Создаёт постоянный каталог только после успешной проверки комплекта.
for item in stage.iterdir(): # Переносит результаты только этого вызова общего postcompile.
if item.is_dir(): shutil.copytree(item, output / item.name, dirs_exist_ok=True) # Сохраняет полный архив текущей сборки, не удаляя предыдущие.
else: shutil.copy2(item, output / item.name) # Сохраняет плоские HEX, BIN, карты и контрольные файлы.
image = output / staged_image.name # Возвращает постоянный путь после удаления временного каталога.
if "copy_to" in settings: # Копирование в прежний Bin включается только настройкой конкретного проекта.
destination = config_path(config, settings["copy_to"]) # Разрешает дополнительный каталог относительно JSON без привязки к имени репозитория.
destination.mkdir(parents=True, exist_ok=True) # Позволяет перенести проект в ещё не созданную папку выпусков.
for file in (image, image.with_suffix(".bin.manifest.json"), image.with_suffix(".bin.sha256"), output / (Path(artifact).name + ".hex")): # Копирует проверенный комплект, сохраняя прежние имена потребителей BIN и HEX.
if file.resolve() != destination / file.name: shutil.copy2(file, destination / file.name) # Допускает совпадение дополнительной папки с firmware без копирования файла в себя.
shutil.copy2(output / (Path(artifact).name + ".hex.map"), destination / (Path(artifact).name + ".map")) # Поддерживает старое имя карты конверсии без зашитого имени изделия.
verify_package(destination / image.name, metadata) # Подтверждает целостность также скопированного BIN и его контрольных файлов.
print(f"[firmware-build] Verified BIN: {image}") # Показывает пользователю точный файл проверенной сборки.
return image # Даёт тестам и ручным средствам явный результат без сканирования каталогов.
if __name__ == "__main__": # Импорт из тестов не запускает счётчик или конверсию.
parser = argparse.ArgumentParser(description=__doc__) # Объясняет назначение переносимого сценария при вызове через BAT.
commands = parser.add_subparsers(dest="command", required=True) # Разделяет prebuild и postbuild явными командами.
for command in ("prepare", "build"): # Оба шага принимают одинаковые настройки и BuildDirectory.
step = commands.add_parser(command) # Создаёт независимый интерфейс выбранного шага сборки.
step.add_argument("--config", type=Path, default=Path(__file__).with_name("firmware_identity.json")) # По умолчанию читает JSON рядом со скриптом; другой путь можно передать явно.
step.add_argument("--build-dir", type=Path, required=True) # CCS выбирает отдельную папку для каждой конфигурации.
if command == "build": # Компилятор и имя OUT требуются только после компиляции.
step.add_argument("--compiler", type=Path, required=True) # Получает CG_TOOL_ROOT без поиска конкретной установки CCS.
step.add_argument("--artifact", required=True) # Получает BuildArtifactFileBaseName без расширения OUT и без изменения пути.
args = parser.parse_args() # Проверяет обязательные параметры до изменения файлов.
config, build_dir = args.config.resolve(), args.build_dir.resolve() # Нормализует пути до передачи BAT, независимо от текущего каталога.
if args.command == "prepare": print(json.dumps(prepare(config, build_dir), ensure_ascii=False)) # Показывает фактическую версию и метаданные текущего prebuild.
else: build(args.compiler.resolve(), build_dir, args.artifact, config) # Запускает конверсию и обязательные проверки готового образа.