From 50133f63941d072c7dd2268c3f6f9c86502f8ee5 Mon Sep 17 00:00:00 2001 From: JP Hutchins Date: Tue, 14 Apr 2026 09:50:19 -0700 Subject: [PATCH] feat: image format --bypass -> --format option --- .github/workflows/lint.yaml | 2 +- .github/workflows/test.yaml | 2 +- poetry.lock | 145 ++++++++---------------------------- pyproject.toml | 6 +- smpmgr/image_management.py | 66 ++++++++++------ smpmgr/main.py | 110 +++++++++++++-------------- 6 files changed, 135 insertions(+), 196 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 4e5781b..77a516a 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c6fb7b0..d196460 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/poetry.lock b/poetry.lock index 913847d..8ec9d36 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,28 +14,27 @@ files = [ ] [[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" +name = "annotated-doc" +version = "0.0.4" +description = "Document parameters, class attributes, return types, and variables inline, with Annotated." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, + {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, + {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, ] [[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] -markers = "python_version == \"3.10\"" files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] @@ -76,8 +75,6 @@ mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -98,7 +95,6 @@ files = [ ] [package.dependencies] -async-timeout = {version = ">=3.0.0", markers = "python_version < \"3.11\""} dbus-fast = {version = ">=1.83.0", markers = "platform_system == \"Linux\""} pyobjc-core = {version = ">=10.3", markers = "platform_system == \"Darwin\""} pyobjc-framework-CoreBluetooth = {version = ">=10.3", markers = "platform_system == \"Darwin\""} @@ -315,9 +311,6 @@ files = [ {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}, ] -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] @@ -395,25 +388,6 @@ files = [ [package.dependencies] packaging = ">=20.9" -[[package]] -name = "exceptiongroup" -version = "1.3.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, - {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "flake8" version = "6.1.0" @@ -810,7 +784,6 @@ files = [ librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = ">=4.6.0" [package.extras] @@ -1288,11 +1261,9 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -1413,80 +1384,26 @@ pydantic = ">=2.6,<3.0" [[package]] name = "smpclient" -version = "6.1.0" +version = "7.0.0" description = "Simple Management Protocol (SMP) Client for remotely managing MCU firmware" optional = false python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "smpclient-6.1.0-py3-none-any.whl", hash = "sha256:b3aa7318f4084de6c4c6b635ea19a4cf968906a73a04f0f473a9ac6e31fb4696"}, - {file = "smpclient-6.1.0.tar.gz", hash = "sha256:580728400cf4164dd525dd4913ebfd93404879aa06017b40b454454d9c9d9638"}, + {file = "smpclient-7.0.0-py3-none-any.whl", hash = "sha256:e570defc262f8c8ae5f2b2b4687bab461a8b431317a10e41f581c79c24cfcd22"}, + {file = "smpclient-7.0.0.tar.gz", hash = "sha256:3bd9731176746c4b82c1bfc3d906f32900d43f8b1006bcbc89e92addf3cab467"}, ] [package.dependencies] -async-timeout = {version = ">=4.0.3,<5.0.0", markers = "python_version < \"3.11\""} -bleak = ">=2.0.0,<3.0.0" -intelhex = ">=2.3.0,<3.0.0" -pyserial = ">=3.5,<4.0" -smp = ">=4.0.2,<5.0.0" +bleak = {version = ">=2.0.0", optional = true, markers = "extra == \"all\""} +intelhex = ">=2.3.0" +pyserial = {version = ">=3.5", optional = true, markers = "extra == \"all\""} +smp = ">=4.0.2" -[[package]] -name = "tomli" -version = "2.4.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_full_version <= \"3.11.0a6\"" -files = [ - {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, - {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, - {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, - {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, - {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, - {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, - {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, - {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, - {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, - {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, - {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, - {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, - {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, - {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, - {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, - {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, - {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, - {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, - {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, -] +[package.extras] +all = ["bleak (>=2.0.0)", "pyserial (>=3.5)"] +ble = ["bleak (>=2.0.0)"] +serial = ["pyserial (>=3.5)"] [[package]] name = "tomlkit" @@ -1502,21 +1419,21 @@ files = [ [[package]] name = "typer" -version = "0.16.1" +version = "0.24.1" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "typer-0.16.1-py3-none-any.whl", hash = "sha256:90ee01cb02d9b8395ae21ee3368421faf21fa138cb2a541ed369c08cec5237c9"}, - {file = "typer-0.16.1.tar.gz", hash = "sha256:d358c65a464a7a90f338e3bb7ff0c74ac081449e53884b12ba658cbd72990614"}, + {file = "typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e"}, + {file = "typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45"}, ] [package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" +annotated-doc = ">=0.0.2" +click = ">=8.2.1" +rich = ">=12.3.0" shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" [[package]] name = "types-pyserial" @@ -1880,5 +1797,5 @@ all = ["winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt- [metadata] lock-version = "2.1" -python-versions = ">=3.10, <4" -content-hash = "ebf0c6617f6c6fb924e90bd0375d38833663a59fa9b4d116159c11f73785dc63" +python-versions = ">=3.11, <4" +content-hash = "3637352439bf1c3cbe9e681aa7bbbf3f217d05e1bd13d553309a0a4cd8773805" diff --git a/pyproject.toml b/pyproject.toml index de05a4e..ef7a73d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,9 @@ pattern = '(?P\d+\.\d+\.\d+)' format-jinja = "{% if distance == 0 %}{{ base }}{% if dirty %}+dirty{% endif %}{% else %}{{ base }}-dev{{ distance }}+g{{ commit }}{% if dirty %}.dirty{% endif %}{% endif %}" [tool.poetry.dependencies] -python = ">=3.10, <4" -smpclient = "^6.1.0" -typer = { extras = ["all"], version = "^0.16.0" } +python = ">=3.11, <4" +smpclient = { extras = ["all"], version = "^7" } +typer = { extras = ["all"], version = "^0.24.0" } readchar = "^4.0.5" [tool.poetry.group.dev.dependencies] diff --git a/smpmgr/image_management.py b/smpmgr/image_management.py index 2261f34..c42fa3d 100644 --- a/smpmgr/image_management.py +++ b/smpmgr/image_management.py @@ -2,9 +2,10 @@ import asyncio import logging +from enum import StrEnum, unique from io import BufferedReader from pathlib import Path -from typing import Annotated, cast +from typing import Annotated, TypeAlias, assert_never, cast import typer from rich import print @@ -24,6 +25,32 @@ from smpmgr.common import Options, connect_with_spinner, get_smpclient, smp_request + +@unique +class ImageFormat(StrEnum): + MCUBOOT = "mcuboot" + ANY = "any" + + +ImageFormatOption: TypeAlias = Annotated[ + ImageFormat, + typer.Option( + "--format", + help="The expected image format for local inspection. " + "'mcuboot' (default) inspects the image as an MCUboot image before upload. " + "'any' skips local MCUboot inspection and does not attempt to interpret the file " + "as an MCUboot image. " + "This is useful when uploading images that are not in MCUboot format, such as " + "custom bootloader formats (e.g., NXP's SB3.1). " + "[bold red]WARNING[/bold red]: When using --format=any, the responsibility for " + "validating image integrity is placed entirely on the device's bootloader. " + "If the bootloader does not verify the image, corrupted firmware could be uploaded. " + "[bold red]Only use --format=any if your bootloader performs its own image integrity " + "validation.[/bold red]", + ), +] + + app = typer.Typer(name="image", help="The SMP Image Management Group.") logger = logging.getLogger(__name__) @@ -174,30 +201,25 @@ def upload( ctx: typer.Context, file: Annotated[Path, typer.Argument(help="Path to FW image")], slot: Annotated[int, typer.Option(help="The image slot to upload to")] = 0, - bypass_inspect: Annotated[ - bool, - typer.Option( - "--bypass-inspect", - help="Skip local MCUboot image inspection. " - "This is useful when uploading images that are not in MCUboot format, such as " - "custom bootloader formats (e.g., NXP's SB3.1). " - "[bold red]WARNING[/bold red]: When using this option, the responsibility for " - "validating image integrity is placed entirely on the device's bootloader. " - "If the bootloader does not verify the image, corrupted firmware could be uploaded. " - "[bold red]Only use this option if your bootloader performs its own image integrity " - "validation.[/bold red]", - ), - ] = False, + format: ImageFormatOption = ImageFormat.MCUBOOT, ) -> None: """Upload a FW image.""" - if not bypass_inspect: - try: - image_info = ImageInfo.load_file(str(file)) - logger.info(str(image_info)) - except Exception: - logger.exception("Inspection of FW image failed") - raise typer.Exit(code=1) + match format: + case ImageFormat.MCUBOOT: + try: + image_info = ImageInfo.load_file(str(file)) + logger.info(str(image_info)) + except Exception: + logger.exception( + "Inspection of FW image failed. " + "If this is not an MCUboot image, retry with --format=any." + ) + raise typer.Exit(code=1) + case ImageFormat.ANY: + pass + case _ as unreachable: + assert_never(unreachable) options = cast(Options, ctx.obj) smpclient = get_smpclient(options) diff --git a/smpmgr/main.py b/smpmgr/main.py index 8cbf22d..049690d 100644 --- a/smpmgr/main.py +++ b/smpmgr/main.py @@ -13,7 +13,7 @@ from smp import error as smperr from smp.os_management import OS_MGMT_RET_RC from smpclient.generics import error, error_v1, error_v2, success -from smpclient.mcuboot import IMAGE_TLV, ImageInfo, TLVNotFound +from smpclient.mcuboot import IMAGE_TLV, ImageInfo, ImageTLVValue, TLVNotFound from smpclient.requests.image_management import ImageStatesRead, ImageStatesWrite from smpclient.requests.os_management import ResetWrite from typing_extensions import Annotated, assert_never @@ -34,7 +34,7 @@ get_smpclient, smp_request, ) -from smpmgr.image_management import upload_with_progress_bar +from smpmgr.image_management import ImageFormat, ImageFormatOption, upload_with_progress_bar from smpmgr.logging import LogLevel, setup_logging from smpmgr.plugins import get_plugins from smpmgr.user import intercreate @@ -169,42 +169,37 @@ def upgrade( "(or some other mechanism).", ), ] = False, - bypass_inspect: Annotated[ - bool, - typer.Option( - "--bypass-inspect", - help="Skip local MCUboot image inspection and read the image hash from the device " - "instead of extracting it from the file. " - "This is useful when uploading images that are not in MCUboot format, such as " - "custom bootloader formats (e.g., NXP's SB3.1) where the hash may be calculated " - "differently (e.g., over a specific block rather than the entire binary). " - "[bold red]WARNING[/bold red]: When using this option, the responsibility for " - "validating image integrity is placed entirely on the device's bootloader. " - "If the bootloader does not verify the image, corrupted firmware could be uploaded " - "and marked as valid. " - "It's assumed the image format encodes some sort of integrity check " - "(e.g., CRC or hash)." - "[bold red]Only use this option if your bootloader performs its own image integrity " - "validation.[/bold red]", - ), - ] = False, + format: ImageFormatOption = ImageFormat.MCUBOOT, ) -> None: """Upload a FW image, mark it for next boot, and reset the device.""" - if not bypass_inspect: - try: - image_info = ImageInfo.load_file(str(file)) - logger.info(str(image_info)) - except Exception as e: - typer.echo(f"Inspection of FW image failed: {e}") - raise typer.Exit(code=1) + image_tlv_sha256: ImageTLVValue | None = None + + match format: + case ImageFormat.MCUBOOT: + try: + image_info = ImageInfo.load_file(str(file)) + logger.info(str(image_info)) + except Exception as e: + typer.echo( + f"Inspection of FW image failed: {e}\n" + "If this is not an MCUboot image, retry with --format=any." + ) + raise typer.Exit(code=1) - try: - image_tlv_sha256 = image_info.get_tlv(IMAGE_TLV.SHA256) - logger.info(f"IMAGE_TLV_SHA256: {image_tlv_sha256}") - except TLVNotFound: - typer.echo("Could not find IMAGE_TLV_SHA256 in image.") - raise typer.Exit(code=1) + try: + image_tlv_sha256 = image_info.get_tlv(IMAGE_TLV.SHA256) + logger.info(f"IMAGE_TLV_SHA256: {image_tlv_sha256}") + except TLVNotFound: + typer.echo( + "Could not find IMAGE_TLV_SHA256 in image. " + "If this is not an MCUboot image, retry with --format=any." + ) + raise typer.Exit(code=1) + case ImageFormat.ANY: + pass + case _ as unreachable: + assert_never(unreachable) options = cast(Options, ctx.obj) smpclient = get_smpclient(options) @@ -217,28 +212,33 @@ async def f() -> None: await upload_with_progress_bar(smpclient, f, slot) if slot != 0 or confirm: - if bypass_inspect: - # Read hash from device since we skipped local image inspection - r = await smp_request(smpclient, ImageStatesRead(), "Waiting for image states...") - - if error(r): - print(r) - raise typer.Exit(code=1) - elif success(r): - if len(r.images) == 0: - print("No images on device!") - raise typer.Exit(code=1) - for image in r.images: - if image.slot == slot: - image_hash = image.hash - break - if image_hash is None: - print(f"Image with slot {slot} not found!") + match format: + case ImageFormat.MCUBOOT: + assert image_tlv_sha256 is not None + image_hash = image_tlv_sha256.value + case ImageFormat.ANY: + r = await smp_request( + smpclient, ImageStatesRead(), "Waiting for image states..." + ) + + if error(r): + print(r) raise typer.Exit(code=1) - else: - assert_never(r) - else: - image_hash = image_tlv_sha256.value + elif success(r): + if len(r.images) == 0: + print("No images on device!") + raise typer.Exit(code=1) + for image in r.images: + if image.slot == slot: + image_hash = image.hash + break + if image_hash is None: + print(f"Image with slot {slot} not found!") + raise typer.Exit(code=1) + else: + assert_never(r) + case _ as unreachable: + assert_never(unreachable) image_states_response = await smp_request( smpclient,