From 8603233a6d399e2aa39bc412f9b5d217d41b0853 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Sat, 1 Aug 2026 20:35:21 +0200 Subject: [PATCH 01/12] build: generate UI before building distributions --- .github/workflows/ci.yml | 4 +++- contrib/build_dist.sh | 1 + contrib/generate-ui.sh | 1 + contrib/generate_setup.sh | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10e2c5153..d4eb98d39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,6 +95,9 @@ jobs: git config --global --add safe.directory ${{ github.workspace }} contrib/build_bin.sh contrib/build_dist.sh + # Verify setup.py regeneration is stable after generating GUI artifacts. + contrib/generate_setup.sh + git diff --exit-code -- setup.py find dist -type f -exec sha256sum {} \; " @@ -242,4 +245,3 @@ jobs: with: device: keepkey runs-on: ubuntu-latest - diff --git a/contrib/build_dist.sh b/contrib/build_dist.sh index e15b9e5d9..8b0b50c35 100755 --- a/contrib/build_dist.sh +++ b/contrib/build_dist.sh @@ -14,6 +14,7 @@ gui_support="${1:---with-gui}"; # Setup poetry and install the dependencies if [[ $gui_support == "--with-gui" ]]; then poetry install -E qt + poetry run contrib/generate-ui.sh else poetry install fi diff --git a/contrib/generate-ui.sh b/contrib/generate-ui.sh index 9fc982ea9..8b6609633 100755 --- a/contrib/generate-ui.sh +++ b/contrib/generate-ui.sh @@ -3,6 +3,7 @@ set -ex pushd hwilib/ui +rm -f ui_*.py for file in *.ui do gen_file=ui_`echo $file| cut -d. -f1`.py diff --git a/contrib/generate_setup.sh b/contrib/generate_setup.sh index 3e3424d8a..6046ffe9a 100755 --- a/contrib/generate_setup.sh +++ b/contrib/generate_setup.sh @@ -5,6 +5,8 @@ set -ex # Setup poetry and install the dependencies poetry install -E qt +# Generate the Python modules so Poetry consistently detects hwilib.ui as a package. +poetry run contrib/generate-ui.sh # Build the source distribution poetry build -f sdist From 68c51cf90495821e34d3fd0907657c4baeea57ab Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 21 Jan 2026 13:08:59 +0100 Subject: [PATCH 02/12] build: add docker / container ignore files --- .containerignore | 11 +++++++++++ .dockerignore | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .containerignore create mode 100644 .dockerignore diff --git a/.containerignore b/.containerignore new file mode 100644 index 000000000..895b58026 --- /dev/null +++ b/.containerignore @@ -0,0 +1,11 @@ +# Exclude local build artifacts and any symlinks outside the context +build* + +# Local documentation files (can be symlinks outside the context) +*.md + +# Common Python / tooling artifacts +__pycache__/ +*.pyc +.venv/ +dist/ diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..895b58026 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +# Exclude local build artifacts and any symlinks outside the context +build* + +# Local documentation files (can be symlinks outside the context) +*.md + +# Common Python / tooling artifacts +__pycache__/ +*.pyc +.venv/ +dist/ From 5c9d86d9364123f997b772326e7fa323752af5a5 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 21 Jan 2026 13:10:32 +0100 Subject: [PATCH 03/12] build: use Podman friendly syntax --- contrib/build-wine.Dockerfile | 2 -- contrib/build.Dockerfile | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/contrib/build-wine.Dockerfile b/contrib/build-wine.Dockerfile index 335b580bb..d781dfdcd 100644 --- a/contrib/build-wine.Dockerfile +++ b/contrib/build-wine.Dockerfile @@ -1,7 +1,5 @@ FROM debian:bookworm-slim -SHELL ["/bin/bash", "-c"] - ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update RUN apt-get install -y \ diff --git a/contrib/build.Dockerfile b/contrib/build.Dockerfile index 4e1025352..766d5b0d1 100644 --- a/contrib/build.Dockerfile +++ b/contrib/build.Dockerfile @@ -1,7 +1,5 @@ FROM debian:bookworm-slim -SHELL ["/bin/bash", "-c"] - ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update RUN apt-get install -y \ @@ -38,7 +36,7 @@ COPY contrib/reproducible-python.diff /opt/reproducible-python.diff ENV PYTHON_CONFIGURE_OPTS="--enable-shared" ENV BUILD_DATE="Jan 1 2019" ENV BUILD_TIME="00:00:00" -RUN eval "$(pyenv init --path)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.9.19 +RUN /bin/bash -c 'eval "$(pyenv init --path)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.9.19' ENV LC_ALL=C.UTF-8 ENV LANG=C.UTF-8 From 45de0be7d14e4c5a754b76f8b0bd4f953b236251 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 3 Feb 2026 10:43:34 +0100 Subject: [PATCH 04/12] build: deterministic builds use Python 3.10 --- contrib/build.Dockerfile | 4 ++-- contrib/build_bin.sh | 7 ++++-- contrib/build_dist.sh | 7 ++++-- contrib/build_wine.sh | 33 ++++++++++++++-------------- docs/development/release-process.rst | 12 +++++----- poetry.lock | 6 ++--- pyproject.toml | 2 +- setup.py | 2 +- 8 files changed, 39 insertions(+), 34 deletions(-) diff --git a/contrib/build.Dockerfile b/contrib/build.Dockerfile index 766d5b0d1..d1d23861c 100644 --- a/contrib/build.Dockerfile +++ b/contrib/build.Dockerfile @@ -34,9 +34,9 @@ ENV PATH="$PYENV_ROOT/bin:$PATH" COPY contrib/reproducible-python.diff /opt/reproducible-python.diff ENV PYTHON_CONFIGURE_OPTS="--enable-shared" -ENV BUILD_DATE="Jan 1 2019" +ENV BUILD_DATE="Jan 1 2026" ENV BUILD_TIME="00:00:00" -RUN /bin/bash -c 'eval "$(pyenv init --path)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.9.19' +RUN /bin/bash -c 'eval "$(pyenv init --path)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.10.20' ENV LC_ALL=C.UTF-8 ENV LANG=C.UTF-8 diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index 2db94f439..48f9a6d3b 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -6,8 +6,11 @@ set -ex ARCH=$(uname -m | tr '[:upper:]' '[:lower:]') +PYTHON_VERSION=3.10.20 + eval "$(pyenv init --path)" eval "$(pyenv virtualenv-init -)" +export PYENV_VERSION="$PYTHON_VERSION" pip install -U pip pip install poetry @@ -21,8 +24,8 @@ else fi # We also need to change the timestamps of all of the base library files -lib_dir=$(pyenv prefix)/lib/python3.9 -TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" '{}' \; +lib_dir=$(pyenv prefix)/lib/python3.10 +TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "202601010000.00" '{}' \; # Make the standalone binary export PYTHONHASHSEED=42 diff --git a/contrib/build_dist.sh b/contrib/build_dist.sh index 8b0b50c35..104d37f96 100755 --- a/contrib/build_dist.sh +++ b/contrib/build_dist.sh @@ -4,8 +4,11 @@ set -ex +PYTHON_VERSION=3.10.20 + eval "$(pyenv init --path)" eval "$(pyenv virtualenv-init -)" +export PYENV_VERSION="$PYTHON_VERSION" pip install -U pip pip install poetry @@ -21,5 +24,5 @@ fi # Make the distribution archives for pypi poetry build -f wheel -# faketime is needed to make sdist detereministic -TZ=UTC faketime -f "2019-01-01 00:00:00" poetry build -f sdist +# faketime is needed to make sdist deterministic +TZ=UTC faketime -f "2026-01-01 00:00:00" poetry build -f sdist diff --git a/contrib/build_wine.sh b/contrib/build_wine.sh index 64d211f97..30efca207 100755 --- a/contrib/build_wine.sh +++ b/contrib/build_wine.sh @@ -3,7 +3,11 @@ set -ex -PYTHON_VERSION=3.9.13 +# Note: Python MSIs/EXEs are no longer hosted for 3.10.x on python.org. +# The NuGet python package currently only goes up to 3.10.11, so Windows builds use that. +PYTHON_VERSION=3.10.11 +PYTHON_NUGET_URL="https://api.nuget.org/v3-flatcontainer/python/${PYTHON_VERSION}/python.${PYTHON_VERSION}.nupkg" +PYTHON_NUGET_HASH="7c6f99b160a36a7e09492dfcff2b0a3a60bb5229ca44cdcc3ecb32871a6144d0" PYTHON_FOLDER="python3" PYHOME="c:/$PYTHON_FOLDER" @@ -19,20 +23,15 @@ WINDOWS_SDK_VERSION=10.0.17763.0 wine 'wineboot' -# Install Python -# Get the PGP keys -wget -O pubkeys.txt -N -c "https://keybase.io/stevedower/pgp_keys.asc?fingerprint=7ed10b6531d7c8e1bc296021fc624643487034e5" -gpg --import pubkeys.txt -rm pubkeys.txt - -# Install python components -for msifile in core dev exe lib pip tools; do - wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/amd64/${msifile}.msi" - wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/amd64/${msifile}.msi.asc" - gpg --verify "${msifile}.msi.asc" "${msifile}.msi" - wine msiexec /i "${msifile}.msi" /qb TARGETDIR=$PYHOME - rm $msifile.msi* -done +# Install Python from NuGet package +wget -O python.nupkg -N -c "$PYTHON_NUGET_URL" +echo "$PYTHON_NUGET_HASH python.nupkg" | sha256sum -c +rm -rf python-nupkg +7z x python.nupkg -opython-nupkg >/dev/null +rm -rf ~/.wine/drive_c/python3 +mkdir -p ~/.wine/drive_c/python3 +cp -a python-nupkg/tools/* ~/.wine/drive_c/python3/ +rm -rf python.nupkg python-nupkg # Get and build libusb wget -N -c -O libusb.tar.bz2 "$LIBUSB_URL" @@ -40,7 +39,7 @@ echo "$LIBUSB_HASH libusb.tar.bz2" | sha256sum -c tar -xf libusb.tar.bz2 pushd "libusb-$LIBUSB_VERSION" ./configure --host=x86_64-w64-mingw32 -faketime -f "2019-01-01 00:00:00" make +faketime -f "2026-01-01 00:00:00" make cp libusb/.libs/libusb-1.0.dll ~/.wine/drive_c/python3/ popd rm -r libusb* @@ -62,7 +61,7 @@ $PYTHON -m pip install poetry # We also need to change the timestamps of all of the base library files lib_dir=~/.wine/drive_c/python3/Lib -TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" '{}' \; +TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "202601010000.00" '{}' \; # Install python dependencies POETRY="wine $PYHOME/Scripts/poetry.exe" diff --git a/docs/development/release-process.rst b/docs/development/release-process.rst index fb739ffdc..16cf86081 100644 --- a/docs/development/release-process.rst +++ b/docs/development/release-process.rst @@ -4,7 +4,7 @@ Release Process 1. Bump version number in ``pyproject.toml`` and ``hwilib/__init__.py``, generate the setup.py file, and git tag release 2. Build distribution archives for PyPi with ``contrib/build_dist.sh`` 3. For MacOS and Linux, use ``contrib/build_bin.sh``. This needs to be run on a macOS machine for the macOS binary and on a Linux machine for the linux one. -4. For Windows, use ``contrib/build_wine.sh`` to build the Windows binary using wine +4. For Windows, use ``contrib/build_wine.sh`` to build the Windows binary using wine. Note that this uses Python 3.10.11 via the NuGet package because python.org no longer hosts the MSIs for newer 3.10.x releases. 5. Make ``SHA256SUMS.txt`` using ``contrib/make_shasums.sh``. 6. Make ``SHA256SUMS.txt.asc`` using ``gpg --clearsign SHA256SUMS.txt`` 7. Upload distribution archives to PyPi @@ -26,7 +26,7 @@ Build everything:: docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_bin.sh && contrib/build_dist.sh" docker run -it --name hwi-wine-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-wine-builder /bin/bash -c "contrib/build_wine.sh" - docker run --platform linux/arm64 -it --rm --name hwi-builder-arm64 -v $PWD:/opt/hwi --workdir /opt/hwi hwi-builder-arm64 /bin/bash -c "contrib/build_bin.sh --without-gui && contrib/build_dist.sh --without-gui" + docker run --platform linux/arm64 -it --rm --name hwi-builder-arm64 -v $PWD:/opt/hwi --workdir /opt/hwi hwi-builder-arm64 /bin/bash -c "contrib/build_bin.sh --without-gui && contrib/build_dist.sh --without-gui" Building macOS binary ===================== @@ -35,14 +35,14 @@ Note that the macOS build is non-deterministic. First install `pyenv `_ using whichever method you prefer. -Then a deterministic build of Python 3.9.19 needs to be installed. This can be done with the patch in ``contrib/reproducible-python.diff``. First ``cd`` into HWI's source tree. Then use:: +Then a deterministic build of Python 3.10.20 needs to be installed. This can be done with the patch in ``contrib/reproducible-python.diff``. First ``cd`` into HWI's source tree. Then use:: - cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2019" BUILD_TIME="00:00:00" pyenv install -kp 3.9.19 + cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2026" BUILD_TIME="00:00:00" pyenv install -kp 3.10.20 -Make sure that python 3.9.19 is active:: +Make sure that python 3.10.20 is active:: $ python --version - Python 3.9.19 + Python 3.10.20 Now install `Poetry `_ with ``pip install poetry`` diff --git a/poetry.lock b/poetry.lock index f7f47dc75..d0f491686 100644 --- a/poetry.lock +++ b/poetry.lock @@ -933,7 +933,7 @@ description = "Python bindings for the Qt cross-platform application and UI fram optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11" groups = ["main"] -markers = "python_version == \"3.9\" and extra == \"qt\"" +markers = "python_version < \"3.11\" and extra == \"qt\"" files = [ {file = "PySide2-5.15.2.1-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:b5e1d92f26b0bbaefff67727ccbb2e1b577f2c0164b349b3d6e80febb4c5bde2"}, {file = "PySide2-5.15.2.1-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:235240b6ec8206d9fdf0232472c6ef3241783d480425e5b54796f06e39ed23da"}, @@ -1017,7 +1017,7 @@ description = "Python / C++ bindings helper module" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11" groups = ["main"] -markers = "python_version == \"3.9\" and extra == \"qt\"" +markers = "python_version < \"3.11\" and extra == \"qt\"" files = [ {file = "shiboken2-5.15.2.1-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:f890f5611ab8f48b88cfecb716da2ac55aef99e2923198cefcf781842888ea65"}, {file = "shiboken2-5.15.2.1-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87079c07587859a525b9800d60b1be971338ce9b371d6ead81f15ee5a46d448b"}, @@ -1311,4 +1311,4 @@ qt = ["pyside2"] [metadata] lock-version = "2.1" python-versions = "^3.9,<3.13" -content-hash = "cfdbc60bf068f88587fe801325c19dfada1e868dbb0cdd65c5b7a7353dc0ca6a" +content-hash = "f1695383c9bf2a946924a19c7aaeda082e6160a43932173e989129ad6794be95" diff --git a/pyproject.toml b/pyproject.toml index 3137310c4..eaf34cab8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ pyaes = "^1.6" mnemonic = "~0" typing-extensions = "^4.4" libusb1 = ">=1.7,<4" -pyside2 = { version = "^5.14.0", optional = true, python = "<3.10" } +pyside2 = { version = "^5.15.2.1", optional = true, python = "<3.11" } cbor2 = ">=5.4.6,<5.8" pyserial = "^3.5" semver = "^3.0.1" diff --git a/setup.py b/setup.py index d6255f68e..79c1cd591 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ 'typing-extensions>=4.4,<5.0'] extras_require = \ -{'qt:python_version < "3.10"': ['pyside2>=5.14.0,<6.0.0']} +{'qt:python_version < "3.11"': ['pyside2>=5.15.2.1,<6.0.0.0']} entry_points = \ {'console_scripts': ['hwi = hwilib._cli:main', 'hwi-qt = hwilib._gui:main']} From edfeace84c614d4a80143eb3412b9f20d204ecc5 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 21 Jan 2026 13:22:14 +0100 Subject: [PATCH 05/12] Drop Python 3.9 support --- .github/workflows/device-test.yml | 2 +- .python-version | 2 +- ci/py39.Dockerfile | 23 -------------- poetry.lock | 51 +++---------------------------- pyproject.toml | 2 +- setup.py | 2 +- test/setup_environment.sh | 2 +- 7 files changed, 10 insertions(+), 74 deletions(-) delete mode 100644 ci/py39.Dockerfile diff --git a/.github/workflows/device-test.yml b/.github/workflows/device-test.yml index 5232314e7..5ec9ede76 100644 --- a/.github/workflows/device-test.yml +++ b/.github/workflows/device-test.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12'] device: - ${{ inputs.device }} test: diff --git a/.python-version b/.python-version index bd28b9c5c..44677e5cc 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.9 +3.10.20 diff --git a/ci/py39.Dockerfile b/ci/py39.Dockerfile deleted file mode 100644 index 9c6f5eeb8..000000000 --- a/ci/py39.Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Cache break (modify this line to break cirrus' dockerfile build cache) 1 - -FROM python:3.9 - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update -RUN apt-get install -y \ - cython3 \ - git \ - libpcsclite-dev \ - libsdl2-dev \ - libsdl2-image-dev \ - libslirp-dev \ - libudev-dev \ - libusb-1.0-0-dev \ - qemu-user-static \ - swig - -RUN pip install poetry flake8 - -ENV LC_ALL=C.UTF-8 -ENV LANG=C.UTF-8 -ENV LANGUAGE=C.UTF-8 diff --git a/poetry.lock b/poetry.lock index d0f491686..b26f93c74 100644 --- a/poetry.lock +++ b/poetry.lock @@ -507,27 +507,6 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -[[package]] -name = "importlib-metadata" -version = "7.0.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, - {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] - [[package]] name = "jinja2" version = "3.1.3" @@ -882,7 +861,6 @@ files = [ [package.dependencies] altgraph = "*" -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} packaging = ">=22.0" pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} @@ -907,7 +885,6 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} packaging = ">=22.0" setuptools = ">=42.0.0" @@ -933,7 +910,7 @@ description = "Python bindings for the Qt cross-platform application and UI fram optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11" groups = ["main"] -markers = "python_version < \"3.11\" and extra == \"qt\"" +markers = "python_version == \"3.10\" and extra == \"qt\"" files = [ {file = "PySide2-5.15.2.1-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:b5e1d92f26b0bbaefff67727ccbb2e1b577f2c0164b349b3d6e80febb4c5bde2"}, {file = "PySide2-5.15.2.1-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:235240b6ec8206d9fdf0232472c6ef3241783d480425e5b54796f06e39ed23da"}, @@ -1017,7 +994,7 @@ description = "Python / C++ bindings helper module" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11" groups = ["main"] -markers = "python_version < \"3.11\" and extra == \"qt\"" +markers = "python_version == \"3.10\" and extra == \"qt\"" files = [ {file = "shiboken2-5.15.2.1-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:f890f5611ab8f48b88cfecb716da2ac55aef99e2923198cefcf781842888ea65"}, {file = "shiboken2-5.15.2.1-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87079c07587859a525b9800d60b1be971338ce9b371d6ead81f15ee5a46d448b"}, @@ -1069,7 +1046,6 @@ babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.18.1,<0.21" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.13" @@ -1252,7 +1228,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -1288,27 +1264,10 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "zipp" -version = "3.17.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-ruff"] - [extras] qt = ["pyside2"] [metadata] lock-version = "2.1" -python-versions = "^3.9,<3.13" -content-hash = "f1695383c9bf2a946924a19c7aaeda082e6160a43932173e989129ad6794be95" +python-versions = "^3.10,<3.13" +content-hash = "32c83e332e591f7a95b48ef02297798399287c93bc25be7730a25f84d20b4f2b" diff --git a/pyproject.toml b/pyproject.toml index eaf34cab8..618a14b32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.9,<3.13" +python = "^3.10,<3.13" hidapi = ">=0.14.0" ecdsa = "~0" pyaes = "^1.6" diff --git a/setup.py b/setup.py index 79c1cd591..8ea9d7019 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,7 @@ 'install_requires': install_requires, 'extras_require': extras_require, 'entry_points': entry_points, - 'python_requires': '>=3.9,<3.13', + 'python_requires': '>=3.10,<3.13', } diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 36788bfdb..069b2387e 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -69,7 +69,7 @@ TREZOR_VERSION="core/v2.9.6" BITBOX01_VERSION="v7.1.0" BITBOX02_VERSION="firmware/v9.24.0" KEEPKEY_VERSION="v7.10.0" -SPECULOS_VERSION="v0.25.10" # Last version supporting Python 3.9 (v0.25.11+ requires >=3.10) +SPECULOS_VERSION="v0.25.10" # v0.25.11+ requires Python >=3.10 JADE_VERSION="1.0.36" # Keep COLDCARD_VERSION in sync with .github/actions/install-sim/action.yml From eab5bad366c580a386c765670f9ce8bd1ce76836 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 3 Jul 2026 16:51:01 +0200 Subject: [PATCH 06/12] test: make device signing cases more granular Most device simulators accept arbitrary keypool paths, so keep testing that behavior by default. Some devices enforce their own derivation path policies. Let those emulators opt out of the arbitrary-path portion while still running the remaining keypool checks. --- test/test_bitbox02.py | 1 + test/test_coldcard.py | 1 + test/test_device.py | 22 +++++++++++++--------- test/test_digitalbitbox.py | 1 + test/test_jade.py | 1 + test/test_keepkey.py | 1 + test/test_trezor.py | 1 + 7 files changed, 19 insertions(+), 9 deletions(-) diff --git a/test/test_bitbox02.py b/test/test_bitbox02.py index a9d64b999..400da4afa 100644 --- a/test/test_bitbox02.py +++ b/test/test_bitbox02.py @@ -43,6 +43,7 @@ def __init__(self, simulator): self.include_xpubs = True self.supports_device_multiple_multisig = True self.supports_legacy = False + self.supports_arbitrary_keypool_paths = True def start(self): super().start() diff --git a/test/test_coldcard.py b/test/test_coldcard.py index c30742b3d..4259d1efd 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -45,6 +45,7 @@ def __init__(self, simulator): self.include_xpubs = False self.supports_device_multiple_multisig = True self.supports_legacy = True + self.supports_arbitrary_keypool_paths = True def start(self): super().start() diff --git a/test/test_device.py b/test/test_device.py index abc6ef1ae..fb88ee72e 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -36,6 +36,7 @@ def __init__(self): self.include_xpubs = None self.supports_device_multiple_multisig = None self.supports_legacy = None + self.supports_arbitrary_keypool_paths = True def start(self): assert self.type is not None @@ -313,13 +314,14 @@ def test_getkeypool(self): addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('bech32')) self.assertTrue(addr_info['hdkeypath'].startswith("m/84h/1h/3h/1/")) - keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', 'm/0h/0h/4h/*', '0', '20']) - self.assertIsInstance(keypool_desc, list, f"getkeypool returned error: {keypool_desc}") - import_result = self.wrpc.importdescriptors(keypool_desc) - self.assertTrue(import_result[0]['success']) - for _ in range(0, 21): - addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'bech32')) - self.assertTrue(addr_info['hdkeypath'].startswith("m/0h/0h/4h/")) + if self.emulator.supports_arbitrary_keypool_paths: + keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', 'm/0h/0h/4h/*', '0', '20']) + self.assertIsInstance(keypool_desc, list, f"getkeypool returned error: {keypool_desc}") + import_result = self.wrpc.importdescriptors(keypool_desc) + self.assertTrue(import_result[0]['success']) + for _ in range(0, 21): + addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'bech32')) + self.assertTrue(addr_info['hdkeypath'].startswith("m/0h/0h/4h/")) keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', '/0h/0h/4h/*', '0', '20']) self.assertEqual(keypool_desc['error'], 'Path must start with m/') @@ -498,10 +500,12 @@ def _test_signtx(self, input_types, multisig_types, external, op_return: bool): in_amt = 1 number_inputs = 0 # Single-sig - if "segwit" in input_types: + if "segwit" in input_types or "sh_wit" in input_types: self.wpk_rpc.sendtoaddress(sh_wpkh_addr, in_amt) + number_inputs += 1 + if "segwit" in input_types or "wit" in input_types: self.wpk_rpc.sendtoaddress(wpkh_addr, in_amt) - number_inputs += 2 + number_inputs += 1 if "legacy" in input_types: self.wpk_rpc.sendtoaddress(pkh_addr, in_amt) number_inputs += 1 diff --git a/test/test_digitalbitbox.py b/test/test_digitalbitbox.py index 1a98c9562..9fc41ed3f 100755 --- a/test/test_digitalbitbox.py +++ b/test/test_digitalbitbox.py @@ -43,6 +43,7 @@ def __init__(self, simulator): self.include_xpubs = False self.supports_device_multiple_multisig = True self.supports_legacy = True + self.supports_arbitrary_keypool_paths = True def start(self): super().start() diff --git a/test/test_jade.py b/test/test_jade.py index 238c88f37..f5fef76dd 100755 --- a/test/test_jade.py +++ b/test/test_jade.py @@ -53,6 +53,7 @@ def __init__(self, jade_qemu_emulator_path): self.include_xpubs = False self.supports_device_multiple_multisig = True self.supports_legacy = True + self.supports_arbitrary_keypool_paths = True def start(self): super().start() diff --git a/test/test_keepkey.py b/test/test_keepkey.py index 845a0976a..ff9cf84c0 100755 --- a/test/test_keepkey.py +++ b/test/test_keepkey.py @@ -65,6 +65,7 @@ def __init__(self, path): self.include_xpubs = False self.supports_device_multiple_multisig = True self.supports_legacy = True + self.supports_arbitrary_keypool_paths = True def start(self): super().start() diff --git a/test/test_trezor.py b/test/test_trezor.py index 335fa571f..b61c1f00a 100755 --- a/test/test_trezor.py +++ b/test/test_trezor.py @@ -64,6 +64,7 @@ def __init__(self, path, model): self.include_xpubs = False self.supports_device_multiple_multisig = True self.supports_legacy = True + self.supports_arbitrary_keypool_paths = True def start(self): super().start() From 976353eded87c8c37a56f67be3b2e3d65368b105 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 21 Jan 2026 17:34:36 +0100 Subject: [PATCH 07/12] Bump Speculos and Ledger Bitcoin app --- .github/workflows/ledger-app-builder.yml | 10 +++++----- .../workflows/ledger-legacy-app-builder.yml | 2 +- test/data/speculos-automation.json | 19 ++++++++++++++++++- test/setup_environment.sh | 2 +- test/test_ledger.py | 4 +++- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ledger-app-builder.yml b/.github/workflows/ledger-app-builder.yml index e97f883b6..19cb9cd0a 100644 --- a/.github/workflows/ledger-app-builder.yml +++ b/.github/workflows/ledger-app-builder.yml @@ -11,14 +11,14 @@ jobs: build: name: Build Bitcoin App runs-on: ${{ inputs.runs-on }} - # Pin to 4.23.0 for SDK v25.9.0 compatibility with Speculos v0.25.10 - container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:4.23.0 + # app-bitcoin v2.5.0 targets SDK v26.5.0, provided by builder 5.3.17. + container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:5.3.17 steps: - run: | - # Pin to v2.4.1 - last version that worked with HWI CI (PR #795 merged Sept 2025) - git clone --branch 2.4.1 --depth 1 https://github.com/LedgerHQ/app-bitcoin.git + # Pin to v2.5.0 + git clone --branch 2.5.0 --depth 1 https://github.com/LedgerHQ/app-bitcoin.git cd app-bitcoin - make DEBUG=1 BOLOS_SDK=$NANOX_SDK + make DEBUG=1 COIN=bitcoin_testnet BOLOS_SDK=$NANOX_SDK - uses: actions/upload-artifact@v4 with: name: ledger_app diff --git a/.github/workflows/ledger-legacy-app-builder.yml b/.github/workflows/ledger-legacy-app-builder.yml index d9c6179bc..28d7a2d45 100644 --- a/.github/workflows/ledger-legacy-app-builder.yml +++ b/.github/workflows/ledger-legacy-app-builder.yml @@ -11,7 +11,7 @@ jobs: build: name: Build Bitcoin Legacy App runs-on: ${{ inputs.runs-on }} - # Pin to 4.23.0 for SDK v25.9.0 compatibility with Speculos v0.25.10 + # Pin to 4.23.0 for SDK v25.9.0 compatibility with Speculos v0.26.9 container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:4.23.0 steps: - run: | diff --git a/test/data/speculos-automation.json b/test/data/speculos-automation.json index 31f69f72a..bc16699d5 100644 --- a/test/data/speculos-automation.json +++ b/test/data/speculos-automation.json @@ -1,6 +1,13 @@ { "version": 1, "rules": [ + { + "regexp": "^(Security risk|High fees warning).*", + "actions": [ + [ "button", 2, true ], + [ "button", 2, false ] + ] + }, { "text": "Confirm", "x": 43, "y": 37, @@ -11,6 +18,16 @@ [ "button", 2, false ] ] }, + { + "text": "Confirm", + "x": 41, "y": 34, + "actions": [ + [ "button", 1, true ], + [ "button", 2, true ], + [ "button", 1, false ], + [ "button", 2, false ] + ] + }, { "text": "Confirm account ", "actions": [ @@ -28,7 +45,7 @@ ] }, { - "regexp": "^(Address|Review|Amount|Fee|Confirm|The derivation|Derivation path|Reject if|The change path|Change path|Register wallet|Policy map|Key|Path|Public key|Spend from|Wallet name|Wallet policy|Descriptor template|Verify Bitcoin|Output|Warning).*", + "regexp": "^(Address|Review|Amount|External amounts|You spend|You receive|Fee|Confirm|The derivation|Derivation path|Reject if|The change path|Change path|Register wallet|Policy map|Key|Path|Public key|Our key|Their key|Unspendable key|Spend from|Spending policy|Primary spending path|Spending path|Transaction output|Wallet name|Wallet policy|Descriptor template|Verify [Bb]itcoin|Output|Warning).*", "actions": [ [ "button", 2, true ], [ "button", 2, false ] diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 069b2387e..c9c2477de 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -69,7 +69,7 @@ TREZOR_VERSION="core/v2.9.6" BITBOX01_VERSION="v7.1.0" BITBOX02_VERSION="firmware/v9.24.0" KEEPKEY_VERSION="v7.10.0" -SPECULOS_VERSION="v0.25.10" # v0.25.11+ requires Python >=3.10 +SPECULOS_VERSION="v0.26.9" # Requires Python >=3.10 (v0.25.11+) JADE_VERSION="1.0.36" # Keep COLDCARD_VERSION in sync with .github/actions/install-sim/action.yml diff --git a/test/test_ledger.py b/test/test_ledger.py index 6ab162e74..00a0ceeb2 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -47,12 +47,14 @@ def __init__(self, path, legacy=False): self.include_xpubs = True self.supports_device_multiple_multisig = True self.supports_legacy = True + # Bitcoin app 2.5.0 enforces standard Ledger paths for xpub derivation. + self.supports_arbitrary_keypool_paths = legacy def start(self): super().start() automation_path = os.path.abspath("data/speculos-automation.json") app_path = f"./apps/btc-test{'-legacy' if self.legacy else ''}.elf" - os.environ["SPECULOS_APPNAME"] = "Bitcoin Test:1.6.6" if self.legacy else "Bitcoin Test:2.4.1" + os.environ["SPECULOS_APPNAME"] = "Bitcoin Test:1.6.6" if self.legacy else "Bitcoin Test:2.5.0" self.emulator_stderr = open('ledger-emulator.stderr', 'a') # Start the emulator From cd8a43174fc806d3c53f5dd9feeef5708339f1c4 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 4 Aug 2026 20:32:01 +0200 Subject: [PATCH 08/12] test: increase stdin command timeout Ledger app 2.5.0 can take over 60 seconds to sign the large transaction fixture. Allow stdin commands up to 120 seconds. --- test/test_device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_device.py b/test/test_device.py index fb88ee72e..33d3955ba 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -175,7 +175,7 @@ def do_command(self, args): args = [f'"{arg}"' for arg in args] input_str = '\n'.join(args) + '\n' proc = subprocess.Popen(['hwi', '--stdin'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) - result = proc.communicate(input_str.encode(), timeout=60) + result = proc.communicate(input_str.encode(), timeout=120) return json.loads(result[0].decode()) else: return process_commands(args) From d2a5874ac9cc36a1c5e058472d86df5e583046a8 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 5 Sep 2025 12:37:35 +0200 Subject: [PATCH 09/12] test: reenable LedgerX tests --- test/test_device.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/test/test_device.py b/test/test_device.py index 33d3955ba..af26c5c78 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -595,20 +595,12 @@ def test_signtx(self): # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 raise unittest.SkipTest("Coldcard sign test temporarily disabled") - if self.emulator.type == "ledger" and not self.emulator.legacy: - # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 - raise unittest.SkipTest("Test temporarily disabled for NanoX") - for addrtypes, multisig_types, external, op_return in self.signtx_cases: with self.subTest(addrtypes=addrtypes, multisig_types=multisig_types, external=external, op_return=op_return): self._test_signtx(addrtypes, multisig_types, external, op_return) # Make a huge transaction which might cause some problems with different interfaces def test_big_tx(self): - if self.emulator.type == "ledger" and not self.emulator.legacy: - # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 - raise unittest.SkipTest("Test temporarily disabled for NanoX") - # make a huge transaction keypool_desc = self.do_command(self.dev_args + ["getkeypool", "--account", "10", "--addr-type", "sh_wit", "0", "100"]) self.assertIsInstance(keypool_desc, list, f"getkeypool returned error: {keypool_desc}") @@ -640,10 +632,6 @@ def test_big_tx(self): class TestDisplayAddress(DeviceTestCase): def test_display_address_path(self): - if self.emulator.type == "ledger" and not self.emulator.legacy: - # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 - raise unittest.SkipTest("Test temporarily disabled for NanoX") - result = self.do_command(self.dev_args + ['displayaddress', "--addr-type", "legacy", '--path', 'm/44h/1h/0h/0/0']) if self.emulator.supports_legacy: self.assertNotIn('error', result) @@ -669,10 +657,6 @@ def test_display_address_bad_path(self): self.assertEqual(result['code'], -7) def test_display_address_descriptor(self): - if self.emulator.type == "ledger" and not self.emulator.legacy: - # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 - raise unittest.SkipTest("Test temporarily disabled for NanoX") - account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/84h/1h/0h'])['xpub'] p2sh_segwit_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/49h/1h/0h'])['xpub'] legacy_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/44h/1h/0h'])['xpub'] @@ -794,10 +778,6 @@ def _check_sign_msg(self, msg): self.assertTrue(self.rpc.verifymessage(addr, sig, msg)) def test_sign_msg(self): - if self.emulator.type == "ledger" and not self.emulator.legacy: - # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927 - raise unittest.SkipTest("Test temporarily disabled for NanoX") - self._check_sign_msg("Message signing test") self._check_sign_msg("285") # Specific test case for Ledger shorter S From 1b0cbea5023fc712e764db488d8df6e0142860a6 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 3 Feb 2026 11:16:13 +0100 Subject: [PATCH 10/12] Skip archiving hwi-qt on non-x86 platforms --- contrib/build_bin.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index 48f9a6d3b..af8c233dc 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -48,7 +48,7 @@ fi target_tarfile="hwi-${VERSION}-${OS}-${ARCH}.tar.gz" -if [[ $gui_support == "--with-gui" ]]; then +if [[ $gui_support == "--with-gui" && $ARCH == "x86_64" ]]; then tar -czf $target_tarfile hwi hwi-qt else tar -czf $target_tarfile hwi From 0379e278589999c51d50a4e8714d8a66d22ec47c Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 3 Feb 2026 15:11:48 +0100 Subject: [PATCH 11/12] Drop Ledger deny rule from tests It's unused and occasionally trips up a test. --- test/data/speculos-automation.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/data/speculos-automation.json b/test/data/speculos-automation.json index bc16699d5..a14eaabd4 100644 --- a/test/data/speculos-automation.json +++ b/test/data/speculos-automation.json @@ -114,13 +114,6 @@ [ "button", 2, false ], [ "setbool", "seen_msg_hash", false ] ] - }, - { - "regexp": "^(Cancel|Reject).*", - "actions": [ - [ "button", 1, true ], - [ "button", 1, false ] - ] } ] } From d27d196def0b24b9f0a1a1c15cad755907583221 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 31 Jul 2026 10:28:32 +0200 Subject: [PATCH 12/12] test: disambiguate Ledger warning automation Limit the warning rule to the centered title so it does not also click the repeated header on the details screen. This prevents the queued extra click from selecting Back to safety before Continue anyway is confirmed. --- test/data/speculos-automation.json | 1 + 1 file changed, 1 insertion(+) diff --git a/test/data/speculos-automation.json b/test/data/speculos-automation.json index a14eaabd4..41cc06129 100644 --- a/test/data/speculos-automation.json +++ b/test/data/speculos-automation.json @@ -3,6 +3,7 @@ "rules": [ { "regexp": "^(Security risk|High fees warning).*", + "y": 34, "actions": [ [ "button", 2, true ], [ "button", 2, false ]