feat(firmware-info): копируй папку текущей сборки целиком
Добавь каталог назначения последним аргументом postcompile CCS и Keil. Копируй только архив текущего запуска с сохранением имени папки, всех файлов и проверкой SHA-256. Обнови MD-команды и проверки выбора папки, вложенных файлов и ошибок экспорта.
This commit is contained in:
@@ -6,7 +6,7 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
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):
|
class ArchiveTests(unittest.TestCase):
|
||||||
@@ -48,6 +48,41 @@ class ArchiveTests(unittest.TestCase):
|
|||||||
self.assertNotEqual(folders[0], folders[1])
|
self.assertNotEqual(folders[0], folders[1])
|
||||||
self.assertTrue(all((folder / image.name).is_file() for folder in folders))
|
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):
|
def test_offset_normalizes_to_utc_and_dirty_is_visible(self):
|
||||||
metadata = self.metadata("2026-10-08T15:30:00+03:00")
|
metadata = self.metadata("2026-10-08T15:30:00+03:00")
|
||||||
metadata["source"]["dirty"] = True
|
metadata["source"]["dirty"] = True
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from identity import archive_build, seal, verify
|
|||||||
|
|
||||||
|
|
||||||
def postbuild(compiler: Path, build_dir: Path, artifact: str,
|
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 '\\/:*?"<>|'):
|
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")
|
raise ValueError("artifact must be a file base name, without a path")
|
||||||
tool = compiler.resolve() / "bin" / "hex2000.exe"
|
tool = compiler.resolve() / "bin" / "hex2000.exe"
|
||||||
@@ -53,7 +53,7 @@ def postbuild(compiler: Path, build_dir: Path, artifact: str,
|
|||||||
archive_build(result, metadata, output, (
|
archive_build(result, metadata, output, (
|
||||||
output / hex_image.name,
|
output / hex_image.name,
|
||||||
output / (hex_image.name + ".map"),
|
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] BIN: {result}")
|
||||||
print(f"[firmware-build] Manifest: {result}.manifest.json")
|
print(f"[firmware-build] Manifest: {result}.manifest.json")
|
||||||
print(f"[firmware-build] SHA-256: {result}.sha256")
|
print(f"[firmware-build] SHA-256: {result}.sha256")
|
||||||
@@ -65,6 +65,7 @@ def main() -> int:
|
|||||||
for name in ("compiler", "build-dir", "metadata", "output"):
|
for name in ("compiler", "build-dir", "metadata", "output"):
|
||||||
parser.add_argument("--" + name, type=Path, required=True)
|
parser.add_argument("--" + name, type=Path, required=True)
|
||||||
parser.add_argument("--artifact", required=True)
|
parser.add_argument("--artifact", required=True)
|
||||||
|
parser.add_argument("--export", type=Path)
|
||||||
try:
|
try:
|
||||||
postbuild(**vars(parser.parse_args()))
|
postbuild(**vars(parser.parse_args()))
|
||||||
except (OSError, ValueError, KeyError, TypeError, subprocess.CalledProcessError) as error:
|
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")
|
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,
|
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."""
|
"""Keep a byte-identical build snapshot, alongside the existing flat output."""
|
||||||
image, metadata_path = image.resolve(), metadata_path.resolve()
|
image, metadata_path = image.resolve(), metadata_path.resolve()
|
||||||
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
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():
|
for file in stage.iterdir():
|
||||||
file.replace(destination / file.name)
|
file.replace(destination / file.name)
|
||||||
print(f"[firmware-build] Folder: {destination}")
|
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():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
@@ -173,6 +190,7 @@ def main():
|
|||||||
p.add_argument("--image", type=Path, required=True)
|
p.add_argument("--image", type=Path, required=True)
|
||||||
p.add_argument("--metadata", dest="metadata_path", type=Path, required=True)
|
p.add_argument("--metadata", dest="metadata_path", type=Path, required=True)
|
||||||
p.add_argument("--output", type=Path)
|
p.add_argument("--output", type=Path)
|
||||||
|
p.add_argument("--export", type=Path)
|
||||||
args = vars(parser.parse_args())
|
args = vars(parser.parse_args())
|
||||||
command = args.pop("command")
|
command = args.pop("command")
|
||||||
result = {"prepare": prepare, "seal": seal, "verify": verify, "archive": archive_build}[command](**args)
|
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 "%~4"=="" goto usage
|
||||||
if "%~5"=="" goto usage
|
if "%~5"=="" goto usage
|
||||||
if "%~6"=="" 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"
|
python "%~dp0..\..\..\c\firmware-info\tools\c2000_postbuild.py" --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6"
|
||||||
exit /b %errorlevel%
|
exit /b %errorlevel%
|
||||||
:usage
|
: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
|
echo Legacy: postcompile.bat "firmware-release.cmd" 1>&2
|
||||||
exit /b 2
|
exit /b 2
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ exit /b %errorlevel%
|
|||||||
:identity
|
:identity
|
||||||
if "%~2"=="" goto usage
|
if "%~2"=="" goto usage
|
||||||
if "%~3"=="" 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" (
|
if /i not "%~x2"==".hex" if /i not "%~x2"==".bin" (
|
||||||
echo [firmware-build] Expected a ready HEX or BIN image: "%~2" 1>&2
|
echo [firmware-build] Expected a ready HEX or BIN image: "%~2" 1>&2
|
||||||
exit /b 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%
|
if errorlevel 1 exit /b %errorlevel%
|
||||||
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" verify --image "%~f2" --manifest "%~f2.manifest.json"
|
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" verify --image "%~f2" --manifest "%~f2.manifest.json"
|
||||||
if errorlevel 1 exit /b %errorlevel%
|
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"
|
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3"
|
||||||
|
:archive_done
|
||||||
if errorlevel 1 exit /b %errorlevel%
|
if errorlevel 1 exit /b %errorlevel%
|
||||||
echo [firmware-build] Image: "%~f2"
|
echo [firmware-build] Image: "%~f2"
|
||||||
echo [firmware-build] Manifest: "%~f2.manifest.json"
|
echo [firmware-build] Manifest: "%~f2.manifest.json"
|
||||||
echo [firmware-build] SHA-256: "%~f2.sha256"
|
echo [firmware-build] SHA-256: "%~f2.sha256"
|
||||||
exit /b 0
|
exit /b 0
|
||||||
:usage
|
: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
|
exit /b 2
|
||||||
|
|||||||
Reference in New Issue
Block a user