diff --git a/UKSSTMS320F28335/README.md b/UKSSTMS320F28335/README.md index 6ecff92..8aa6d45 100644 --- a/UKSSTMS320F28335/README.md +++ b/UKSSTMS320F28335/README.md @@ -145,6 +145,12 @@ manifest, SHA-256, HEX и карту конверсии в `K:\git_project\28335 После 255.255.999 сценарий выдаёт ошибку. Clean номер не сбрасывает. Одновременные сборки с одним JSON запускать нельзя. +Итератор реализован в общем `templates/c/firmware-info/tools/identity.py`. +Он работает и при прямом вызове `templates/tools/firmware-build/ccs/precompile.bat +--identity`, который настроен в Debug, и через `firmware_build.bat prepare`. +Проектная обёртка передаёт настройки в templates без повторного увеличения. +Без поля `increment_version` либо при `false` сохраняется ручная версия. + Build ID состоит из восьми символов: восемь символов ревизии либо семь символов и `+` при изменениях исходников или templates. При недоступном Git используется `NOGIT000`; ревизии `source` и `templates` проверяйте в manifest. diff --git a/UKSSTMS320F28335/build_identity.py b/UKSSTMS320F28335/build_identity.py index 419f914..9118b58 100644 --- a/UKSSTMS320F28335/build_identity.py +++ b/UKSSTMS320F28335/build_identity.py @@ -7,7 +7,7 @@ from pathlib import Path # Разрешает относительные пут import shutil # Переносит только прошедшие проверку файлы текущей сборки. import struct # Читает загрузочные секции SCI8 как 16-битные слова C28x. import subprocess # Запускает публичные BAT общей библиотеки и передаёт ошибки вызывающей сборке. -import tempfile # Изолирует конверсию и обеспечивает атомарную замену файла счётчика. +import tempfile # Изолирует результаты конверсии до проверки паспорта и контрольных файлов; счётчик теперь сохраняет общий генератор templates. def read_settings(config): # Проверяет настройки до изменения счётчика и запуска общих шагов. @@ -32,30 +32,12 @@ def config_path(config, value): # Делает размещение templates return (config.parent / value).resolve() # Абсолютный путь остаётся абсолютным, относительный отсчитывается от JSON. -def next_version(version): # Вычисляет следующий номер по пределам общего контракта паспорта. - result = list(version) # Не меняет исходные настройки до успешной проверки переполнения. - for index, limit in ((2, 999), (1, 255), (0, 255)): # Переносит единицу из patch в minor, затем в major. - if result[index] < limit: # Увеличивает первое поле, ещё не достигшее своего предела. - result[index] += 1 # Резервирует один следующий номер для очередного запуска prebuild. - return result # Старшие поля сохраняются, младшие уже обнулены при переносе. - result[index] = 0 # Сбрасывает переполненное поле перед увеличением следующего. - raise ValueError("Firmware version exhausted at 255.255.999") # Запрещает повторное использование номеров после полного переполнения. - - -def prepare(config, build_dir): # Увеличивает сохраняемую версию и вызывает штатный precompile templates. +def prepare(config, build_dir): # Передаёт резервирование версии и генерацию паспорта штатному precompile templates без повторного увеличения номера. settings = read_settings(config) # Сначала проверяет настройки, чтобы ошибочный JSON не занимал номер сборки. script = config_path(config, settings["templates"]) / "tools/firmware-build/ccs/precompile.bat" # Выбирает публичную точку входа общей зависимости из JSON. if not script.is_file(): raise FileNotFoundError(script) # Отсутствие templates останавливает шаг до изменения счётчика. - if settings.get("increment_version", False): # Проект включает автоматический итератор явным флагом в JSON. - settings["version"] = next_version(settings["version"]) # Получает следующий номер без отдельного C-заголовка. - with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=config.parent, prefix=".firmware-version-", delete=False) as stream: # Создаёт UTF-8 файл на том же диске для атомарной замены настроек. - temporary = Path(stream.name) # Запоминает только собственный временный файл для очистки при отказе. - try: stream.write(json.dumps(settings, ensure_ascii=False, indent=2) + "\n") # Сохраняет все поля проекта вместе с новым номером версии. - except BaseException: stream.close(); temporary.unlink(); raise # При ошибке записи сохраняет исходный JSON и удаляет незавершённый файл. - try: temporary.replace(config) # Фиксирует занятый номер до запуска генератора и компилятора. - finally: temporary.unlink(missing_ok=True) # Не оставляет временный файл при отказе замены конфигурации. output = build_dir / "generated" # Каждая конфигурация CCS получает отдельные заголовок и метаданные. - subprocess.run(["cmd.exe", "/d", "/c", "call", str(script), "--identity", str(config.parent), str(config), str(output)], check=True) # Общий precompile создаёт заголовок, UTC и Git-метаданные; ошибка останавливает сборку. + subprocess.run(["cmd.exe", "/d", "/c", "call", str(script), "--identity", str(config.parent), str(config), str(output)], check=True) # Общий precompile сам увеличивает версию при increment_version=true и создаёт единый паспорт для компиляции; ошибка останавливает сборку. return json.loads((output / "firmware_identity.json").read_text(encoding="utf-8")) # Возвращает фактический паспорт без повторного вычисления времени или Build ID. diff --git a/UKSSTMS320F28335/firmware_identity.json b/UKSSTMS320F28335/firmware_identity.json index 283fb0c..68899f7 100644 --- a/UKSSTMS320F28335/firmware_identity.json +++ b/UKSSTMS320F28335/firmware_identity.json @@ -4,7 +4,7 @@ "version": [ 1, 0, - 4 + 5 ], "increment_version": true, "templates": "../../newProject/templates", diff --git a/tests/firmware_identity/test_build_steps.py b/tests/firmware_identity/test_build_steps.py index 53e71ba..d99075f 100644 --- a/tests/firmware_identity/test_build_steps.py +++ b/tests/firmware_identity/test_build_steps.py @@ -81,16 +81,20 @@ class BuildStepsTest(unittest.TestCase): # Проверяет контракт def test_carry_and_exhaustion(self): # Проверяет обе границы переноса и запрет переполнения полного номера. for previous, expected in (([1, 9, 999], [1, 10, 0]), ([1, 255, 999], [2, 0, 0])): # Эталонные значения заданы независимо от production-счётчика. - self.assertEqual(builder.next_version(previous), expected) # Проверяет перенос из patch в minor и из minor в major. + self.settings["version"] = list(previous) # Передаёт граничный номер общему итератору через штатный JSON проекта. + self.save_settings() # Сохраняет исходный номер только во временном проекте проверки. + self.assertEqual(builder.prepare(self.config, self.build_dir)["version"], expected) # Проверяет перенос через реальную цепочку проектный BAT → общий precompile. self.settings["version"] = [255, 255, 999] # Задаёт последний допустимый номер контракта. self.save_settings() # Устанавливает границу в сохраняемой конфигурации. previous = self.config.read_bytes() # Полное переполнение не должно портить исходный JSON. - with self.assertRaises(ValueError): builder.prepare(self.config, self.build_dir) # Больше допустимых номеров нет, поэтому prebuild обязан остановиться. + with self.assertRaises(subprocess.CalledProcessError): builder.prepare(self.config, self.build_dir) # Общий precompile отклоняет переполнение и передаёт проектной обёртке ненулевой код процесса. self.assertEqual(self.config.read_bytes(), previous) # Проверяет отсутствие сброса версии к нулю или частичной записи. def test_failure_consumes_number_and_bat_returns_error(self): # Сохраняет правило занятых номеров при неудачной сборке и передачу ошибки CCS. - with patch.object(builder.subprocess, "run", side_effect=subprocess.CalledProcessError(7, "precompile.bat")): # Моделирует отказ общего генератора после резервирования версии. - with self.assertRaises(subprocess.CalledProcessError): builder.prepare(self.config, self.build_dir) # Ошибка должна дойти до вызывающего процесса. + self.build_dir.mkdir() # Готовит каталог только временного проекта для воспроизведения отказа записи заголовка. + (self.build_dir / "generated").write_text("blocked", encoding="utf-8") # Файл вместо каталога generated вызывает отказ общего генератора после резервирования номера. + result = subprocess.run(["cmd.exe", "/d", "/c", "call", str(self.project / "firmware_build.bat"), "prepare", "--build-dir", str(self.build_dir)], cwd=ROOT, capture_output=True) # Реальный BAT проверяет передачу ошибки templates без подмены процесса, владеющего счётчиком. + self.assertNotEqual(result.returncode, 0) # Неудачная генерация заголовка обязана остановить pre-build. self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], [1, 0, 1]) # Следующая попытка не должна повторять номер неудачной сборки. result = subprocess.run(["cmd.exe", "/d", "/c", "call", str(self.project / "firmware_build.bat"), "build"], cwd=ROOT, capture_output=True) # Намеренно пропускает обязательные аргументы CLI через настоящий BAT. self.assertNotEqual(result.returncode, 0) # BAT не должен маскировать ошибку Python успешным завершением. @@ -156,14 +160,19 @@ class BuildStepsTest(unittest.TestCase): # Проверяет контракт builder.verify_package(self.project.parent / "images copy" / image.name, metadata) # Проверяет SHA-256 и метаданные дополнительной копии. self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], [1, 0, 1]) # Postbuild не увеличивает номер повторно после prebuild. - def test_ccs_uses_bat_in_both_configurations(self): # Не допускает возврат к длинным проектным Python-командам в CCS. + def test_ccs_prebuild_increments_in_both_configurations(self): # Проверяет фактический итератор и при прямом подключении templates, и через проектную обёртку CCS. configurations = ET.parse(PROJECT / ".cproject").findall("./storageModule/cconfiguration/storageModule/configuration") # Проверяет реальные настройки проекта. self.assertEqual({item.get("name") for item in configurations}, {"Debug", "Release"}) # Требует обнаружения обеих конфигураций. - for configuration in configurations: # Проверяет подключение обоих шагов независимо от активной конфигурации IDE. - for field, command in (("prebuildStep", "prepare"), ("postbuildStep", "build")): # Оба этапа должны запускаться через один переносимый BAT. - self.assertIn(f'firmware_build.bat" {command} ', configuration.get(field)) # Проверяет команду и единый BAT-файл. - self.assertNotIn("python", configuration.get(field)) # Выбор интерпретатора и путь к сценарию остаются внутри BAT. - self.assertNotIn("SRC_ROOT", configuration.get(field)) # Шаги CCS больше не зависят от структуры репозитория исходного изделия. + self.settings["version"] = [1, 0, 4] # Воспроизводит рабочий номер, на котором прямое подключение templates перестало увеличивать версию. + self.save_settings() # Не меняет настоящий JSON и не занимает номера пользовательских прошивок. + for configuration, expected in zip(configurations, ([1, 0, 5], [1, 0, 6])): # Два запуска с одним JSON должны увеличивать номер ровно по одному разу. + build_dir = self.build_dir / configuration.get("name") # Каждая конфигурация получает собственные generated, как при сборке в IDE. + command = configuration.get("prebuildStep").rstrip(";").replace("${PROJECT_LOC}", str(self.project)).replace("${BuildDirectory}", str(build_dir)) # Подставляет макросы CCS и удаляет завершающий разделитель списка команд IDE. + result = subprocess.run(command, cwd=ROOT, capture_output=True) # Выполняет команду из настоящего XML через cmd с путями, содержащими пробелы. + self.assertEqual(result.returncode, 0, repr(result.stdout) + repr(result.stderr)) # Показывает диагностику фактического BAT, если подключение templates неработоспособно. + self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], expected) # Проверяет сохраняемый счётчик после прямого вызова и обёртки без двойного увеличения. + self.assertEqual(json.loads((build_dir / "generated/firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Паспорт компиляции обязан содержать тот же номер, который зарезервирован в JSON. + self.assertIn(f"#define FIRMWARE_VERSION_PATCH {expected[2]}U", (build_dir / "generated/firmware_identity.h").read_text(encoding="utf-8")) # Проверяет, что компилятор получит обновлённую версию, а не только новый manifest. if __name__ == "__main__": unittest.main() # Любое нарушение версии, переносимости или защиты BIN возвращает ненулевой код.