Убрать повторное увеличение версии из проектной обёртки. Проверять прямое подключение templates и вызов через обёртку в CCS. Сохранить текущий номер прошивки 1.0.5 и уточнить документацию.
125 lines
20 KiB
Python
125 lines
20 KiB
Python
"""Подготавливает версию и выпускает проверенный 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) # Запускает конверсию и обязательные проверки готового образа.
|