Files
templates/c/firmware-info/tools/c2000_build.py
T
Andrey 5bf3167064 build(firmware-info): Объедини подготовку и выпуск образов C2000
Направь CCS precompile/postcompile в общий c2000_build, чтобы резервирование версии, проверка EEPROM, MAP, SCI8 и Flash выполнялись одним процессом перед экспортом.

Сохраняй варианты eeprom, flash, can и rs в одном проверенном комплекте с manifest и SHA-256. Ошибка проверки блокирует постоянный выпуск и копирование устаревших образов.
2026-10-23 14:16:20 +03:00

437 lines
75 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.
"""Единая подготовка паспорта и проверенного RAM/Flash-комплекта C2000 для CCS и Публикатора.""" # Сетевой выпуск выполняет отдельный Публикатор SETGUI после импорта подготовленной папки.
import argparse # Разбирает одинаковые аргументы защищённого postbuild для CCS Debug и Release.
import base64 # Декодирует точные байты секций COFF из официального XML-вывода TI ofd2000.
from datetime import datetime # Восстанавливает слова UTC из паспорта, созданного перед компиляцией.
import hashlib # Проверяет точные байты конечного BIN и сопроводительного SHA-256.
import importlib.util # Подключает штатный механизм экспорта только после проверки временного выпуска.
import json # Сверяет изделие, аппаратный профиль и идентификацию текущей сборки.
from pathlib import Path # Разрешает пути относительно выбранной конфигурации CCS, а не текущего терминала.
import re # Читает абсолютные EEPROM-символы из карты линкера TI.
import shutil # Копирует только полностью проверенные файлы из временного каталога.
import struct # Разбирает 16-битные слова SCI8 и адреса загрузочных блоков C28x.
import subprocess # Вызывает общую BAT-точку конверсии без публикации во внешнее приложение.
import tempfile # Изолирует непроверенные результаты общего postcompile от постоянных выпусков.
import xml.etree.ElementTree as ET # Проверяет фактические LOAD/RUN адреса Flash COFF без собственного двоичного парсера TI.
import sys # Запускает общий CLI и подключает конвертор без зависимости от GUI.
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / 'python')) # Контракт защищённой Flash берётся из того же checkout templates, что общий сборщик.
from protocan.f28335_flash import APP_START, APP_DATA, APP_STOP, coff_sections, validate_words # Проверяет отсутствие сектора A и описание FAB1 одинаково для COFF и SCI8.
def numeric_define(header, name): # Читает единственное числовое C-определение без исполнения выражений из заголовка платы.
text = header.read_text(encoding="utf-8-sig") # Поддерживает UTF-8 с BOM для пользовательских заголовков конфигурации.
text = re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S) # Удаляет комментарии перед поиском настоящих директив препроцессора.
values = re.findall(r"^\s*#\s*define\s+" + re.escape(name) + r"\s+([^\n]+)", text, re.M) # Требует определение нужного макроса, а не упоминание в тексте.
if len(values) != 1 or not re.fullmatch(r"(?:0[xX][0-9a-fA-F]+|[0-9]+)[uUlL]*", values[0].strip()): # Исключает неоднозначность и выражения, которые могут отличаться от C-препроцессора.
raise ValueError(f"{header}: define {name} once as an integer") # Останавливает выпуск при неподдерживаемой структуре общей разметки.
return int(values[0].strip().rstrip("uUlL"), 0) # Преобразует десятичную либо шестнадцатеричную величину в число Python.
def git_state(repository): # Снимает происхождение выбранного рабочего дерева с локальным safe.directory без изменения глобальных настроек Git.
directory = repository.resolve() # Рабочий каталог CCS может быть вложен в репозиторий приложения.
trusted = next((path for path in (directory, *directory.parents) if (path / ".git").exists()), directory) # Git проверяет корень checkout; .git-файл также учитывает подключённый templates submodule.
command = ["git", "-c", "safe.directory=" + trusted.as_posix(), "-C", str(directory)] # Локальное доверие ограничено корнем явно выбранной зависимости или исходников проекта.
try: # Отсутствие Git сохраняет прежний явный NOGIT-контракт общей библиотеки.
commit = subprocess.check_output(command + ["rev-parse", "HEAD"], stderr=subprocess.DEVNULL, text=True, encoding="utf-8").strip() # Получает полный SHA именно компилируемого дерева.
dirty = bool(subprocess.check_output(command + ["status", "--porcelain", "--untracked-files=normal"], stderr=subprocess.DEVNULL, text=True, encoding="utf-8").strip()) # Учитывает новые и изменённые исходники при формировании знака плюс.
return {"commit": commit, "dirty": dirty} # Возвращает совместимую с общим генератором структуру происхождения.
except (OSError, subprocess.CalledProcessError): # Не выдаёт неизвестную ревизию за чистое дерево.
return {"commit": None, "dirty": None} # Генератор пометит такую сборку идентификатором NOGIT000.
# Объявляет функцию read_version и её входные параметры.
def read_version(header):
# Читает исходный файл или образ для последующей проверки и разбора.
text = header.read_text(encoding="utf-8-sig")
# Задаёт text по выражению справа; значение используется последующими операциями.
text = re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S)
# Задаёт version по выражению справа; значение используется последующими операциями.
version = []
# Перебирает part, limit in (("MAJOR", 255), ("MINOR", 255), ("PATCH", 999)) в указанном порядке.
for part, limit in (("MAJOR", 255), ("MINOR", 255), ("PATCH", 999)):
# Задаёт name по выражению справа; значение используется последующими операциями.
name = "FIRMWARE_VERSION_" + part
# Задаёт definitions по выражению справа; значение используется последующими операциями.
definitions = re.findall(r"^\s*#\s*define\s+" + name + r"\b([^\n]*)", text, re.M)
# Проверяет условие if len(definitions) != 1 перед выполнением следующего блока.
if len(definitions) != 1:
# Останавливает текущую операцию с исключением ValueError(f"{header}: define {name} exactly once").
raise ValueError(f"{header}: define {name} exactly once")
# Задаёт value по выражению справа; значение используется последующими операциями.
value = definitions[0].strip()
# Проверяет условие if not re.fullmatch(r"(?:0|[1-9][0-9]*)[uU]?", value) перед выполнением следующего блока.
if not re.fullmatch(r"(?:0|[1-9][0-9]*)[uU]?", value):
# Останавливает текущую операцию с исключением ValueError(f"{header}: {name} must be a decimal integer").
raise ValueError(f"{header}: {name} must be a decimal integer")
# Задаёт number по выражению справа; значение используется последующими операциями.
number = int(value.rstrip("uU"))
# Проверяет условие if not 0 <= number <= limit перед выполнением следующего блока.
if not 0 <= number <= limit:
# Останавливает текущую операцию с исключением ValueError(f"{header}: {name} must be in 0..{limit}").
raise ValueError(f"{header}: {name} must be in 0..{limit}")
# Продолжает текущий вызов, список или выражение: version.append(number); порядок аргументов сохраняется.
version.append(number)
# Возвращает version вызывающему коду и завершает функцию.
return version
def read_policy(project): # Загружает декларативные настройки памяти платы, оставляя общий алгоритм в templates.
path = project / "firmware_build_profile.json" # Профиль расположен рядом с CCS-проектом и версионируется вместе с linker-файлами.
return json.loads(path.read_text(encoding="utf-8-sig")) if path.is_file() else {} # Старые проекты 166 используют прежний JSON без обязательного нового файла.
def configured_version(project, settings): # Поддерживает ручной C-заголовок 167 и сохраняемый JSON-счётчик 166 одной реализацией.
header = read_policy(project).get("version_header") # Источник версии задаётся платой явно, без определения модели по имени каталога.
return read_version(project / header) if header else settings["version"] # Проверку диапазонов JSON выполняет существующий генератор identity.
def read_profile(project, templates, eeprom=None): # Получает физическую EEPROM из тех же C-заголовков, которыми пользуется компилятор.
policy = read_policy(project) # Все аппаратные пути и имена макросов остаются данными конкретной платы.
can_profile = policy.get("can_boot_profile") # Явное включение CAN-выпуска не меняет поведение старых проектов без этого поля.
if can_profile not in (None, "f28335-m95m02-ab"): # Поддерживаемый профиль соответствует реальному порту M95M02 с двумя слотами по 130560 октетов.
raise ValueError("Unsupported CAN boot profile: " + str(can_profile)) # Опечатка в профиле не должна незаметно отключать обязательный CAN-образ.
config = policy.get("eeprom") # Отсутствие профиля сохраняет контракт 166 с max_image_bytes в основном JSON.
if not config: # У старого проекта нет дополнительной аппаратной разметки для проверки MAP.
path = project / "firmware_identity.json" # Прямой общий postcompile также поддерживает отдельно переданный готовый паспорт.
settings = json.loads(path.read_text(encoding="utf-8-sig")) if path.is_file() else {} # Отсутствие проектной конфигурации не создаёт вымышленные ограничения памяти.
limit = settings.get("max_image_bytes") # Прежняя конфигурация 166 задаёт предел в физических октетах полного SCI8.
if limit is not None and (type(limit) is not int or limit < 24): # Не допускает отрицательную ёмкость, bool или размер меньше заголовка и терминатора SCI8.
raise ValueError("max_image_bytes must be an integer >= 24") # Не создаёт паспорт с непригодным проектным ограничением.
return {"max_image_bytes": limit} if limit is not None else {} # Старый предел EEPROM также применяется общим postbuild.
selected = numeric_define(project / config["selection_header"], config["selection_macro"]) # Читает только числовое определение, исключая исполнение текста заголовка.
variants = config["variants"] # Таблица связывает значение compiler define, название микросхемы и макрос физической ёмкости.
if str(selected) not in variants: # Неизвестное значение конфигурации не подменяется произвольным профилем.
raise ValueError("Unsupported EEPROM selection: " + str(selected)) # Ошибка возникает до компиляции и создания постоянного выпуска.
chosen = next((value for value in variants.values() if value["name"] == eeprom), None) if eeprom else variants[str(selected)] # CLI override сохраняет согласованный выбор отдельной тестовой сборки.
if chosen is None: # Неподдерживаемый профиль запрещён и при программном вызове общего модуля.
raise ValueError("Unsupported EEPROM profile: " + str(eeprom)) # Не создаёт паспорт с вымышленной ёмкостью.
layout = templates / config["layout_header"] # Разметка читается из фактически компилируемой зависимости, а не из другой копии templates.
total = numeric_define(layout, chosen["bytes_macro"]) # Ёмкость выражена физическими октетами EEPROM согласно общему C-контракту.
words = numeric_define(layout, config["param_words_macro"]) # Число параметров выражено 16-битными словами C28x и включает контрольную сумму.
limit = total - (words + 1) * 2 # Сохраняет параметры и последнее резервное слово; одно слово EEPROM занимает два октета.
if total <= 0 or total % 2 or words <= 0 or limit < 24: # Полная SCI8-таблица требует минимум 22 октета заголовка и два октета терминатора.
raise ValueError("Invalid EEPROM layout") # Не выпускает образ с отрицательной или неполной областью прошивки.
result = {"eeprom": chosen["name"], "eeprom_bytes": total, "eeprom_param_words": words, "max_image_bytes": limit, "ram_ranges": policy["ram_ranges"]} # Паспорт фиксирует аппаратный выбор и допустимые адреса приложения.
if can_profile and chosen["name"] == "M95M02": result["can_boot_profile"] = can_profile # Малая AT25256B не получает неподдерживаемый A/B-образ, даже если проект поддерживает второй аппаратный вариант.
return result # Prebuild сохраняет CAN-профиль, а postbuild проверяет его неизменность вместе с EEPROM и версией.
def load_identity(templates): # Загружает действующий генератор паспорта из явно выбранной библиотеки.
spec = importlib.util.spec_from_file_location("unified_identity_tools", templates / "c/firmware-info/tools/identity.py") # Обе схемы CCS используют одинаковый публичный контракт identity.
module = importlib.util.module_from_spec(spec) # Изолированный модуль не меняет импорты SETGUI и других проектов.
spec.loader.exec_module(module) # Ошибка отсутствующей зависимости возвращается вызывающему процессу до записи выпуска.
module.git_state = git_state # Локальное доверие Git ограничено выбранными рабочими деревьями без изменения глобального safe.directory.
return module # Подготовка и упаковка используют одну реализацию формата manifest.
def prepare(project, templates, output, eeprom=None, config=None): # Единый prebuild поддерживает исходный счётчик 166 и ручную версию 167.
project, templates, output = project.resolve(), templates.resolve(), output.resolve() # Все относительные пути фиксируются до запуска общих инструментов.
config = config.resolve() if config else project / "firmware_identity.json" # Старые BAT-вызовы могут явно передать прежний файл настроек.
settings = json.loads(config.read_text(encoding="utf-8-sig")) # Читает пользовательские поля без потери дополнительных настроек проекта.
policy, profile = read_policy(project), read_profile(project, templates, eeprom) # Физическая разметка проверяется до изменения счётчика версии.
identity = load_identity(templates) # Подключает генератор той библиотеки, которая включена в компиляцию.
if policy.get("version_header"): # Ручной заголовок остаётся единственным источником версии 167.
settings.update(version=configured_version(project, settings), increment_version=False) # Не увеличивает номер при повторном prebuild или ошибке компиляции.
with tempfile.TemporaryDirectory(prefix="c2000-identity-") as directory: # Временные настройки не переписывают пользовательский JSON.
temporary = Path(directory) / "config.json" # Временный файл нужен только публичному интерфейсу существующего генератора.
temporary.write_text(json.dumps(settings, ensure_ascii=False), encoding="utf-8") # Сохраняет ручной номер и все поля изделия в UTF-8.
metadata = identity.prepare(project, templates, temporary, output) # Генерирует прежние 12 слов паспорта без изменения протокола прошивки.
else: # Проект 166 продолжает резервировать следующий номер непосредственно в своём JSON.
metadata = identity.prepare(project, templates, config, output) # Итератор вызывается ровно один раз, сохраняя старый контракт precompile.
metadata.update(profile) # Предел EEPROM применяется и к старым проектам, и к новым аппаратным профилям.
if "eeprom_bytes" in profile: # Только явный аппаратный профиль требует сверки compiler define и происхождения разметки.
metadata["templates_root"] = str(templates) # Защищает postbuild от проверки по другой копии C-заголовков.
selection = policy["eeprom"] # Имена заголовка и макроса определены декларативным профилем платы.
value = next(key for key, item in selection["variants"].items() if item["name"] == profile["eeprom"]) # Вычисляет точное значение define для выбранной микросхемы.
guard = '\n#include "' + Path(selection["selection_header"]).name + '" /* Проверяет аппаратный профиль текущей компиляции. */\n' # Проверка включается в генерируемый C-заголовок паспорта.
guard += '#if ' + selection["selection_macro"] + ' != ' + value + ' /* Сверяет define компилятора с подготовленным паспортом. */\n#error EEPROM_profile_differs_from_prebuild /* Требует согласованную пересборку платы. */\n#endif /* Завершает проверку профиля EEPROM. */\n' # Compiler-only override не может незаметно изменить физическую разметку.
header = output / "firmware_identity.h" # Добавляет защиту после прежних констант версии и времени.
identity.write_atomic(header, header.read_text(encoding="utf-8") + guard) # Заголовок заменяется атомарно и всегда остаётся в UTF-8.
identity.write_atomic(output / "firmware_identity.json", json.dumps(metadata, ensure_ascii=False, indent=2) + "\n") # Сохраняет фактическую разметку для проверки после линковки.
return metadata # Публичный результат совместим с прежними host-тестами и проектными вызовами.
def convert(compiler, build_dir, artifact, metadata_path, stage, tools_templates): # Выполняет только конвертацию во временный каталог до общей проверки выпуска.
spec = importlib.util.spec_from_file_location("unified_c2000_converter", tools_templates / "c/firmware-info/tools/c2000_postbuild.py") # Использует единственную низкоуровневую реализацию hex2000 и сборки дополнительного Flash.
converter = importlib.util.module_from_spec(spec) # Изолирует импорт от старых командных точек входа и исключает рекурсивный postbuild.
sys.path.insert(0, str(tools_templates / "c/firmware-info/tools")) # Соседний identity нужен существующему конвертору при импорте.
try: # Любая ошибка конвертации остаётся внутри staged-выпуска.
spec.loader.exec_module(converter) # Загружает библиотечную функцию без запуска CLI.
source = Path(artifact) # CCS передаёт абсолютный базис OUT либо имя относительно активного BuildDirectory.
source = Path(str(source if source.is_absolute() else build_dir / source) + ".out") # Выбирает конкретный исходный файл текущей сборки.
with tempfile.TemporaryDirectory(prefix=".input-", dir=stage) as directory: # Дополнительные копии конвертора не попадают рядом с постоянным RAM OUT до проверки.
staged = Path(directory) / source.name # Имя артефакта сохраняется для штатного именования Flash и BIN.
shutil.copy2(source, staged) # Передаёт точные байты текущего COFF, сохраняя исходный файл.
converter.postbuild(compiler, build_dir, str(staged.with_suffix("")), metadata_path, stage) # Публикация и сеть отсутствуют: результаты ещё должны пройти проверки общего модуля.
finally: # Импорт другого проекта не должен наследовать временно добавленный каталог инструментов.
sys.path.pop(0) # Возвращает прежний порядок поиска Python-модулей даже при отказе TI.
def write_receipt(output, archive_name, image_name): # Передаёт Публикатору точную папку текущего проверенного выпуска.
value = {"schema": 1, "folder": archive_name, "manifest": image_name + ".manifest.json"} # Относительные имена сохраняют переносимость комплекта между компьютерами.
receipt = output / "latest_build.json" # Служебный указатель находится вне папки неизменяемого комплекта.
temporary = receipt.with_suffix(".tmp") # Атомарная замена не оставляет частичный JSON при прерывании записи.
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") # Записывает только локальный указатель без сетевой публикации.
temporary.replace(receipt) # Указатель меняется лишь после успешной проверки и копирования всех файлов.
def atomic_copy(source, destination): # Сохраняет проверенный файл без открытия старого результата на перезапись и без частичного содержимого.
destination = Path(destination) # Нормализует также строковые пути, переданные стандартным shutil.copytree.
with tempfile.NamedTemporaryFile(dir=destination.parent, prefix=".prepared-", delete=False) as stream: # Собственный временный файл наследует права каталога и находится на том же диске для атомарной замены.
temporary = Path(stream.name) # Запоминает только созданный текущим вызовом файл для безопасной очистки.
try: # Ошибка записи или замены должна оставить прежний результат пригодным для чтения.
shutil.copy2(source, temporary) # Сначала полностью записывает новые байты и сохраняет время исходного артефакта.
temporary.replace(destination) # Одной файловой операцией заменяет старый результат после успешной записи временного файла.
finally: # Незавершённый временный файл не должен попадать в будущий комплект Публикатора.
temporary.unlink(missing_ok=True) # Удаляет только собственный незавершённый файл; после успешной замены его уже нет.
return str(destination) # Сохраняет контракт copy_function стандартного shutil.copytree.
def verify_embedded(image, metadata, flash=False): # RAM и Flash SCI8 проверяются отдельно по разным допустимым адресам и ограничениям загрузчика.
data = image.read_bytes() # Проверяет содержимое файла, которое будет передано в EEPROM-программатор.
limit = 0xFC00 * 2 - 2 if flash else metadata.get("max_image_bytes", 0xFFFFFFFF) # RS Flash использует внешний буфер 0x200000..0x20FC00 и ещё два октета CRC; RAM BIN ограничен резервом физической EEPROM.
if not 24 <= len(data) <= limit: # Учитывает заголовки SCI8 вместе с кодом, не позволяя переписать резерв параметров либо выйти за буфер RS.
raise ValueError("SCI8 exceeds EEPROM firmware boundary or is too short") # Отвергает слишком большой BIN до создания постоянного каталога выпуска.
if data[:2] != b"\xaa\x08": # 0x08AA — обязательный ключ восьмибитной boot-таблицы TI.
raise ValueError("Invalid SCI8 header") # Не принимает HEX, COFF либо плоский дамп под видом загрузочного BIN.
stamp = datetime.fromisoformat(metadata["built_at_utc"].replace("Z", "+00:00")) # Берёт момент именно того prebuild, который подготовил C-заголовок.
build_id = metadata["build_id"] # Контракт firmware-info v1 хранит ровно восемь ASCII-символов идентификатора.
if not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", build_id) or stamp.utcoffset() is None or stamp.utcoffset().total_seconds(): # Исключает неоднозначную дату и неподдерживаемый идентификатор.
raise ValueError("Invalid prebuild identity") # Требует заново выполнить prebuild вместо выпуска чужих метаданных.
words = [1, *metadata["version"], stamp.year, stamp.month * 256 + stamp.day, stamp.hour * 256 + stamp.minute, stamp.second] # Независимо восстанавливает первые восемь 16-битных регистров общего контракта.
words += [ord(build_id[index]) * 256 + ord(build_id[index + 1]) for index in range(0, 8, 2)] # Упаковывает две ASCII-октеты в каждое из четырёх слов идентификатора.
expected = struct.pack("<12H", *words) # TI SCI8 хранит каждое слово полезных данных младшей октетой вперёд.
high, low = struct.unpack_from("<HH", data, 18) # Читает точку входа после ключа и восьми резервных слов загрузчика.
entry = (high << 16) | low # Адрес C28x передаётся старшим словом перед младшим.
offset, found, blocks, boot, loaded = 22, 0, [], 0, {} # Запоминает диапазоны, boot-переход и точные загружаемые слова для сопоставления Flash COFF с SCI8.
while offset + 2 <= len(data): # Перед чтением размера требует наличие полного 16-битного слова.
count, = struct.unpack_from("<H", data, offset) # Длина блока задана в адресуемых словах C28x, а не физических октетах.
offset += 2 # Пропускает размер перед адресом блока или терминатором таблицы.
if not count: # Нулевое слово завершает SCI8 без последующего адресного поля.
if offset != len(data) or found != 1 or (flash and boot != 1) or not any(start <= entry < end for start, end in blocks): # Дополнительно требует единственный двухсловный аппаратный переход Flash boot.
raise ValueError(f"SCI8 identity/entry/terminator mismatch: matching identities={found}") # Сообщает отсутствие встроенного паспорта вместо ложной подписи старого OUT.
if flash: validate_words(entry, loaded) # CRC FAB1 и границы сектора A проверяются по фактическим октетам SCI8 до публикации.
return entry, loaded # Flash-проверка сопоставит реальные загрузочные слова с секциями исходного COFF; RAM-вызов может игнорировать результат.
if offset + 4 + count * 2 > len(data): # Требует полный 32-битный адрес и все октеты полезной нагрузки.
raise ValueError("Truncated SCI8 section") # Не допускает выпуск усечённой секции даже при паспорте в её начале.
high, low = struct.unpack_from("<HH", data, offset) # Получает адрес назначения текущего блока в словах C28x.
address, end = (high << 16) | low, ((high << 16) | low) + count # Конец исключается из диапазона, поэтому соседние блоки могут соприкасаться.
allowed = (APP_START <= address < end <= APP_STOP) if flash else any(start <= address < end <= stop for start, stop in metadata.get("ram_ranges", ((0, 0x100000000),))) # Flash-приложение целиком находится ниже сектора A; RAM A/B по-прежнему ограничено своим payload-диапазоном.
if not allowed or any(address < old_end and start < end for start, old_end in blocks): # Любое перекрытие секций либо несовместимый тип таблицы блокирует выпуск.
raise ValueError("SCI8 block outside application RAM or overlapping") # Не выпускает Flash-таблицу как EEPROM/RAM-образ.
if len(blocks) >= metadata.get("max_sections", 0xFFFFFFFF): # CAN-порт F28335 принимает не более 64 секций; обычные RAM/Flash-профили сохраняют прежний контракт.
raise ValueError("SCI8 exceeds bootloader section count") # Совместимый размер файла не разрешает таблицу, которую аппаратный загрузчик не сможет разобрать.
blocks.append((address, end)) # Сохраняет проверенный диапазон для контроля следующих блоков и точки входа.
payload = data[offset + 4:offset + 4 + count * 2] # Ищет паспорт только внутри данных, исключая поля заголовка и адреса.
boot += int(flash and address == APP_START and count >= 16) # Ровно один блок содержит полный FAB1; аппаратный boot 0x33FFF6 запрещён.
loaded.update((address + index, value) for index, value in enumerate(struct.unpack("<" + "H" * count, payload))) # Фиксирует реальные 16-битные слова, которые RS-загрузчик запрограммирует во Flash.
found += sum(payload[index:index + 24] == expected for index in range(0, len(payload) - 23, 2)) # Учитывает лишь выровненные на слово экземпляры 12 регистров firmware-info.
offset += 4 + count * 2 # Переходит за полный адрес и полезные слова к следующему размеру.
raise ValueError("Missing SCI8 terminator") # Исчерпание файла без нулевого слова никогда не считается успешной конверсией.
def verify_package(image, metadata): # Связывает BIN, manifest и SHA-256 с тем же prebuild и аппаратной EEPROM.
manifest = json.loads(image.with_suffix(image.suffix + ".manifest.json").read_text(encoding="utf-8")) # Один контракт SHA/паспорта обслуживает отдельные RAM BIN, Flash COFF и Flash SCI8.
data = image.read_bytes() # Повторно считывает точные октеты перед постоянной публикацией.
digest = hashlib.sha256(data).hexdigest() # Вычисляет контрольную сумму независимо от общего seal.
if manifest.get("file") != image.name or manifest.get("size_bytes") != len(data) or manifest.get("sha256") != digest: # Требует согласованности имени, размера и содержимого файла.
raise ValueError("BIN does not match its manifest") # Не публикует повреждённый либо перепутанный комплект.
if any(manifest.get(key) != value for key, value in metadata.items()): # Проверяет также eeprom, физическую границу, версию и происхождение компилируемых templates.
raise ValueError("Manifest does not match prebuild identity/EEPROM") # Один лишь корректный SHA не разрешает паспорт другого аппаратного варианта.
if image.with_suffix(image.suffix + ".sha256").read_text(encoding="utf-8").strip() != f"{digest} {image.name}": # Общий sidecar содержит SHA и точное имя через два пробела.
raise ValueError("Invalid BIN SHA-256 sidecar") # Требует целостности всех трёх публикуемых файлов.
def verify_linker_profile(map_path, profile): # Сверяет аппаратную настройку Python с фактическими абсолютными символами линкера.
text = map_path.read_text(encoding="utf-8-sig") # MAP текущей конфигурации является обязательным результатом успешной линковки.
expected = {"_EEPROM_TOTAL_BYTES": profile["eeprom_bytes"], "_EEPROM_FIRMWARE_LIMIT_BYTES": profile["max_image_bytes"], "_EEPROM_PARAMS_START_WORD": profile["max_image_bytes"] // 2} # Символы разделяют физические октеты и адресуемые слова C28x.
for name, value in expected.items(): # Проверяет ёмкость, конец прошивки и начало сохраняемых параметров до запуска hex2000.
values = re.findall(r"^\s*(?:\w+\s+)?([0-9a-fA-F]{8})\s+" + re.escape(name) + r"\b", text, re.M) # Принимает формат глобальных TI-символов с номером страницы либо признаком ABS.
if not values or any(int(item, 16) != value for item in values): # Повторные списки MAP обязаны описывать один и тот же профиль.
raise ValueError(f"Linker EEPROM profile mismatch: {name}") # Выявляет define, переданный только компилятору или только линкеру, и устаревшую карту.
def verify_flash_coff(image, compiler): # Проверяет реальные секции COFF официальным TI-декодером и возвращает точные Flash-слова загрузки.
tool = compiler / "bin/ofd2000.exe" # Разбирает формат тем же установленным C2000 toolchain, который создал OUT.
document = ET.fromstring(subprocess.check_output([str(tool), "-x", str(image)], stderr=subprocess.STDOUT)) # XML содержит фактические адреса и двоичные данные секций, а не неподтверждённую соседнюю MAP.
coff = document.find(".//ti_coff") # Вариант предназначен строго для COFF ABI F28335.
if coff is None or coff.findtext("file_header/target_id") != "0x9d" or coff.findtext("file_header/exec") != "true": # Не принимает объект другого процессора, ELF либо незалинкованный модуль.
raise ValueError("Expected executable C2000 COFF Flash image") # Явно отделяет JTAG Flash OUT от обычного RAM OUT и чужих архитектур.
sections = {section.findtext("name"): section for section in coff.findall("section")} # Именованные выходные секции соответствуют отдельной Flash-карте линкера 167.
required = {".text": (APP_DATA, APP_STOP, APP_DATA, APP_STOP), ".flash_app": (APP_START, APP_DATA, APP_START, APP_DATA), "flash_rs": (0x8000, 0xC000, APP_DATA, APP_STOP), "ramfuncs": (0xC000, 0xD000, APP_DATA, APP_STOP)} # FAB1 находится в секторе H; все Flash LOAD-секции исключают постоянный сектор A, сохраняя прежние RUN-области RAM.
for name, (run_begin, run_end, load_begin, load_end) in required.items(): # Наличие автономного RAM-загрузчика и InitFlash обязательно для выпуска Flash-варианта.
section = sections.get(name) # Отсутствующая обязательная секция не маскируется другими именами или символами MAP.
if section is None: # Проверяет отдельную карту и startup, а не одно лишь расположение основного кода.
raise ValueError("Missing Flash COFF section: " + name) # Не выдаёт RAM COFF за пригодный для автономной Flash.
run, load, size = (int(section.findtext(field, "0"), 0) for field in ("physical_addr", "virtual_addr", "raw_data_size")) # Длина OFD задана физическими октетами, оба адреса — 16-битными словами CPU.
if not size or size % 2 or not run_begin <= run < run + size // 2 <= run_end or not load_begin <= load < load + size // 2 <= load_end or (name == ".flash_app" and size != 32): # FAB1 занимает ровно 16 слов; прочие секции целиком помещаются в разрешённых LOAD/RUN-областях ниже сектора A.
raise ValueError("Invalid Flash COFF LOAD/RUN section: " + name) # Отвергает stale linker, размещение API во Flash во время стирания и аппаратный вход вне 0x33FFF6.
loaded = {} # Сопоставляет слова всех загружаемых Flash-секций с будущей SCI8-таблицей RS.
for section in sections.values(): # Служебные debug/copy-секции адреса ноль в программирование не входят.
address = int(section.findtext("virtual_addr", "0"), 0) # Для COFF TI это загрузочный адрес содержимого секции.
raw = section.findtext("raw_data") # Пустая .ebss, NOLOAD или DSECT не задаёт байтов прошивки.
if raw is None or not 0x300000 <= address < 0x340000: # Не включает отладочные записи, ROM-таблицы и рабочую RAM в образ Flash.
continue # Права оставшихся загружаемых Flash-слов дополнительно проверит общий SCI8-валидатор.
data = base64.b64decode(raw) # Получает именно те октеты, которые записаны в COFF и должны оказаться в Flash SCI8.
if len(data) % 2: # Любая программируемая секция C28x должна содержать целые 16-битные слова.
raise ValueError("Odd Flash COFF section size") # Не допускает неоднозначную упаковку образа RS.
for index, value in enumerate(struct.unpack("<" + "H" * (len(data) // 2), data)): # Восстанавливает little-endian представление каждого физического слова COFF.
if address + index in loaded: # Два выходных диапазона не должны программировать одно слово повторно.
raise ValueError("Overlapping Flash COFF load sections") # Проверяет реальный OUT независимо от доверия к линкерной карте.
loaded[address + index] = value # Сохраняет ожидаемые данные для независимого сравнения с hex2000.
entry = int(coff.findtext("optional_file_header/entry_point", "0"), 0) # Получает реальную точку входа C runtime из заголовка COFF.
if entry not in loaded or not 0x300000 <= entry < 0x33FF80: # C startup должен находиться в полностью загружаемой исполняемой Flash.
raise ValueError("Flash COFF entry is not loaded") # Исключает OUT со старой RAM-точкой входа или отрезанным startup.
checked_entry, checked_loaded, _ = coff_sections(document) # Общий декодер дополнительно отвергает любой загружаемый RAW вне области приложения, включая OTP и RAM.
if checked_entry != entry or checked_loaded != loaded: raise ValueError('Flash COFF load data mismatch') # Ни одна секция не должна быть потеряна при независимом обходе OFD XML.
validate_words(entry, loaded) # Проверяет FAB1 и CRC по полному согласованному COFF-образу.
return entry, loaded # Полное сравнение с SCI8 обнаружит конверсию другого OUT, потерю секции или подмену одного слова.
def verify_flash_release(stage, artifact, metadata, compiler, identity, required=False): # Проверяет и отдельно маркирует Flash COFF/SCI8 в staged-выпуске до любого постоянного копирования.
base = Path(artifact.replace("\\", "/")).name # Использует исходное имя артефакта CCS без директории и расширения OUT.
coff, serial = stage / (base + "_flash.out"), stage / (base + "_flash_rs.bin") # JTAG COFF и RS SCI8 являются разными формами одного отдельного Flash-варианта.
if not coff.exists() and not serial.exists() and not required: # Проекты без build_flash.py и host-фикстуры сохраняют прежний защищённый выпуск RAM.
return # Наличие обычного RAM BIN само по себе не объявляет поддержку Flash.
if not coff.is_file() or not serial.is_file(): # Сборка Flash должна выпустить оба согласованных артефакта.
raise ValueError("Incomplete Flash COFF/SCI8 release") # Не экспортирует один старый OUT вместе с новым RAM-выпуском.
verify_linker_profile(coff.with_suffix(".map"), metadata) # Flash использует физическую EEPROM для параметров и не меняет её профиль при размещении кода внутри CPU.
if verify_flash_coff(coff, compiler) != verify_embedded(serial, metadata, flash=True): # Сравнивает entry и каждое загрузочное слово официального COFF с результатом hex2000.
raise ValueError("Flash SCI8 does not match its COFF") # Отклоняет stale конверсию даже при правильных независимых SHA-файлах.
pairs = [(path, "COFF") for path in stage.rglob(coff.name)] + [(path, "SCI8-FLASH") for path in stage.rglob(serial.name)] # Включает плоские результаты и все их копии внутри текущего архива.
for path, kind in pairs: # Сначала проверяет весь исходный комплект, не меняя даже временных паспортов при первом отказе.
verify_package(path, metadata) # Общий postcompile обязан сохранить текущие version/EEPROM/SHA для каждого результата.
if path.read_bytes() != (coff if kind == "COFF" else serial).read_bytes(): # Архивированная копия должна быть побайтно тем же проверенным вариантом.
raise ValueError("Flash archive differs from staged release") # Не позволяет экспортировать чужой Flash внутри корректного RAM-архива.
with tempfile.TemporaryDirectory(prefix=".variant-", dir=stage) as directory: # Вариантные JSON нужны только для подписи разных форматов и не попадают в постоянный выпуск.
metadata_path = Path(directory) / "metadata.json" # Общий seal принимает обычный путь и не требует изменения соседнего templates.
for path, kind in pairs: # Только проверенные Flash-файлы получают явный признак исполнения и транспортный формат.
specific = dict(metadata, execution="FLASH", image_format=kind, boot_contract="f28335-universal-v1", boot_protected=True, flash_app_start=APP_START, flash_app_stop=APP_STOP) # Паспорт явно отличает защищённое приложение от старого самостоятельного Flash; аппаратная EEPROM сохраняется отдельно.
metadata_path.write_text(json.dumps(specific, ensure_ascii=False), encoding="utf-8") # Сохраняет все поля происхождения и ручную версию из общего prebuild.
identity.seal(path, metadata_path) # Переподписывает конкретный COFF либо SCI8 с явным execution=FLASH только после проверки байтов.
verify_package(path, specific) # Подтверждает согласованность готового вариантного manifest и SHA перед публикацией.
def create_can_release(stage, image, metadata, map_path, identity): # Добавляет отдельный CAN BIN только после проверки текущего RAM-образа и его интеграции с загрузчиком.
if not metadata.get("can_boot_profile"): return None # Старые проекты и AT25256B продолжают выпускаться без дополнительного неподдерживаемого файла.
if metadata["can_boot_profile"] != "f28335-m95m02-ab" or metadata.get("target") != "TMS320F28335" or metadata.get("eeprom") != "M95M02": # Профиль связан с конкретными процессором, памятью и A/B-портом.
raise ValueError("CAN profile requires TMS320F28335 with M95M02") # Запрещает переименование произвольного RAM либо Flash BIN в пригодный CAN-образ.
limits = dict(metadata, max_image_bytes=130560, ram_ranges=[[0x8000, 0xE000]], max_sections=64) # Ограничения взяты из f28335_boot_store: длина в октетах, адреса в 16-битных словах CPU.
_entry, loaded = verify_embedded(image, limits) # Проверяет SCI8, паспорт, entry, все секции, перекрытия и защиту RAM загрузчика.
symbols = map_path.read_text(encoding="utf-8", errors="replace") # Использует MAP именно активной линковки, уже проверенный по профилю EEPROM.
for name in ("_F28335CanBoot_Init", "_F28335CanBoot_Ready", "_F28335CanBoot_Service", "_F28335CanBoot_RxISR"): # CAN-приложение должно содержать инициализацию, готовность, основной сервис и обработчик приёма.
matches = re.findall(r"^\s*\d+\s+([0-9a-fA-F]{8})\s+" + re.escape(name) + r"\s*$", symbols, re.M) # Читает адреса экспортированных TI-символов, исключая простые упоминания имени в описании секции.
if not matches or any(int(address, 16) not in loaded for address in matches): # Требует присутствие hook в загружаемом payload текущего BIN, а не только в произвольном MAP.
raise ValueError("CAN application hook missing from SCI8/MAP: " + name) # Старое приложение без READY/CONFIRM нельзя выпускать как автоматически пригодное для CAN A/B.
specific = dict(metadata, execution="RAM", image_format="SCI8-RAM", transport="can", protocol="ProtoCAN Boot", product_type=0x2833, hardware_min=1, hardware_max=1) # Паспорт отделяет CAN от RS Flash и сохраняет прежние ручную версию, SHA и аппаратную идентичность.
can_image = image.with_name(image.stem + "_can.bin") # Отдельный суффикс однозначно показывает назначение файла, сохраняя имя артефакта и Build ID.
can_image.write_bytes(image.read_bytes()) # По CAN передаётся та же полная SCI8-таблица RAM без нового заголовка или изменения адресов секций.
with tempfile.TemporaryDirectory(prefix=".can-metadata-", dir=stage) as directory: # Вариантный паспорт не подменяет исходные метаданные компиляции.
metadata_path = Path(directory) / "metadata.json" # Общий seal читает отдельный временный JSON для конкретного транспортного файла.
metadata_path.write_text(json.dumps(specific, ensure_ascii=False), encoding="utf-8") # Русские поля пользовательского паспорта сохраняются в UTF-8.
identity.seal(can_image, metadata_path) # Создаёт собственные manifest и SHA-256 с точным именем CAN BIN.
verify_package(can_image, specific) # До публикации сверяет все поля нового паспорта и точные байты файла.
archive = stage / identity.build_folder_name(metadata) # Дополняет тот же архив текущей сборки, который уже содержит обычный RAM и варианты Flash.
if not archive.is_dir(): raise ValueError("CAN release requires the current build archive") # Не создаёт отдельный неполный выпуск, который не попадёт в штатный экспорт GUI.
for file in (can_image, can_image.with_suffix(".bin.manifest.json"), can_image.with_suffix(".bin.sha256")): # Все три файла должны одновременно попасть в плоский выпуск и архив.
shutil.copy2(file, archive / file.name) # Копирование ещё выполняется внутри временного staged-комплекта без изменения предыдущих выпусков.
verify_package(archive / can_image.name, specific) # Проверяет архивную копию до копирования в output и каталог GUI.
return can_image # Вызывающий выводит точное имя только после успешного завершения всего защищённого postbuild.
def arrange_release(stage, archive_name, artifact, image_name, can_image): # Разделяет уже проверенный комплект по способу программирования в текущем выпуске и его архиве.
base = Path(artifact.replace("\\", "/")).name # Использует базовое имя CCS, включая случай абсолютного BuildArtifactFileBaseName.
routes = {base + "_flash.out": "flash", base + "_flash.map": "flash", base + "_flash_rs.bin": "rs"} # OUT и карта относятся к JTAG, а отдельный SCI8 Flash BIN передаётся последовательным загрузчиком.
if can_image is not None: routes[can_image.name] = "can" # CAN получает только вариант, прошедший ограничения A/B-слота и проверку прикладных hooks.
shutil.copy2(stage / archive_name / "firmware_identity.json", stage / "firmware_identity.json") # Общий паспорт связывает четыре папки как в текущем локальном выпуске CCS, так и в его неизменяемом архиве.
for folder in (stage / archive_name, stage): # Сначала перестраивает архив, затем плоские результаты того же временного выпуска.
for file in tuple(folder.iterdir()): # Снимок списка исключает повторную обработку создаваемых папок назначения.
if not file.is_file() or file.name == "firmware_identity.json": continue # Архив и исходный общий паспорт остаются на своём уровне без дублирования между назначениями.
purpose = next((route for name, route in routes.items() if file.name == name or file.name.startswith(name + ".")), "eeprom") # Sidecar-паспорта, SHA и карты следуют за точным именем своего образа; прочие RAM/HEX-файлы сопровождают EEPROM.
destination = folder / purpose # Одинаковые четыре назначения применяются к локальному CCS-выпуску и экспортируемому архиву.
destination.mkdir(exist_ok=True) # Пустая папка неподдержанного CAN/Flash не создаётся и не обещает отсутствующего образа.
file.replace(destination / file.name) # Перемещает только внутри временного stage, не затрагивая ранее опубликованные файлы.
return (Path("eeprom") / image_name).as_posix() # Переносимый относительный путь сохраняется в receipt и используется штатным экспортом identity.
def publish(project, templates, tools_templates, compiler, build_dir, artifact, map_path, output, export=None, eeprom=None): # Выпускает только проверенные результаты конкретной конфигурации CCS.
project, templates, tools_templates = project.resolve(), templates.resolve(), tools_templates.resolve() # Фиксирует выбранные исходники и инструменты независимо от рабочего каталога терминала.
build_dir, compiler = build_dir.resolve(), compiler.resolve() # Общая BAT-точка получает полные пути текущих компилятора и Debug/Release.
metadata_path = build_dir / "generated/firmware_identity.json" # Использует паспорт, подготовленный до компиляции этой конфигурации.
metadata = json.loads(metadata_path.read_text(encoding="utf-8")) # Отсутствующий prebuild останавливает работу до создания выпуска.
profile = read_profile(project, templates, eeprom) # Повторно читает текущий выбор EEPROM и общий резерв параметров.
settings = json.loads((project / "firmware_identity.json").read_text(encoding="utf-8-sig")) if (project / "firmware_identity.json").is_file() else metadata # Сверяет product и target с действующим проектом.
expected = {"product": settings["product"], "target": settings["target"], "version": configured_version(project, settings), **({"templates_root": str(templates)} if "eeprom_bytes" in profile else {}), **profile} # Ручной номер и аппаратный вариант обязаны совпадать с prebuild.
if any(metadata.get(key) != value for key, value in expected.items()) or metadata.get("can_boot_profile") != profile.get("can_boot_profile"): # Включение и отключение CAN после prebuild также требуют пересборки, как изменение версии, EEPROM или зависимости.
raise ValueError("Configuration changed: run prebuild and rebuild before publishing") # Требует пересборку вместо подмены старых метаданных.
verify_linker_profile(map_path.resolve(), profile) if "eeprom_bytes" in profile else None # Проверяет аппаратный выбор линкера, который нельзя вывести только из Python-конфигурации.
with tempfile.TemporaryDirectory(prefix=".verified-release-", dir=build_dir) as directory: # Единственное место, куда общий postcompile пишет до подтверждения пригодности BIN.
stage = Path(directory) # Изолирует все архивы и промежуточные HEX от постоянных каталогов выпуска.
convert(compiler, build_dir, artifact, metadata_path, stage, tools_templates) # Любая ошибка TI или общей библиотеки прерывает публикацию.
image_name = Path(artifact.replace("\\", "/")).name + "_" + metadata["build_id"] + ".bin" # Повторяет штатное имя конкретного образа, не сканируя старые BIN.
image = stage / image_name # Проверяет только результат текущего вызова конвертора.
verify_embedded(image, metadata) # Обязательные проверки EEPROM, полной SCI8-таблицы и реально встроенных 12 слов.
verify_package(image, metadata) # Сверяет SHA и весь паспорт, включая физический профиль EEPROM.
for archived in stage.rglob(image_name): # Проверяет также копии внутри текущего архива перед их публикацией или export.
verify_package(archived, metadata) # Архив обязан содержать те же байты и профиль, что плоский выпуск.
spec = importlib.util.spec_from_file_location("release_identity_tools", tools_templates / "c/firmware-info/tools/identity.py") # Подключает существующий формат именования и экспорта архива без копирования реализации.
identity = importlib.util.module_from_spec(spec) # Создаёт отдельный модуль инструментов, не подменяя компилируемую зависимость паспорта.
spec.loader.exec_module(identity) # Ошибка подключения остаётся внутри временного этапа до постоянных файлов.
verify_flash_release(stage, artifact, metadata, compiler, identity, required=(project / "build_flash.py").is_file()) # Отдельная Flash-сборка требует собственного безопасного COFF/SCI8 и не ослабляет прежние RAM-проверки.
can_image = create_can_release(stage, image, metadata, map_path, identity) # Автоматически создаёт и проверяет CAN-вариант до переноса любого результата в постоянный выпуск.
archive_name = identity.build_folder_name(metadata) # Сохраняет принятые имена архивов с ревизией, признаком изменений и временем UTC+3.
if not (stage / archive_name / image_name).is_file(): # Полный архив является обязательным результатом общей конверсии, а не необязательной старой папкой.
raise ValueError("Common postcompile did not produce the current build archive") # Не публикует неполный комплект без OUT, MAP и паспорта текущего выпуска.
image_name = arrange_release(stage, archive_name, artifact, image_name, can_image) # Общая раскладка выполняется после всех проверок RAM/Flash/CAN и до постоянного копирования.
output = output.resolve() # Постоянный каталог появляется только после всех проверок staged-комплекта.
output.mkdir(parents=True, exist_ok=True) # Прежние проверенные выпуски сохраняются при добавлении нового.
for item in sorted(stage.iterdir(), key=lambda item: item.is_dir()): # Переносит только файлы и архив, созданные данным postcompile.
if item.is_dir(): # Архив содержит сопроводительные результаты той же сборки.
shutil.copytree(item, output / item.name, dirs_exist_ok=True, copy_function=atomic_copy) # Сохраняет только проверенный архив с атомарной заменой файлов при повторном выпуске той же компиляции.
else: # Общие служебные файлы, если они появятся, сохраняются в корне текущего выпуска.
atomic_copy(item, output / item.name) # Не открывает старые файлы другой ветки на перезапись и не оставляет частичный BIN/HEX при отказе записи.
verify_package(output / image_name, metadata) # Подтверждает отсутствие повреждения при копировании на постоянный диск.
if export is not None: # Копирование в каталог GUI выполняется последним и лишь для прошедшего проверки архива.
identity.export_build(output / archive_name, export.resolve(), image_name) # Использует общий экспорт уже проверенного локального выпуска.
print(f"[firmware-build] Verified {profile.get('eeprom', settings['target'])} BIN: {output / image_name}") # Показывает пригодный аппаратный профиль и точный путь конечного файла.
if can_image is not None: print(f"[firmware-build] CAN BIN: {output / 'can' / can_image.name}") # После успешного выпуска показывает файл из отдельной папки ProtoCAN.
write_receipt(output, archive_name, image_name) # Фиксирует точную папку выпуска для импорта Публикатором без поиска по времени.
return output / image_name # Даёт тестам и внешним средствам однозначный результат без поиска по времени.
def main(argv=None): # Общая точка входа CCS и старых BAT: подготовка и проверка не выполняют сетевую публикацию.
parser = argparse.ArgumentParser(description="Единая подготовка C2000 для CCS и Публикатора SETGUI") # Показывает назначение обеих команд пользователю терминала.
parser.add_argument("command", choices=("prepare", "build")) # Разделяет резервирование версии до компиляции и проверку готового OUT после неё.
for name in ("project", "templates", "tools-templates", "config", "compiler", "build-dir", "metadata", "map", "output", "export"): # Все пути передаются явно или выводятся из публичных соглашений CCS.
parser.add_argument("--" + name, type=Path) # Path исключает интерпретацию аргументов через командную оболочку.
parser.add_argument("--artifact") # Базовое имя OUT передаётся без расширения как штатный макрос CCS.
parser.add_argument("--eeprom", choices=("AT25256B", "M95M02")) # Сохраняет существующий явный выбор тестового аппаратного профиля.
args = parser.parse_args(argv) # Ошибки аргументов завершают процесс до любых изменений версии или файлов выпуска.
tools = Path(__file__).resolve().parents[3] # В полной библиотеке модуль расположен в c/firmware-info/tools.
project = (args.project or (args.build_dir.parent if args.build_dir else Path.cwd())).resolve() # Старый postcompile определяет проект по родителю Debug/Release.
config = args.config or project / "firmware_identity.json" # Старый JSON 166 остаётся совместимым без обязательной миграции.
if args.command == "build" and not config.is_file() and args.build_dir: # Прямой postcompile допускает подготовленный паспорт без отдельного пользовательского JSON.
config = args.metadata or args.build_dir / "generated/firmware_identity.json" # В таком случае доступные проверки определяются реальными метаданными компиляции.
settings = json.loads(config.read_text(encoding="utf-8-sig")) # Изделие и зависимости определяются настройками проекта, а не текущим каталогом.
templates = args.templates or (config.parent / settings["templates"] if settings.get("templates") else tools) # Поддерживает относительный путь общей библиотеки старых проектов.
if args.command == "build" and not args.templates and args.build_dir: # Старый BAT postcompile не передаёт отдельный путь компилируемых исходников templates.
previous = json.loads((args.metadata or args.build_dir / "generated/firmware_identity.json").read_text(encoding="utf-8")) # Использует точный путь, зафиксированный prebuild активной конфигурации.
templates = Path(previous.get("templates_root", templates)) # Проверки EEPROM читают те же заголовки даже при отдельном полном checkout инструментов.
try: # Возвращает CCS ненулевой код при отказе любой проверки.
if args.command == "prepare": # Prebuild вызывается ровно один раз перед компиляцией паспорта.
prepare(project, templates, args.output or args.build_dir / "generated", args.eeprom, config) # При ручной версии исходный JSON не меняется.
else: # Postbuild получает уже подготовленный паспорт текущей конфигурации.
if not args.compiler or not args.build_dir or not args.artifact: # Нельзя подменить отсутствующий артефакт произвольным старым файлом из каталога.
parser.error("build requires --compiler, --build-dir and --artifact") # Показывает обязательные параметры до запуска TI.
metadata = args.build_dir / "generated/firmware_identity.json" # Основное расположение совпадает с прежними конфигурациями CCS.
if args.metadata and args.metadata.resolve() != metadata.resolve(): # Защищённый выпуск использует только паспорт активной конфигурации.
raise ValueError("metadata must belong to the selected build directory") # Не принимает паспорт другой сборки с совпадающим номером версии.
image = Path(args.artifact) # Путь карты по умолчанию находится рядом с выбранным OUT.
image = image if image.is_absolute() else args.build_dir / image # Относительные артефакты разрешаются от BuildDirectory.
publish(project, templates, args.tools_templates or tools, args.compiler, args.build_dir, args.artifact, args.map or image.with_suffix(".map"), args.output or args.build_dir / "firmware", args.export, args.eeprom) # Подготовленный комплект передаётся Публикатору отдельно от сетевых действий.
except (OSError, ValueError, KeyError, TypeError, struct.error, subprocess.CalledProcessError) as error: # Ошибки формата, зависимостей и компилятора не скрываются успешным завершением сборки.
parser.exit(1, f"[firmware-build] ERROR: {error}\n") # Краткая причина остаётся видна в Console CCS.
return 0 # Успех подтверждает подготовку проверенного локального комплекта.
if __name__ == "__main__": # Импорт из проектного адаптера или тестов не запускает сборку.
raise SystemExit(main()) # Возвращает командной оболочке фактический результат подготовки.