Использовать общий итератор версии прошивки

Убрать повторное увеличение версии из проектной обёртки.
Проверять прямое подключение templates и вызов через обёртку в CCS.
Сохранить текущий номер прошивки 1.0.5 и уточнить документацию.
This commit is contained in:
2026-10-19 12:02:08 +03:00
parent 31f92e89d9
commit dce9dd41a4
4 changed files with 29 additions and 32 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -4,7 +4,7 @@
"version": [
1,
0,
4
5
],
"increment_version": true,
"templates": "../../newProject/templates",

View File

@@ -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 возвращает ненулевой код.