Исправить сборку CCS и Keil на Python 3.7

Убрать несовместимый параметр missing_ok при очистке временных файлов.
Обеспечить повторное копирование архивов без параметра dirs_exist_ok.
Добавить проверки генерации заголовков и экспорта с API Python 3.7.
This commit is contained in:
2026-11-10 15:13:35 +03:00
parent a640f98f74
commit 7b1a16d6e4
3 changed files with 90 additions and 4 deletions
@@ -0,0 +1,66 @@
"""Проверки CCS/Keil с интерфейсами pathlib и shutil, доступными в Python 3.7.""" # Воспроизводит ограничения старого Python без установки другого интерпретатора.
import importlib.util # Загружает проверяемые инструменты именно из этой библиотеки templates.
import json # Проверяет сохранённый счётчик и фактический паспорт компиляции.
from pathlib import Path # Моделирует прежнюю сигнатуру удаления временных файлов.
import shutil # Моделирует прежнюю сигнатуру копирования каталогов.
import tempfile # Ограничивает запись изолированным каталогом теста.
import unittest # Проверки запускаются стандартным Python без сторонних зависимостей.
from unittest.mock import patch # Подменяет только несовместимые сигнатуры стандартной библиотеки.
def load_tool(name): # Импортирует одну точку входа без запуска CLI.
spec = importlib.util.spec_from_file_location("compatibility_" + name, Path(__file__).resolve().parents[1] / "tools" / (name + ".py")) # Выбор не зависит от PATH компьютера сборки.
module = importlib.util.module_from_spec(spec) # Отдельный экземпляр сохраняет изоляцию других тестов.
spec.loader.exec_module(module) # Загружает публичные функции подготовки и копирования.
return module # Тесты вызывают те же функции, что BAT-обвязки двух IDE.
identity, c2000 = load_tool("identity"), load_tool("c2000_build") # Обе цепочки используют общий формат паспорта.
original_unlink, original_copytree = Path.unlink, shutil.copytree # Сохраняет настоящие реализации перед ограничением их сигнатур.
def unlink37(path): # Python 3.7 не допускает аргумент missing_ok.
return original_unlink(path) # Реальное удаление сохраняет настоящие ошибки доступа и отсутствующего файла.
def copytree37(source, destination, symlinks=False, ignore=None, copy_function=shutil.copy2, ignore_dangling_symlinks=False): # Сигнатура Python 3.7 не допускает dirs_exist_ok.
return original_copytree(source, destination, symlinks, ignore, copy_function, ignore_dangling_symlinks) # Старый интерфейс продолжает копировать обычные новые каталоги.
class Python37CompatibilityTests(unittest.TestCase): # Проверяет успешный prebuild и повторный экспорт вместо одного синтаксического анализа.
def setUp(self): # Каждый сценарий начинает с отдельного проекта со включённым итератором версии.
temporary = tempfile.TemporaryDirectory(prefix="firmware python37 ") # Пробелы в пути соответствуют обычным пользовательским библиотекам.
self.addCleanup(temporary.cleanup) # Очистка выполняется после снятия ограничений стандартной библиотеки.
self.root = Path(temporary.name) # Рабочие проекты CCS/Keil не изменяются.
self.config = self.root / "firmware_identity.json" # Конфигурация принадлежит только временной проверке.
self.config.write_text(json.dumps({"product": "TEST_BOARD", "target": "TMS320F2812", "version": [1, 2, 3], "increment_version": True}), encoding="utf-8") # Этот режим воспроизводит исходную ошибку unlink после увеличения версии.
def test_keil_and_ccs_prepare_generate_headers_with_old_unlink(self): # Обе обвязки должны завершить генерацию при старой сигнатуре pathlib.
with patch.object(Path, "unlink", unlink37): # Любой оставшийся missing_ok вызовет тот же TypeError, что в журнале пользователя.
keil = identity.prepare(self.root, self.root, self.config, self.root / "Generated/target_0") # Keil напрямую вызывает общий prepare.
ccs = c2000.prepare(self.root, Path(__file__).resolve().parents[3], self.root / "STEND40_PCH_1/generated", config=self.config) # CCS загружает генератор из полной проверяемой библиотеки templates.
for relative, expected in (("Generated/target_0", [1, 2, 4]), ("STEND40_PCH_1/generated", [1, 2, 5])): # Две попытки резервируют разные номера без пропуска после TypeError.
output = self.root / relative # Проверяет фактический include-каталог каждой IDE.
self.assertIn("#define FIRMWARE_VERSION_PATCH " + str(expected[2]) + "U", (output / "firmware_identity.h").read_text(encoding="utf-8")) # Заголовок существует и содержит зарезервированную версию.
self.assertEqual(json.loads((output / "firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Паспорт совпадает с константами для компилятора.
self.assertEqual((keil["version"], ccs["version"]), ([1, 2, 4], [1, 2, 5])) # Обе публичные функции возвращают успешный результат.
def test_archive_export_and_atomic_copy_support_old_file_apis(self): # Проверяет повторное сохранение комплекта после успешного prebuild.
generated = self.root / "generated" # Метаданные сохраняются отдельно от исходного JSON.
identity.prepare(self.root, self.root, self.config, generated) # Получает настоящий паспорт для проверки SHA-256 и архива.
image = self.root / "application.bin" # Произвольные байты достаточны для общего Keil-контракта файловой целостности.
image.write_bytes(b"firmware bytes") # Проверяет сохранение точных байтов при нескольких копированиях.
with patch.object(Path, "unlink", unlink37), patch.object(shutil, "copytree", copytree37): # Воспроизводит оба ограничения Python 3.7 одновременно.
for attempt in range(2): # Второй экспорт должен объединять уже существующие каталоги.
result = identity.archive_build(image, generated / "firmware_identity.json", self.root / "archive", export=self.root / "export") # Печать папки соответствует настоящему postcompile Keil.
folder = Path(result["directory"]) # Использует точный архив текущего вызова.
(folder / "nested").mkdir(exist_ok=True) # Вложенный сопроводительный файл проверяет рекурсивное копирование.
(folder / "nested/map.txt").write_text("map " + str(attempt), encoding="utf-8") # Повторный выпуск обновляет существующий файл.
identity.copy_tree(folder, self.root / "ccs-output", copy_function=c2000.atomic_copy) # C2000 сохраняет атомарную замену с прежним интерфейсом unlink.
self.assertEqual((self.root / "ccs-output/nested/map.txt").read_text(encoding="utf-8"), "map 1") # Вложенные файлы обновились при повторном выпуске.
self.assertEqual((Path(result["exported_directory"]) / image.name).read_bytes(), image.read_bytes()) # Экспорт содержит исходный образ без изменения байтов.
self.assertFalse(list((self.root / "ccs-output").rglob(".firmware-*"))) # Успешная атомарная замена не оставляет незавершённых файлов.
if __name__ == "__main__": # Позволяет запустить регрессию стандартным unittest на компьютере сборки.
unittest.main() # Ненулевой результат выявляет несовместимость инструментов до настройки IDE.
+5 -2
View File
@@ -181,7 +181,10 @@ def atomic_copy(source, destination): # Сохраняет проверенны
shutil.copy2(source, temporary) # Сначала полностью записывает новые байты и сохраняет время исходного артефакта.
temporary.replace(destination) # Одной файловой операцией заменяет старый результат после успешной записи временного файла.
finally: # Незавершённый временный файл не должен попадать в будущий комплект Публикатора.
temporary.unlink(missing_ok=True) # Удаляет только собственный незавершённый файл; после успешной замены его уже нет.
try: # Python 3.7 не поддерживает параметр missing_ok у pathlib.Path.unlink.
temporary.unlink() # Удаляет только собственный незавершённый файл при ошибке копирования или замены.
except FileNotFoundError: # Успешный replace уже перенёс файл на постоянное имя.
pass # Отсутствующий временный файл не превращает успешное копирование в ошибку сборки.
return str(destination) # Сохраняет контракт copy_function стандартного shutil.copytree.
@@ -386,7 +389,7 @@ def publish(project, templates, tools_templates, compiler, build_dir, artifact,
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) # Сохраняет только проверенный архив с атомарной заменой файлов при повторном выпуске той же компиляции.
identity.copy_tree(item, output / item.name, copy_function=atomic_copy) # Общий обход совместим с Python 3.7 и сохраняет атомарную замену каждого проверенного файла.
else: # Общие служебные файлы, если они появятся, сохраняются в корне текущего выпуска.
atomic_copy(item, output / item.name) # Не открывает старые файлы другой ветки на перезапись и не оставляет частичный BIN/HEX при отказе записи.
verify_package(output / image_name, metadata) # Подтверждает отсутствие повреждения при копировании на постоянный диск.
+19 -2
View File
@@ -73,7 +73,11 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
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) # Удаляет временный файл при ошибке замены; после успеха он уже переименован.
finally: # Очистка должна работать и в Python 3.7, который ещё не принимает missing_ok.
try: # После успешной замены временное имя уже отсутствует.
temporary.unlink() # Удаляет только собственный незавершённый файл при отказе replace.
except FileNotFoundError: # Отсутствие файла после успешного rename является штатным результатом.
pass # Другие ошибки файловой системы продолжают останавливать подготовку.
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
@@ -152,6 +156,19 @@ def build_folder_name(metadata: dict) -> str:
return identity + "_" + stamp.strftime("%Y-%m-%d_%H-%M-%S_UTC+3")
def copy_tree(source: Path, destination: Path, copy_function=shutil.copy2): # Объединяет архив с существующим каталогом без dirs_exist_ok, отсутствующего в Python 3.7.
source, destination = Path(source), Path(destination) # Принимает пути обоих типов из общего экспорта и проверенной C2000-сборки.
destination.mkdir(parents=True, exist_ok=True) # Повторный экспорт сохраняет соседние файлы, но не принимает обычный файл вместо папки.
for entry in source.iterdir(): # Копирует только содержимое конкретного проверенного архива.
target = destination / entry.name # Сохраняет исходные имена и вложенную структуру комплекта.
if entry.is_dir(): # Вложенные каталоги требуют того же объединения при повторном выпуске.
copy_tree(entry, target, copy_function) # Передаёт атомарное копирование C2000 на все уровни архива.
else: # Каждый файл обрабатывается выбранным способом сохранения.
copy_function(entry, target) # По умолчанию сохраняет метаданные copy2, а C2000 использует atomic_copy.
shutil.copystat(source, destination) # Восстанавливает время и атрибуты каталога после записи дочерних файлов.
return str(destination) # Сохраняет возвращаемый тип стандартного shutil.copytree.
def export_build(folder: Path, export: Path, image_name: str) -> Path:
"""Copy the current complete build folder, without scanning old archives."""
folder, export = folder.resolve(), export.resolve()
@@ -161,7 +178,7 @@ def export_build(folder: Path, export: Path, image_name: str) -> Path:
verify(folder / image_name, folder / (image_name + ".manifest.json"))
export.mkdir(parents=True, exist_ok=True)
if destination != folder:
shutil.copytree(folder, destination, dirs_exist_ok=True)
copy_tree(folder, destination) # Повторное копирование архива совместимо с Python 3.7 без потери соседних выпусков.
verify(destination / image_name, destination / (image_name + ".manifest.json"))
print(f"[firmware-build] Exported folder: {destination}")
return destination