Исправить сборку CCS и Keil на Python 3.7
Убрать несовместимый параметр missing_ok при очистке временных файлов. Обеспечить повторное копирование архивов без параметра dirs_exist_ok. Добавить проверки генерации заголовков и экспорта с API Python 3.7.
This commit is contained in:
@@ -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.
|
||||
@@ -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) # Подтверждает отсутствие повреждения при копировании на постоянный диск.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user