feat(firmware-info): копируй папку текущей сборки целиком

Добавь каталог назначения последним аргументом postcompile CCS и Keil. Копируй только архив текущего запуска с сохранением имени папки, всех файлов и проверкой SHA-256. Обнови MD-команды и проверки выбора папки, вложенных файлов и ошибок экспорта.
This commit is contained in:
2026-10-08 16:26:21 +03:00
parent da055c2787
commit 4fc0ad4e14
6 changed files with 94 additions and 23 deletions

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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