feat(flash): pm3flash --build PLATFORM (build from the submodule, then flash)

build_firmware()/find_firmware_src() cross-compile fullimage.elf via
'make -C firmware PLATFORM=<explicit> armsrc/all' and pm3flash --build flashes it.
PLATFORM is explicit (never inferred from is_rdv4 — that's the running firmware's
flag, not the board).

Hardened per an adversarial multi-agent review of the change:
- --build now implies --force: it must flash the image it just built. A dirty
  rebuild keeps the same base SHA, so matches_pin can't distinguish it from the
  old firmware and would otherwise silently skip the flash (exit 0).
- make missing/not-executable -> FlashError (was an uncaught OSError traceback).
- find_firmware_src no longer falls back to a CWD-relative ./firmware — running
  make on a stray Makefile is arbitrary code execution; only explicit arg /
  $PM3PY_FIRMWARE_SRC / the package repo root. Suite 1045 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-05 22:20:04 -07:00
parent 9f926a176c
commit c0ae7b90c8
4 changed files with 184 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
"""Tests for the firmware pin + needs-reflash detection (no hardware)."""
import asyncio
import pytest
from pm3py._firmware import FirmwarePin, FIRMWARE_PIN, matches
from pm3py.core.client import Proxmark3
@@ -73,3 +74,30 @@ def test_cli_help_exits_clean(capsys):
assert e.code == 0
out = capsys.readouterr().out
assert "pm3flash" in out
def test_cli_build_and_image_mutually_exclusive(capsys):
with pytest.raises(SystemExit):
flash_cli.main(["--build", "PM3GENERIC", "--image", "x.elf"])
def test_cli_build_invokes_builder(monkeypatch, tmp_path):
elf = tmp_path / "fullimage.elf"
elf.write_bytes(b"\x7fELF")
monkeypatch.setattr(flash_cli, "build_firmware", lambda platform: elf)
seen = {}
async def fake_run(args):
seen["image"] = args.image
seen["force"] = args.force
return 0
monkeypatch.setattr(flash_cli, "_run", fake_run)
# main() uses asyncio.run(); route it through the suite's shared loop so it
# doesn't close/null the global loop and break later get_event_loop() tests.
monkeypatch.setattr(flash_cli.asyncio, "run",
lambda coro: asyncio.get_event_loop().run_until_complete(coro))
rc = flash_cli.main(["--build", "PM3GENERIC"])
assert rc == 0
assert seen["image"] == str(elf)
assert seen["force"] is True # --build flashes what it built, regardless of the pin