Перенести итератор версии прошивки в общий генератор

Увеличивать версию при прямом подключении precompile из CCS и Keil.
Сохранять счётчик атомарно и оставлять ручной режим без включённого флага.
Добавить проверки итератора и обновить примеры подключения.
This commit is contained in:
2026-10-19 12:02:07 +03:00
parent 0c4eab4615
commit d42f2e1622
5 changed files with 123 additions and 13 deletions
+22 -3
View File
@@ -45,15 +45,34 @@ def write_atomic(path: Path, text: str):
temporary.replace(path)
def next_version(version): # Вычисляет следующий номер по ограничениям общего контракта firmware-info v1.
result = list(version) # Сохраняет исходный номер до успешной проверки полного переполнения.
for index, limit in ((2, 999), (1, 255), (0, 255)): # Patch имеет диапазон 0..999, minor и major занимают по одному октету 0..255.
if result[index] < limit: # Находит младший разряд, который ещё можно увеличить.
result[index] += 1 # Резервирует следующий номер попытки сборки, включая будущие неудачные компиляции.
return result # Старшие разряды сохраняются, переполненные младшие уже обнулены.
result[index] = 0 # Переносит единицу в следующий разряд после достижения предела текущего.
raise ValueError("Firmware version exhausted at 255.255.999") # Не допускает повторного использования номера после полного переполнения.
def prepare(repository: Path, templates: Path, config: Path, output: Path) -> dict:
settings = json.loads(config.read_text(encoding="utf-8-sig"))
version = settings["version"]
if (len(version) != 3 or any(type(v) is not int for v in version)
or not all(0 <= v <= limit for v, limit in zip(version, (255, 255, 999)))):
raise ValueError("version must be [major 0..255, minor 0..255, patch 0..999]")
if not isinstance(version, list) or len(version) != 3 or any(type(value) is not int or not 0 <= value <= limit for value, limit in zip(version, (255, 255, 999))): # Проверяет JSON-массив трёх целых разрядов до любого изменения сохраняемого счётчика.
raise ValueError("version must be [major 0..255, minor 0..255, patch 0..999]") # Ошибочные типы, логические значения и выход за пределы контракта останавливают pre-build.
if type(settings.get("increment_version", False)) is not bool: # Отсутствующий флаг сохраняет ручной режим старых проектов; строки true/false не считаются настройкой.
raise ValueError("increment_version must be true or false") # Требует явное логическое значение JSON вместо неявного преобразования Python.
for field in ("product", "target"):
if not isinstance(settings.get(field), str) or not settings[field].strip():
raise ValueError(f"missing {field}")
if settings.get("increment_version", False): # Прямые вызовы templates из CCS, Keil и CLI используют один и тот же сохраняемый итератор.
version = settings["version"] = next_version(version) # Вычисляет номер для JSON, заголовка и метаданных только один раз.
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=config.parent, prefix=".firmware-version-", delete=False) as stream: # Временный файл на том же диске позволяет атомарно заменить настройки без частичной записи.
temporary = Path(stream.name) # Запоминает только собственный файл, чтобы не затронуть пользовательские данные при очистке.
try: stream.write(json.dumps(settings, ensure_ascii=False, indent=2) + "\n") # Сохраняет новый номер и все дополнительные поля проекта в UTF-8.
except BaseException: stream.close(); temporary.unlink(); raise # Ошибка записи оставляет исходный JSON целым и удаляет незавершённый файл.
try: temporary.replace(config) # Занимает номер до дальнейших шагов, поэтому отказ генерации или компиляции не повторит его.
finally: temporary.unlink(missing_ok=True) # Удаляет временный файл при ошибке замены; после успеха он уже переименован.
source, dependency = git_state(repository), git_state(templates)
# A clean application built with a modified templates checkout is dirty too.
known = source["commit"] is not None and dependency["commit"] is not None