Let motatool size authenticated nRF52 delta workspaces

Forward --inplace-memory only when explicitly supplied, preserving authenticated layout bounds and the conservative legacy fallback.

Validation: MOTATOOL_TEST_BIN=/home/mesh/git/motatool/target/release/motatool python3 -m unittest test_lora_ota (318 passed). Both new auto-sizing regressions fail against the prior runner.
This commit is contained in:
mikecarper committed 2026-10-20 19:42:41 -07:00
1 parent 784d2f4964
commit e22d5d7d0a
2 files changed
+76 -9

No files matched your search

+9 -9
View File
@@ -1560,14 +1560,11 @@ def prepare_package(
"--patch-type",
"in-place",
])
# External staging leaves the application region available as the
# detools workspace. 0xC6000 is safe for both S140 v7 (app starts
# at 0x27000) and older v6 layouts (0x26000). Internal staging
# retains its deliberately smaller legacy workspace.
inplace_memory = args.inplace_memory or (
"0xC6000" if target.nrf_external else "0x98000"
)
command.extend(["--inplace-memory", inplace_memory])
# Let motatool size the workspace from both authenticated layout
# records and validate the application/staging bounds. Images
# without those records retain motatool's conservative fallback.
if args.inplace_memory is not None:
command.extend(["--inplace-memory", args.inplace_memory])
if args.sign_key:
command.extend(["--sign", str(args.sign_key.resolve())])
result = run_checked(
@@ -7949,7 +7946,10 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--public-key", type=Path, help="require this signer when verifying")
parser.add_argument(
"--inplace-memory",
help="nRF52 OTAFIX workspace (auto: 0x98000 internal, 0xC6000 external SD/QSPI)",
help=(
"explicit nRF52 OTAFIX workspace override; default: motatool "
"authenticated-layout sizing, or its conservative legacy fallback"
),
)
parser.add_argument(
"--platform", choices=("esp32", "nrf52"),
+67
View File
@@ -8065,6 +8065,73 @@ class MotatoolIntegrationTests(unittest.TestCase):
)
subprocess.run([cls.motatool, "--version"], check=True, capture_output=True)
def prepare_layout_delta(
self, *, external: bool = False, memory: str | None = None,
authenticated: bool = True,
) -> tuple[ota.MotaInfo, list[mock._Call]]:
# The real tool must size an image larger than its legacy 0x98000
# workspace from the EndF-authenticated geometry, including two spare
# detools segments. Only a small content change needs transferring.
layout = struct.pack(
"<8sBBHIII", b"mOTALay1", 1, 1 if external else 0, 24,
0x27000, 0xED000, 0xED000,
) if authenticated else b""
body = b"A" * (0x99000 - 56 - len(layout)) + layout
base_image = firmware(body, VERSION_OLD)
new_image = firmware(b"B" * 32 + body[32:], VERSION_NEW)
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
base_path = root / "running.bin"
base_path.write_bytes(base_image)
archive_path = root / "release.zip"
with zipfile.ZipFile(archive_path, "w") as archive:
archive.writestr("firmware.bin", new_image)
args = prepare_args(archive_path, self.motatool, base_path)
args.inplace_memory = memory
work = root / "work"
work.mkdir()
with mock.patch.object(ota, "run_checked", wraps=ota.run_checked) as run:
_path, package, expected = ota.prepare_package(
args, target(
platform="nrf52", nrf_sd=external,
base_hash=ota.parse_endf(base_image).body_hash,
boot_codecs=1 << ota.MOTA_CODEC_IN_PLACE,
), work,
)
self.assertFalse(package.is_full)
self.assertEqual(package.codec_id, ota.MOTA_CODEC_IN_PLACE)
self.assertEqual(package.image_size, len(new_image))
self.assertEqual(package.base_hash, ota.parse_endf(base_image).body_hash)
self.assertEqual(expected, ota.parse_endf(new_image).body_hash)
return package, run.call_args_list
def test_authenticated_internal_layout_selects_large_workspace(self) -> None:
with contextlib.redirect_stdout(io.StringIO()) as output:
_package, calls = self.prepare_layout_delta()
build = next(call.args[0] for call in calls if call.kwargs["label"] == "build mOTA")
self.assertNotIn("--inplace-memory", build)
self.assertIn("in-place memory=0x9B000", output.getvalue())
def test_authenticated_external_layout_uses_minimum_workspace(self) -> None:
with contextlib.redirect_stdout(io.StringIO()) as output:
_package, calls = self.prepare_layout_delta(external=True)
build = next(call.args[0] for call in calls if call.kwargs["label"] == "build mOTA")
self.assertNotIn("--inplace-memory", build)
self.assertIn("in-place memory=0x9B000", output.getvalue())
def test_explicit_workspace_is_preserved_and_layout_checked(self) -> None:
with contextlib.redirect_stdout(io.StringIO()) as output:
_package, calls = self.prepare_layout_delta(memory="0xA0000")
build = next(call.args[0] for call in calls if call.kwargs["label"] == "build mOTA")
self.assertEqual(build[build.index("--inplace-memory") + 1], "0xA0000")
self.assertIn("in-place memory=0xA0000", output.getvalue())
with self.assertRaisesRegex(ota.OtaError, "exceeds authenticated nRF52 workspace"):
self.prepare_layout_delta(memory="0xD0000")
def test_legacy_image_retains_conservative_workspace_limit(self) -> None:
with self.assertRaisesRegex(ota.OtaError, "legacy default memory 0x98000 is too small"):
self.prepare_layout_delta(authenticated=False)
def test_raw_esp32_zip_becomes_full_mota(self) -> None:
image = firmware(b"ESP32-new" * 900, VERSION_NEW)
with tempfile.TemporaryDirectory() as directory: