From eb36bd6980f094ffcf3f0b3d1a1decd1656a7d60 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 3 Jul 2026 17:38:43 +0200 Subject: [PATCH 01/15] test: pin Trezor T Rust nightly to 2025-04-15 The rolling nightly toolchain has drifted past what trezor-firmware core/v2.9.6 supports: recent nightlies reject its reexport_test_harness_main attribute with error E0658. Pin the nightly to 2025-04-15, matching the firmware's own shell.nix. Co-authored-by: Claude (Fable 5) --- test/setup_environment.sh | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 7d602a5f1..36788bfdb 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -115,12 +115,15 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then fi if [[ -n ${build_trezor_t} ]]; then - rustup update - rustup toolchain uninstall nightly - rustup toolchain install nightly - rustup default nightly - rustup component add rustfmt - rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu + # Pin the Rust nightly to the one trezor-firmware ${TREZOR_VERSION} expects + # (see its shell.nix: rust-bin.nightly."2025-04-15"). The rolling "nightly" + # channel has drifted and newer nightlies reject the firmware's + # reexport_test_harness_main attribute with error E0658. + TREZOR_RUST_NIGHTLY="nightly-2025-04-15" + rustup toolchain install ${TREZOR_RUST_NIGHTLY} + rustup default ${TREZOR_RUST_NIGHTLY} + rustup component add rustfmt --toolchain ${TREZOR_RUST_NIGHTLY} + rustup component add rust-src --toolchain ${TREZOR_RUST_NIGHTLY}-x86_64-unknown-linux-gnu # Build trezor t emulator. This is pretty fast, so rebuilding every time is ok # But there should be some caching that makes this faster git am ../../data/trezor-t-build.patch From 4d974551d11878152ae4f9f239963b4b555761aa Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 3 Jul 2026 18:40:46 +0200 Subject: [PATCH 02/15] psbt: don't overwrite PSBTv2 tx version and fallback locktime cache_unsigned_tx_pieces() decided whether to call setup_from_tx() by checking if self.tx is None. But it never is: __init__ sets it to an empty CTransaction. So for PSBTv2, which has no global transaction, setup_from_tx() replaced the deserialized tx version and fallback locktime with the empty transaction's defaults (version 1, locktime 0). This silently modified any PSBTv2 transaction with a different version or locktime, such as those made by Bitcoin Core (version 2 and an anti-fee-sniping locktime), invalidating signatures created before the round-trip through HWI. Check the PSBT version instead, as the docstring already describes. Co-authored-by: Claude (Fable 5) --- hwilib/psbt.py | 7 +++++-- test/test_psbt.py | 26 +++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/hwilib/psbt.py b/hwilib/psbt.py index 28b5e1888..9600ae59d 100644 --- a/hwilib/psbt.py +++ b/hwilib/psbt.py @@ -1046,8 +1046,11 @@ def cache_unsigned_tx_pieces(self) -> None: Does nothing if the PSBT is already v2. """ # To make things easier, we split up the global transaction - # and use the PSBTv2 fields for PSBTv0 - if self.tx is not None: + # and use the PSBTv2 fields for PSBTv0. + # Don't check self.tx instead of self.version: for PSBTv2 it is + # merely an empty placeholder, and its default version and locktime + # would overwrite the deserialized values. + if self.version == 0: self.setup_from_tx(self.tx) def setup_from_tx(self, tx: CTransaction): diff --git a/test/test_psbt.py b/test/test_psbt.py index 3a585b060..face4631a 100755 --- a/test/test_psbt.py +++ b/test/test_psbt.py @@ -1,6 +1,6 @@ #! /usr/bin/env python3 -from hwilib.psbt import PSBT +from hwilib.psbt import PSBT, PartiallySignedInput, PartiallySignedOutput from hwilib.errors import PSBTSerializationError import json import os @@ -28,5 +28,29 @@ def test_valid_psbt(self): serd = psbt.serialize() self.assertEqual(valid, serd) + def test_v2_tx_version_and_fallback_locktime_roundtrip(self): + psbt = PSBT() + psbt.version = 2 + psbt.tx_version = 2 + psbt.fallback_locktime = 123456 + + psbt_in = PartiallySignedInput(psbt.version) + psbt_in.prev_txid = bytes.fromhex("11" * 32) + psbt_in.prev_out = 0 + psbt.inputs.append(psbt_in) + + psbt_out = PartiallySignedOutput(psbt.version) + psbt_out.amount = 1000 + psbt_out.script = bytes.fromhex("51") + psbt.outputs.append(psbt_out) + + serialized = psbt.serialize() + parsed = PSBT() + parsed.deserialize(serialized) + + self.assertEqual(parsed.tx_version, 2) + self.assertEqual(parsed.fallback_locktime, 123456) + self.assertEqual(parsed.serialize(), serialized) + if __name__ == "__main__": unittest.main() From 45cac039b26cef0c8d1a61875d1953b20b1dc609 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 3 Feb 2026 10:25:33 +0100 Subject: [PATCH 03/15] ci: drop cirrus leftovers --- README.md | 2 +- ci/cirrus.Dockerfile | 99 -------------------------------------------- ci/py310.Dockerfile | 2 +- ci/py311.Dockerfile | 2 +- setup.py | 2 +- 5 files changed, 4 insertions(+), 103 deletions(-) delete mode 100644 ci/cirrus.Dockerfile diff --git a/README.md b/README.md index c80374b18..ec1f8fab2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Bitcoin Hardware Wallet Interface -[![Build Status](https://api.cirrus-ci.com/github/bitcoin-core/HWI.svg)](https://cirrus-ci.com/github/bitcoin-core/HWI) +[![CI](https://github.com/bitcoin-core/HWI/actions/workflows/ci.yml/badge.svg)](https://github.com/bitcoin-core/HWI/actions/workflows/ci.yml) [![Documentation Status](https://readthedocs.org/projects/hwi/badge/?version=latest)](https://hwi.readthedocs.io/en/latest/?badge=latest) The Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets. diff --git a/ci/cirrus.Dockerfile b/ci/cirrus.Dockerfile deleted file mode 100644 index 07c201c6e..000000000 --- a/ci/cirrus.Dockerfile +++ /dev/null @@ -1,99 +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 \ - autotools-dev \ - automake \ - bsdmainutils \ - build-essential \ - ccache \ - clang \ - cmake \ - curl \ - cython3 \ - gcc-arm-none-eabi \ - gcc-arm-linux-gnueabihf \ - git \ - libboost-system-dev \ - libboost-filesystem-dev \ - libboost-chrono-dev \ - libboost-test-dev \ - libboost-thread-dev \ - libc6-dev-armhf-cross \ - libdb-dev \ - libdb++-dev \ - libevent-dev \ - libgcrypt20-dev \ - libnewlib-arm-none-eabi \ - libpcsclite-dev \ - libsdl2-dev \ - libsdl2-image-dev \ - libssl-dev \ - libslirp-dev \ - libtool \ - libudev-dev \ - libusb-1.0-0-dev \ - ninja-build \ - pkg-config \ - qemu-user-static \ - swig - -RUN pip install poetry flake8 -RUN wget https://static.rust-lang.org/rustup/dist/x86_64-unknown-linux-gnu/rustup-init -RUN chmod +x rustup-init && ./rustup-init -y -ENV PATH="/root/.cargo/bin:$PATH" -RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v22.0/protoc-22.0-linux-x86_64.zip -RUN unzip protoc-22.0-linux-x86_64.zip -d /usr/local -RUN protoc --version - -#################### -# Local build/test steps -# ----------------- -# To install all simulators/tests locally, uncomment the block below, -# then build the docker image and interactively run the tests -# as needed. -# e.g., -# docker build -f ci/cirrus.Dockerfile -t hwi_test . -# docker run -it --entrypoint /bin/bash hwi_test -# cd test; poetry run ./run_tests.py --ledger --coldcard --interface=cli --device-only -# For BitBox02: -# docker build -f ci/cirrus.Dockerfile -t hwi_test . -# ./ci/build_bitbox02.sh -# docker run -it -v bitbox02_volume:/test/work/bitbox02-firmware --name hwi --entrypoint /bin/bash hwi_test -# cd test; poetry run ./run_tests.py --bitbox02 --interface=cli --device-only -#################### - -#################### -#ENV EMAIL=email -#COPY pyproject.toml pyproject.toml -#RUN poetry run pip install construct pyelftools mnemonic jsonschema -# -## Set up environments first to take advantage of layer caching -#RUN mkdir test -#COPY test/setup_environment.sh test/setup_environment.sh -#COPY test/data/coldcard-multisig.patch test/data/coldcard-multisig.patch -## One by one to allow for intermediate caching of successful builds -#RUN cd test; ./setup_environment.sh --trezor-1 -#RUN cd test; ./setup_environment.sh --trezor-t -#RUN cd test; ./setup_environment.sh --coldcard -#RUN cd test; ./setup_environment.sh --bitbox01 -#RUN cd test; ./setup_environment.sh --ledger -#RUN cd test; ./setup_environment.sh --keepkey -#RUN cd test; ./setup_environment.sh --jade -#RUN cd test; ./setup_environment.sh --bitcoind -# -## Once everything has been built, put rest of files in place -## which have higher turn-over. -#COPY test/ test/ -#COPY hwi.py hwi-qt.py README.md / -#COPY hwilib/ /hwilib/ -#RUN poetry install -# -#################### - -ENV LC_ALL=C.UTF-8 -ENV LANG=C.UTF-8 -ENV LANGUAGE=C.UTF-8 diff --git a/ci/py310.Dockerfile b/ci/py310.Dockerfile index 299b44333..20106aa24 100644 --- a/ci/py310.Dockerfile +++ b/ci/py310.Dockerfile @@ -1,4 +1,4 @@ -# Cache break (modify this line to break cirrus' dockerfile build cache) 1 +# Cache break (modify this line to break the dockerfile build cache) 1 FROM python:3.10 diff --git a/ci/py311.Dockerfile b/ci/py311.Dockerfile index ab80e889a..cf97b42cf 100644 --- a/ci/py311.Dockerfile +++ b/ci/py311.Dockerfile @@ -1,4 +1,4 @@ -# Cache break (modify this line to break cirrus' dockerfile build cache) +# Cache break (modify this line to break the dockerfile build cache) FROM python:3.11 diff --git a/setup.py b/setup.py index e821a4ca2..7589240d6 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ 'name': 'hwi', 'version': '3.2.0', 'description': 'A library for working with Bitcoin hardware wallets', - 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://api.cirrus-ci.com/github/bitcoin-core/HWI.svg)](https://cirrus-ci.com/github/bitcoin-core/HWI)\n[![Documentation Status](https://readthedocs.org/projects/hwi/badge/?version=latest)](https://hwi.readthedocs.io/en/latest/?badge=latest)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\nCaveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependencies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path are known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Documentation\n\nDocumentation for HWI can be found on [readthedocs.io](https://hwi.readthedocs.io/).\n\n### Device Support\n\nFor documentation on devices supported and how they are supported, please check the [device support page](https://hwi.readthedocs.io/en/latest/devices/index.html#support-matrix)\n\n### Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](https://hwi.readthedocs.io/en/latest/examples/bitcoin-core-usage.html).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", + 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![CI](https://github.com/bitcoin-core/HWI/actions/workflows/ci.yml/badge.svg)](https://github.com/bitcoin-core/HWI/actions/workflows/ci.yml)\n[![Documentation Status](https://readthedocs.org/projects/hwi/badge/?version=latest)](https://hwi.readthedocs.io/en/latest/?badge=latest)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\nCaveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependencies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path are known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Documentation\n\nDocumentation for HWI can be found on [readthedocs.io](https://hwi.readthedocs.io/).\n\n### Device Support\n\nFor documentation on devices supported and how they are supported, please check the [device support page](https://hwi.readthedocs.io/en/latest/devices/index.html#support-matrix)\n\n### Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](https://hwi.readthedocs.io/en/latest/examples/bitcoin-core-usage.html).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", 'author': 'Ava Chow', 'author_email': 'me@achow101.com', 'maintainer': 'None', From e76dcb6715aed21acf513a8533cbc174da1c9ede Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 3 Feb 2026 10:28:26 +0100 Subject: [PATCH 04/15] ci: drop unused Python 3.7 Docker file Also drops Python 3.6 dataclasses leftover. --- ci/py37.Dockerfile | 23 ----------------------- poetry.lock | 2 +- pyproject.toml | 1 - setup.py | 3 +-- 4 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 ci/py37.Dockerfile diff --git a/ci/py37.Dockerfile b/ci/py37.Dockerfile deleted file mode 100644 index fc8b67781..000000000 --- a/ci/py37.Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Cache break (modify this line to break cirrus' dockerfile build cache) 1 - -FROM python:3.7 - -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 88af47e0e..a11e72cbb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1311,4 +1311,4 @@ qt = ["pyside2"] [metadata] lock-version = "2.1" python-versions = "^3.9,<3.13" -content-hash = "ffa2aebd594ec1a5db15d7325c7bb26036b593faccf363163379e276bff1c191" +content-hash = "278dc524a94cc0c503225e4a20f353bd09fa3deaab956e9d559c3795a304ad65" diff --git a/pyproject.toml b/pyproject.toml index ab98299ba..034ae7935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ libusb1 = ">=1.7,<4" pyside2 = { version = "^5.14.0", optional = true, python = "<3.10" } cbor2 = ">=5.4.6,<5.8" pyserial = "^3.5" -dataclasses = {version = "^0.8", python = ">=3.6,<3.7"} semver = "^3.0.1" noiseprotocol = "^0.3.1" protobuf = "^4.23.3" diff --git a/setup.py b/setup.py index 7589240d6..d6255f68e 100644 --- a/setup.py +++ b/setup.py @@ -39,8 +39,7 @@ 'typing-extensions>=4.4,<5.0'] extras_require = \ -{':python_version == "3.6"': ['dataclasses>=0.8,<0.9'], - 'qt:python_version < "3.10"': ['pyside2>=5.14.0,<6.0.0']} +{'qt:python_version < "3.10"': ['pyside2>=5.14.0,<6.0.0']} entry_points = \ {'console_scripts': ['hwi = hwilib._cli:main', 'hwi-qt = hwilib._gui:main']} From 3dcdbd10035261937bcb35e48c6c44c079ce746d Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 3 Feb 2026 10:29:43 +0100 Subject: [PATCH 05/15] Regenerate setup.py This reduces churn when running this command again for the upcoming commits. --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index d6255f68e..2eb65578a 100644 --- a/setup.py +++ b/setup.py @@ -17,11 +17,10 @@ 'hwilib.devices.ledger_bitcoin.ledgercomm', 'hwilib.devices.ledger_bitcoin.ledgercomm.interfaces', 'hwilib.devices.trezorlib', - 'hwilib.devices.trezorlib.transport', - 'hwilib.ui'] + 'hwilib.devices.trezorlib.transport'] package_data = \ -{'': ['*'], 'hwilib': ['udev/*']} +{'': ['*'], 'hwilib': ['udev/*', 'ui/*']} modules = \ ['hwi', 'hwi-qt'] From 8244d4bc51adcbf080e3e9509f96c5199a3be88c Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 21 Jan 2026 13:08:59 +0100 Subject: [PATCH 06/15] 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 a43dbcf2bf95553f628b3ac784d6feb6f4432efd Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 21 Jan 2026 13:10:32 +0100 Subject: [PATCH 07/15] 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 ea884ef36a79d3f806898b87d4858f7849dc5f93 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 3 Feb 2026 10:43:34 +0100 Subject: [PATCH 08/15] 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 e15b9e5d9..c4fec9a3e 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 @@ -20,5 +23,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 a11e72cbb..096580a5f 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 = "278dc524a94cc0c503225e4a20f353bd09fa3deaab956e9d559c3795a304ad65" +content-hash = "7409ca07c43208b622b22c5ae02b79cf9383c08c06302748c8d5c6ecbcf6f31c" diff --git a/pyproject.toml b/pyproject.toml index 034ae7935..b7c6a2943 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 2eb65578a..120d44577 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,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 f3ecf5c56da15d16989ea7c0979603504c6836da Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 21 Jan 2026 13:22:14 +0100 Subject: [PATCH 09/15] 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 096580a5f..7637723d7 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 = "7409ca07c43208b622b22c5ae02b79cf9383c08c06302748c8d5c6ecbcf6f31c" +python-versions = "^3.10,<3.13" +content-hash = "304d5c9033aed625eb1f8cbbc2f23019fb708171bec6852c28ea8626221c0df0" diff --git a/pyproject.toml b/pyproject.toml index b7c6a2943..4ed817acc 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 120d44577..60a2a2783 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,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 e8b403349da2b2bf86a951d30f4ab6a60c7000a2 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 3 Jul 2026 16:51:01 +0200 Subject: [PATCH 10/15] 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 d22fdc386..b2029d6ed 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 5be838a5de61deec3affca3519e4b1da91147db9 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 21 Jan 2026 17:34:36 +0100 Subject: [PATCH 11/15] Bump Speculos and Ledger Bitcoin app Also demote discarded-qualifiers warnings in Speculos' bundled deps so -Werror builds keep working with the updated simulator. --- .github/workflows/ledger-app-builder.yml | 13 ++++++++----- .../workflows/ledger-legacy-app-builder.yml | 2 +- test/data/speculos-automation.json | 19 ++++++++++++++++++- test/setup_environment.sh | 14 ++++++++++++-- test/test_ledger.py | 4 +++- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ledger-app-builder.yml b/.github/workflows/ledger-app-builder.yml index d873a2411..6bfc73e4e 100644 --- a/.github/workflows/ledger-app-builder.yml +++ b/.github/workflows/ledger-app-builder.yml @@ -11,14 +11,17 @@ 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 + # Pin to 5.3.2 for SDK compatibility with app-bitcoin-new v2.4.6 + container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:5.3.2 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-new.git + # Pin to v2.4.6 + git clone --branch 2.4.6 --depth 1 https://github.com/LedgerHQ/app-bitcoin-new.git cd app-bitcoin-new - make DEBUG=1 BOLOS_SDK=$NANOX_SDK + # Work around register-wallet policy parsing/display bugs fixed after v2.4.6. + git fetch --depth 2 origin 24bd597ee9c60fe58b522360011437d6a8679d33 a2ba8560c9306c53bd30840bd2333cecd7f47ae4 + git -c user.name="HWI CI" -c user.email="hwi-ci@example.invalid" cherry-pick 24bd597ee9c60fe58b522360011437d6a8679d33 a2ba8560c9306c53bd30840bd2333cecd7f47ae4 + 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 aac5afb1a..bc4ae0ffb 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 f17a9c668..9d821fd2f 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|To|Output|Warning).*", + "regexp": "^(Address|Review|Amount|Fee|Confirm|The derivation|Derivation path|The change path|Change path|Register wallet|Policy map|Key|Path|Public key|Our key|Their key|Unspendable key|Spend from|Transaction output|Wallet name|Wallet policy|Descriptor template|Verify [Bb]itcoin|To|Output|Warning).*", "actions": [ [ "button", 2, true ], [ "button", 2, false ] diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 069b2387e..b00506e63 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 @@ -295,9 +295,19 @@ if [[ -n ${build_ledger} ]]; then cd speculos + # Work around -Werror build failures in Speculos' bundled deps. + # GCC < 15 errors out on unknown "-Wno-error=..." options, so only add the + # unterminated-string-initialization suppression when the compiler supports it. + CFLAGS="-O -fno-builtin -fPIC -Wall -Wextra -Werror -Wno-error=maybe-uninitialized -Wno-error=array-parameter -Wno-error=array-bounds -Wno-error=discarded-qualifiers" + cc_major=$(cc -dumpversion | cut -d. -f1) + if [ "${cc_major:-0}" -ge 15 ]; then + CFLAGS="$CFLAGS -Wno-error=unterminated-string-initialization" + fi + export CFLAGS + # Build the simulator. This is cached, but it is also fast mkdir -p build - cmake -Bbuild -S . + cmake -Bbuild -S . -DCMAKE_C_FLAGS="${CFLAGS}" make -C build/ cd .. diff --git a/test/test_ledger.py b/test/test_ledger.py index 6ab162e74..5265deeba 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.4.6 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.4.6" self.emulator_stderr = open('ledger-emulator.stderr', 'a') # Start the emulator From be87c6ebdf55ed24561a8c3b633cc488dfd08f49 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Sat, 4 Jul 2026 12:10:41 +0200 Subject: [PATCH 12/15] test: only match Ledger "To" screen on the title row The Speculos automation file advances through screens by matching text fragments. The rule for the "To" screen matched any fragment starting with "To", including parts of the destination address shown below that title. This is what failed in CI run 28680592019 (job 85066632783). The address mzmauywUy3WF1TX3YxzQMA5PR4zXqJVLTo was split on the device screen into "mzmauywUy3WF1TX", "3YxzQMA5PR4zXqJVL" and "To". The latter confused the automation rule for "To", which pressed an extra right button: automation: getting actions for "To" (57, 3) automation: getting actions for "mzmauywUy3WF1TX" (9, 19) automation: getting actions for "3YxzQMA5PR4zXqJVL" (8, 33) automation: getting actions for "To" (57, 47) seproxyhal: applying automation ['button', 2, True] seproxyhal: applying automation ['button', 2, False] From there every press landed one screen late; the approval hit "Reject transaction" and the app returned 0x6985, so signtx reported a canceled error. "T" and "o" are both valid base58 characters, and bitcoind generates fresh addresses on every run, which makes this a rare and random failure. Bech32 addresses cannot trigger it ("o" is not in the bech32 character set). Limit the "To" rule to the title row (y=3), where address text never appears. The rule file format does not allow comments, so a warning about short words in automation rules goes in the README. Co-authored-by: Claude (Fable 5) --- test/README.md | 12 ++++++++++++ test/data/speculos-automation.json | 10 +++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/test/README.md b/test/README.md index 1e1264ec7..099f9d027 100644 --- a/test/README.md +++ b/test/README.md @@ -311,6 +311,18 @@ $ cmake -Bbuild -H. $ make -C build/ ``` +### Automation rules + +The Ledger tests drive the emulator's buttons with +`data/speculos-automation.json`. Speculos matches each rule against every +text fragment drawn on the screen, so beware of this pitfall when editing +it: screens can contain randomly generated base58 addresses, wrapped over +several lines, and a rule matching a short word can accidentally match a +wrapped address line. The resulting extra button press derails the rest of +the flow. This happened with a rule for the "To" screen title when an +address ended in "To". Keep rules multi-word where possible, or pin them to +the title row with `"y": 3` like the "To" rule. + ## Coldcard emulator Clone the repository: diff --git a/test/data/speculos-automation.json b/test/data/speculos-automation.json index 9d821fd2f..65042f589 100644 --- a/test/data/speculos-automation.json +++ b/test/data/speculos-automation.json @@ -45,7 +45,15 @@ ] }, { - "regexp": "^(Address|Review|Amount|Fee|Confirm|The derivation|Derivation path|The change path|Change path|Register wallet|Policy map|Key|Path|Public key|Our key|Their key|Unspendable key|Spend from|Transaction output|Wallet name|Wallet policy|Descriptor template|Verify [Bb]itcoin|To|Output|Warning).*", + "regexp": "^(Address|Review|Amount|Fee|Confirm|The derivation|Derivation path|The change path|Change path|Register wallet|Policy map|Key|Path|Public key|Our key|Their key|Unspendable key|Spend from|Transaction output|Wallet name|Wallet policy|Descriptor template|Verify [Bb]itcoin|Output|Warning).*", + "actions": [ + [ "button", 2, true ], + [ "button", 2, false ] + ] + }, + { + "regexp": "^To.*", + "y": 3, "actions": [ [ "button", 2, true ], [ "button", 2, false ] From 60a90bb6f10a8c39be7e78856e81e9dfd7fccd37 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 5 Sep 2025 12:37:35 +0200 Subject: [PATCH 13/15] 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 b2029d6ed..2520f972a 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 3ab105770befa38daea6013e67c684776085db2b Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 3 Feb 2026 11:16:13 +0100 Subject: [PATCH 14/15] 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 887843cbaf497f6eeec59ebb3acef99053747cd9 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 3 Feb 2026 15:11:48 +0100 Subject: [PATCH 15/15] 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 65042f589..e46c4240b 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 ] - ] } ] }