diff --git a/c/firmware-info/tests/test_identity_archive.py b/c/firmware-info/tests/test_identity_archive.py index e7818b3..06ea5ec 100644 --- a/c/firmware-info/tests/test_identity_archive.py +++ b/c/firmware-info/tests/test_identity_archive.py @@ -6,7 +6,7 @@ import tempfile import unittest sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools")) -from identity import archive_build, build_folder_name, verify +from identity import archive_build, build_folder_name, export_build, verify class ArchiveTests(unittest.TestCase): @@ -48,6 +48,41 @@ class ArchiveTests(unittest.TestCase): self.assertNotEqual(folders[0], folders[1]) self.assertTrue(all((folder / image.name).is_file() for folder in folders)) + def test_export_copies_current_folder_only(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + image, metadata = root / "firmware.bin", root / "metadata.json" + image.write_bytes(b"exact current build") + # A folder with a later timestamp must not be selected for this build. + metadata.write_text(json.dumps(self.metadata("2026-10-08T12:40:00+00:00")), encoding="utf-8") + archive_build(image, metadata, root / "local") + metadata.write_text(json.dumps(self.metadata()), encoding="utf-8") + result = archive_build(image, metadata, root / "local", export=root / "exported") + copied = Path(result["exported_directory"]) + self.assertEqual([p.name for p in (root / "exported").iterdir()], + ["a1b2c3d4_2026-10-08_12-30-00_UTC"]) + self.assertEqual((copied / image.name).read_bytes(), image.read_bytes()) + self.assertTrue((copied / "firmware_identity.json").is_file()) + self.assertTrue((copied / (image.name + ".sha256")).is_file()) + verify(copied / image.name, copied / (image.name + ".manifest.json")) + + def test_complete_folder_export_keeps_nested_files_and_old_destination(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + image, metadata = root / "firmware.bin", root / "metadata.json" + image.write_bytes(b"image") + metadata.write_text(json.dumps(self.metadata()), encoding="utf-8") + folder = Path(archive_build(image, metadata, root / "local")["directory"]) + (folder / "notes").mkdir() + (folder / "notes" / "release.txt").write_text("release notes", encoding="utf-8") + old = root / "exported" / "old_build" + old.mkdir(parents=True) + (old / "keep.txt").write_text("keep", encoding="utf-8") + copied = export_build(folder, root / "exported", image.name) + self.assertEqual((copied / "notes" / "release.txt").read_text(), "release notes") + self.assertEqual((old / "keep.txt").read_text(), "keep") + with self.assertRaises(ValueError): + export_build(folder, folder / "recursive", image.name) def test_offset_normalizes_to_utc_and_dirty_is_visible(self): metadata = self.metadata("2026-10-08T15:30:00+03:00") metadata["source"]["dirty"] = True diff --git a/c/firmware-info/tools/c2000_postbuild.py b/c/firmware-info/tools/c2000_postbuild.py index 3984452..ac9f349 100644 --- a/c/firmware-info/tools/c2000_postbuild.py +++ b/c/firmware-info/tools/c2000_postbuild.py @@ -16,7 +16,7 @@ from identity import archive_build, seal, verify def postbuild(compiler: Path, build_dir: Path, artifact: str, - metadata: Path, output: Path) -> Path: + metadata: Path, output: Path, export: Path = None) -> Path: if not artifact or artifact in (".", "..") or any(c in artifact for c in '\\/:*?"<>|'): raise ValueError("artifact must be a file base name, without a path") tool = compiler.resolve() / "bin" / "hex2000.exe" @@ -53,7 +53,7 @@ def postbuild(compiler: Path, build_dir: Path, artifact: str, archive_build(result, metadata, output, ( output / hex_image.name, output / (hex_image.name + ".map"), - output / (bin_image.name + ".map"))) + output / (bin_image.name + ".map")), export=export) print(f"[firmware-build] BIN: {result}") print(f"[firmware-build] Manifest: {result}.manifest.json") print(f"[firmware-build] SHA-256: {result}.sha256") @@ -65,6 +65,7 @@ def main() -> int: for name in ("compiler", "build-dir", "metadata", "output"): parser.add_argument("--" + name, type=Path, required=True) parser.add_argument("--artifact", required=True) + parser.add_argument("--export", type=Path) try: postbuild(**vars(parser.parse_args())) except (OSError, ValueError, KeyError, TypeError, subprocess.CalledProcessError) as error: diff --git a/c/firmware-info/tools/identity.py b/c/firmware-info/tools/identity.py index 954a48c..a3e42bd 100644 --- a/c/firmware-info/tools/identity.py +++ b/c/firmware-info/tools/identity.py @@ -132,8 +132,22 @@ def build_folder_name(metadata: dict) -> str: return identity + "_" + stamp.strftime("%Y-%m-%d_%H-%M-%S_UTC") +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() + if export == folder or folder in export.parents: + raise ValueError("export directory must be outside the current build folder") + destination = export / folder.name + 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) + verify(destination / image_name, destination / (image_name + ".manifest.json")) + print(f"[firmware-build] Exported folder: {destination}") + return destination + def archive_build(image: Path, metadata_path: Path, output: Path = None, - extra_files=()) -> dict: + extra_files=(), export: Path = None) -> dict: """Keep a byte-identical build snapshot, alongside the existing flat output.""" image, metadata_path = image.resolve(), metadata_path.resolve() metadata = json.loads(metadata_path.read_text(encoding="utf-8")) @@ -155,7 +169,10 @@ def archive_build(image: Path, metadata_path: Path, output: Path = None, for file in stage.iterdir(): file.replace(destination / file.name) print(f"[firmware-build] Folder: {destination}") - return {"directory": str(destination), "file": image.name} + result = {"directory": str(destination), "file": image.name} + if export is not None: + result["exported_directory"] = str(export_build(destination, export, image.name)) + return result def main(): parser = argparse.ArgumentParser(description=__doc__) @@ -173,6 +190,7 @@ def main(): p.add_argument("--image", type=Path, required=True) p.add_argument("--metadata", dest="metadata_path", type=Path, required=True) p.add_argument("--output", type=Path) + p.add_argument("--export", type=Path) args = vars(parser.parse_args()) command = args.pop("command") result = {"prepare": prepare, "seal": seal, "verify": verify, "archive": archive_build}[command](**args) diff --git a/tools/firmware-build/README.md b/tools/firmware-build/README.md index 7a17e72..8d83e37 100644 --- a/tools/firmware-build/README.md +++ b/tools/firmware-build/README.md @@ -65,29 +65,30 @@ Debug/ Postcompile автоматически создаёт папку `HASH_YYYY-MM-DD_HH-MM-SS_UTC` для каждой сборки. HASH — первые 8 символов commit прошивки из pre-build JSON; при незакоммиченных изменениях добавляется `+`. Дата и время берутся из `built_at_utc`, в UTC. Если commit исходников неизвестен, используется build ID или NOGIT000. Например: `f5c3654e+_2026-10-08_12-30-00_UTC`. Сохраняются точные байты образа, manifest, SHA-256 и firmware_identity.json; в CCS также HEX и map. Повтор postcompile с теми же метаданными обновляет ту же папку. Файлы непосредственно в firmware сохраняются, поэтому существующие команды копирования продолжают работать. Добавьте generated и firmware в игнорируемые Git-каталоги. Обычно вся папка Debug уже игнорируется. Полный Rebuild обязателен для выпуска. BAT сами по себе не подтверждают, что OUT собран именно с этим заголовком. -### 4. Копирование результата в каталог SETGUI +### 4. Копирование папки текущей сборки в SETGUI -Чтобы после успешного postcompile скопировать BIN в `K:\git_project\newProject\SETGUI\..periph_28335\firmware\`, добавьте в конец существующей команды Post-build: +Передайте каталог назначения последним аргументом postcompile. Скрипт скопирует целиком только папку HASH_DATE, созданную текущим запуском: BIN, HEX, map, manifest, SHA-256 и метаданные. Он использует папку из pre-build JSON, не выбирает её по времени изменения файлов и не копирует остальные архивы или отдельные файлы из корня firmware. + +Полная команда Post-build для templates в `K:\git_project\newProject\newTEMPL\templates`, одной строкой: ```bat -&& copy /Y "${BuildDirectory}\firmware\*.bin" "K:\git_project\newProject\SETGUI\..periph_28335\firmware\" +cmd /c call "K:\git_project\newProject\newTEMPL\templates\tools\firmware-build\ccs\postcompile.bat" --identity "${CG_TOOL_ROOT}" "${BuildDirectory}" "${BuildArtifactFileBaseName}" "${BuildDirectory}\generated\firmware_identity.json" "${BuildDirectory}\firmware" "K:\git_project\newProject\SETGUI\..periph_28335\firmware" ``` -Для копирования BIN вместе с manifest и SHA-256 используйте полную команду ниже. Она соответствует templates в `K:\git_project\newProject\newTEMPL\templates`; при другом расположении замените путь к postcompile.bat. +Старые суффиксы `&& copy ...` и `&& xcopy ...` удалите. Папка назначения создаётся автоматически. В ней появится, например: -```bat -cmd /c call "K:\git_project\newProject\newTEMPL\templates\tools\firmware-build\ccs\postcompile.bat" --identity "${CG_TOOL_ROOT}" "${BuildDirectory}" "${BuildArtifactFileBaseName}" "${BuildDirectory}\generated\firmware_identity.json" "${BuildDirectory}\firmware" && copy /Y "${BuildDirectory}\firmware\*.bin" "K:\git_project\newProject\SETGUI\..periph_28335\firmware\" && copy /Y "${BuildDirectory}\firmware\*.bin.manifest.json" "K:\git_project\newProject\SETGUI\..periph_28335\firmware\" && copy /Y "${BuildDirectory}\firmware\*.bin.sha256" "K:\git_project\newProject\SETGUI\..periph_28335\firmware\" +```text +K:\git_project\newProject\SETGUI\..periph_28335\firmware\ + f5c3654e+_2026-10-08_12-30-00_UTC\ + MyFirmware.hex + MyFirmware_BUILDID.bin + MyFirmware_BUILDID.bin.manifest.json + MyFirmware_BUILDID.bin.sha256 + firmware_identity.json ``` -В поле CCS вся команда должна быть одной строкой. Папка назначения должна существовать. `&&` запускает следующую операцию только при успешном завершении предыдущей. Маски копируют все соответствующие файлы из каталога firmware, включая результаты прошлых сборок; `/Y` разрешает замену одноимённых файлов. Это локальное копирование, без публикации на сервер. +Копирование выполняется после успешного архивирования; SHA-256 копии проверяется. Console покажет `[firmware-build] Exported folder: ...`. Повтор для той же папки обновляет её файлы; другие папки назначения сохраняются. Без последнего аргумента экспорт отключён. Это локальное копирование без сетевой публикации. -Чтобы скопировать также папки HASH_DATE вместе с их содержимым, вместо трёх команд copy используйте одну команду xcopy в конце post-build: - -```bat -&& xcopy /E /I /Y "${BuildDirectory}\firmware" "K:\git_project\newProject\SETGUI\..periph_28335\firmware" -``` - -Она копирует весь каталог результатов, включая папки предыдущих сборок; при необходимости создаёт папку назначения. В поле CCS добавляйте её к основной команде в той же строке. ### Версия внутри устройства Это подготовка метаданных сборки и файла. Чтобы версия присутствовала в программе и ответах устройства, подключите заголовок `firmware_identity.h` к модулю версии и обработчику запроса. Include path: `${BuildDirectory}/generated`. Для C2000 с 16-битным char нужна адаптация байтового контракта; обычный 8-битный порт нельзя считать готовым. Смотрите [PORTING.md](../../c/firmware-info/PORTING.md). @@ -134,6 +135,13 @@ After Build/Rebuild, одной строкой: cmd /c call "C:\Projects\templates\tools\firmware-build\keil\postcompile.bat" --identity "#H" "$PGenerated\firmware_identity.json" ``` +Для копирования только папки текущей сборки добавьте каталог назначения последним аргументом: + +```bat +cmd /c call "C:\Projects\templates\tools\firmware-build\keil\postcompile.bat" --identity "#H" "$PGenerated\firmware_identity.json" "K:\git_project\newProject\SETGUI\..periph_28335\firmware" +``` + +Будет скопирована целиком новая папка HASH_DATE с HEX/BIN, manifest, SHA-256 и метаданными. Старые команды copy/xcopy в этой строке не нужны. В Console появится `[firmware-build] Exported folder`. В Keil `$P` обозначает каталог проекта с завершающим разделителем, а `#H` — полный путь к выходному HEX. Официальное описание макросов: [Keil User commands](https://www.keil.com/support/man/docs/uv4cl/uv4cl_dg_user.htm). Макросы раскрываются в IDE; при ручном запуске BAT замените их реальными путями. В примере файл uvprojx находится в mdk, поэтому `$P..` указывает на корень исходников. Если JSON и uvprojx находятся в одном каталоге, используйте `"$P."` для repository и `"$Pfirmware_identity.json"` для config; третий аргумент Generated сохраните. diff --git a/tools/firmware-build/ccs/postcompile.bat b/tools/firmware-build/ccs/postcompile.bat index 9bcb94e..04265f1 100644 --- a/tools/firmware-build/ccs/postcompile.bat +++ b/tools/firmware-build/ccs/postcompile.bat @@ -16,10 +16,14 @@ if "%~3"=="" goto usage if "%~4"=="" goto usage if "%~5"=="" goto usage if "%~6"=="" goto usage -if not "%~7"=="" goto usage +if not "%~8"=="" goto usage +if "%~7"=="" goto identity_local +python "%~dp0..\..\..\c\firmware-info\tools\c2000_postbuild.py" --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" --export "%~f7" +exit /b %errorlevel% +:identity_local python "%~dp0..\..\..\c\firmware-info\tools\c2000_postbuild.py" --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" exit /b %errorlevel% :usage -echo Usage: postcompile.bat --identity "compiler-root" "build-dir" "artifact-name" "generated-metadata.json" "output-dir" 1>&2 +echo Usage: postcompile.bat --identity "compiler-root" "build-dir" "artifact-name" "generated-metadata.json" "output-dir" ["export-dir"] 1>&2 echo Legacy: postcompile.bat "firmware-release.cmd" 1>&2 exit /b 2 diff --git a/tools/firmware-build/keil/postcompile.bat b/tools/firmware-build/keil/postcompile.bat index 5068a01..81685fa 100644 --- a/tools/firmware-build/keil/postcompile.bat +++ b/tools/firmware-build/keil/postcompile.bat @@ -13,7 +13,7 @@ exit /b %errorlevel% :identity if "%~2"=="" goto usage if "%~3"=="" goto usage -if not "%~4"=="" goto usage +if not "%~5"=="" goto usage if /i not "%~x2"==".hex" if /i not "%~x2"==".bin" ( echo [firmware-build] Expected a ready HEX or BIN image: "%~2" 1>&2 exit /b 2 @@ -22,12 +22,17 @@ python "%~dp0..\..\..\c\firmware-info\tools\identity.py" seal --image "%~f2" --m if errorlevel 1 exit /b %errorlevel% python "%~dp0..\..\..\c\firmware-info\tools\identity.py" verify --image "%~f2" --manifest "%~f2.manifest.json" if errorlevel 1 exit /b %errorlevel% +if "%~4"=="" goto archive_local +python "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3" --export "%~f4" +goto archive_done +:archive_local python "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3" +:archive_done if errorlevel 1 exit /b %errorlevel% echo [firmware-build] Image: "%~f2" echo [firmware-build] Manifest: "%~f2.manifest.json" echo [firmware-build] SHA-256: "%~f2.sha256" exit /b 0 :usage -echo Usage: postcompile.bat --identity "image.hex-or-bin" "generated-metadata.json" 1>&2 +echo Usage: postcompile.bat --identity "image.hex-or-bin" "generated-metadata.json" ["export-dir"] 1>&2 exit /b 2