feat(firmware-info): копируй папку текущей сборки целиком
Добавь каталог назначения последним аргументом postcompile CCS и Keil. Копируй только архив текущего запуска с сохранением имени папки, всех файлов и проверкой SHA-256. Обнови MD-команды и проверки выбора папки, вложенных файлов и ошибок экспорта.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user