diff --git a/c/firmware-info/tests/test_c2000_build.py b/c/firmware-info/tests/test_c2000_build.py new file mode 100644 index 0000000..58bb5a7 --- /dev/null +++ b/c/firmware-info/tests/test_c2000_build.py @@ -0,0 +1,30 @@ +import importlib.util # Загружает общий модуль подготовки непосредственно из проверяемой копии templates. +import json # Проверяет сохранение пользовательских полей при резервировании версии 166. +from pathlib import Path # Все файлы теста находятся в изолированном временном каталоге. +import tempfile # Исключает запись версий и паспортов в рабочие прошивки пользователя. +import unittest # Запускает проверки без зависимостей SETGUI и Qt. + +TOOLS = Path(__file__).resolve().parents[1] / "tools" # Тест переносится вместе с c/firmware-info в полный checkout templates. +SPEC = importlib.util.spec_from_file_location("checked_c2000_build", TOOLS / "c2000_build.py") # Выбирает одну реализацию общего процесса. +BUILD = importlib.util.module_from_spec(SPEC) # Импорт не выполняет prebuild и не меняет счётчики версии. +SPEC.loader.exec_module(BUILD) # Проверяет реальный модуль, а не копию функций внутри теста. + + +class LegacyVersionTests(unittest.TestCase): # Подтверждает совместимость общей подготовки с JSON-конфигурацией 166. + def test_json_counter_increments_once_and_keeps_settings(self): # Перенос в templates не должен удваивать резервирование версии. + with tempfile.TemporaryDirectory() as temporary: # Версия настоящего проекта 166 остаётся неизменной. + project = Path(temporary) # Минимальный проект использует прежние настройки без нового аппаратного профиля. + settings = {"product": "Balsam-166-PM35", "target": "TMS320F28335", "version": [1, 0, 999], "increment_version": True, "max_image_bytes": 32508, "copy_to": "../Bin"} # Patch 999 проверяет перенос в minor по общему контракту версии. + config = project / "firmware_identity.json" # Сохраняемый счётчик расположен в том же JSON, что в ветке 166. + config.write_text(json.dumps(settings), encoding="utf-8") # Русские данные и настройки сохраняются как UTF-8. + templates = TOOLS.parents[2] # Полный путь templates восстанавливается из c/firmware-info/tools. + metadata = BUILD.prepare(project, templates, project / "Debug/generated") # Выполняет настоящий общий prebuild без компиляции и сети. + saved = json.loads(config.read_text(encoding="utf-8")) # Проверяет именно сохранённую версию, а не только возвращённые метаданные. + self.assertEqual(saved["version"], [1, 1, 0]) # Номер увеличен один раз с переносом patch в minor. + self.assertEqual(metadata["version"], saved["version"]) # Паспорт и сохраняемый счётчик описывают одну попытку компиляции. + self.assertEqual(metadata["max_image_bytes"], 32508) # Историческая граница EEPROM 166 остаётся обязательной для postbuild. + self.assertEqual(saved["copy_to"], settings["copy_to"]) # Общий генератор сохраняет пользовательские поля, даже если сеть выполняет Публикатор. + + +if __name__ == "__main__": # Импорт набора проверок не запускает их повторно. + unittest.main() # Позволяет проверить совместимость прямой командой Python. diff --git a/c/firmware-info/tools/c2000_build.py b/c/firmware-info/tools/c2000_build.py new file mode 100644 index 0000000..f175618 --- /dev/null +++ b/c/firmware-info/tools/c2000_build.py @@ -0,0 +1,436 @@ +"""Единая подготовка паспорта и проверенного 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(" len(data): # Требует полный 32-битный адрес и все октеты полезной нагрузки. + raise ValueError("Truncated SCI8 section") # Не допускает выпуск усечённой секции даже при паспорте в её начале. + high, low = struct.unpack_from("= 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()) # Возвращает командной оболочке фактический результат подготовки. diff --git a/tools/firmware-build/ccs/postcompile.bat b/tools/firmware-build/ccs/postcompile.bat index 04265f1..14c730c 100644 --- a/tools/firmware-build/ccs/postcompile.bat +++ b/tools/firmware-build/ccs/postcompile.bat @@ -18,10 +18,12 @@ if "%~5"=="" goto usage if "%~6"=="" goto usage if not "%~8"=="" goto usage if "%~7"=="" goto identity_local -python "%~dp0..\..\..\c\firmware-info\tools\c2000_postbuild.py" --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" --export "%~f7" +rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI. +python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" --export "%~f7" exit /b %errorlevel% :identity_local -python "%~dp0..\..\..\c\firmware-info\tools\c2000_postbuild.py" --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" +rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI. +python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" exit /b %errorlevel% :usage echo Usage: postcompile.bat --identity "compiler-root" "build-dir" "artifact-name" "generated-metadata.json" "output-dir" ["export-dir"] 1>&2 diff --git a/tools/firmware-build/ccs/precompile.bat b/tools/firmware-build/ccs/precompile.bat index 163f2b2..b45ea9a 100644 --- a/tools/firmware-build/ccs/precompile.bat +++ b/tools/firmware-build/ccs/precompile.bat @@ -23,7 +23,8 @@ if not exist "%~f3" ( echo [firmware-build] Created configuration: "%~f3" echo [firmware-build] Edit product, target and version in this file, then rebuild. ) -python "%~dp0..\..\..\c\firmware-info\tools\identity.py" prepare --repository "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4" +rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI. +python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" prepare --project "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4" exit /b %errorlevel% :usage echo Usage: precompile.bat --identity "project-root" "firmware_identity.json" "generated-dir" 1>&2