From 7e73fada74b7a39111bec6437dff1eee301e52bc Mon Sep 17 00:00:00 2001 From: Andreas Griffin <116060138+andreasgriffin@users.noreply.github.com> Date: Sat, 9 Nov 2024 18:45:37 +0100 Subject: [PATCH] Better windows build (via docker) (#23) - Added svg export to Flow diagrams - Can open tx, wallet, psbt files now directly via argument - newer version of qr-tools to improve fps - Speed improvements in label syncing (label.duimp, needed for label comparision is now faster) - added labuage icons in menus --- .github/workflows/python-tests.yml | 3 +- .gitignore | 5 +- .vscode/launch.json | 41 +- README.md | 5 +- bitcoin_safe/__init__.py | 2 +- bitcoin_safe/__main__.py | 16 +- bitcoin_safe/dynamic_lib_load.py | 69 ++- bitcoin_safe/gui/icons/flows.png | Bin 5080 -> 0 bytes bitcoin_safe/gui/icons/flows.svg | 67 +++ bitcoin_safe/gui/qt/about_dialog.py | 2 +- bitcoin_safe/gui/qt/address_list.py | 8 +- bitcoin_safe/gui/qt/block_buttons.py | 2 +- bitcoin_safe/gui/qt/data_tab_widget.py | 1 + bitcoin_safe/gui/qt/fee_group.py | 2 +- bitcoin_safe/gui/qt/hist_list.py | 12 +- bitcoin_safe/gui/qt/language_chooser.py | 3 +- bitcoin_safe/gui/qt/main.py | 14 +- bitcoin_safe/gui/qt/my_treeview.py | 6 +- .../gui/qt/new_wallet_welcome_screen.py | 2 +- bitcoin_safe/gui/qt/plot.py | 2 +- bitcoin_safe/gui/qt/sankey_bitcoin.py | 2 +- bitcoin_safe/gui/qt/sankey_widget.py | 127 +++++- bitcoin_safe/gui/qt/search_tree_view.py | 2 +- bitcoin_safe/gui/qt/ui_tx.py | 7 +- .../gui/qt/update_notification_bar.py | 2 +- bitcoin_safe/gui/qt/utxo_list.py | 3 +- bitcoin_safe/gui/qt/wallet_steps.py | 2 +- bitcoin_safe/{html.py => html_utils.py} | 0 bitcoin_safe/network_config.py | 2 +- bitcoin_safe/signals.py | 1 + bitcoin_safe/storage.py | 3 +- bitcoin_safe/util.py | 46 +- bitcoin_safe/wallet.py | 9 +- poetry.lock | 425 ++++++++++-------- pyproject.toml | 17 +- .../{make_appimage.sh => run_in_docker.sh} | 41 +- tools/build-wine/.dockerignore | 5 + tools/build-wine/Dockerfile | 74 +++ tools/build-wine/apt.preferences | 3 + tools/build-wine/apt.sources.list | 2 + tools/build-wine/bitcoin_safe.nsi | 173 +++++++ tools/build-wine/build_exe.sh | 144 ++++++ tools/build-wine/deterministic.spec | 170 +++++++ ...D10B6531D7C8E1BC296021FC624643487034E5.asc | 108 +++++ tools/build-wine/prepare-wine.sh | 103 +++++ tools/build-wine/run_in_docker.sh | 109 +++++ tools/build.py | 153 +++++-- tools/build_tools_util.sh | 50 +++ .../requirements-build-base.txt | 27 ++ .../requirements-build-wine.txt | 20 + tools/make_libsecp256k1.sh | 76 ++++ tools/make_libusb.sh | 63 +++ tools/make_zbar.sh | 94 ++++ tools/translation_handler.py | 14 +- 54 files changed, 1953 insertions(+), 386 deletions(-) delete mode 100644 bitcoin_safe/gui/icons/flows.png create mode 100644 bitcoin_safe/gui/icons/flows.svg rename bitcoin_safe/{html.py => html_utils.py} (100%) rename tools/build-linux/appimage/{make_appimage.sh => run_in_docker.sh} (83%) create mode 100644 tools/build-wine/.dockerignore create mode 100644 tools/build-wine/Dockerfile create mode 100644 tools/build-wine/apt.preferences create mode 100644 tools/build-wine/apt.sources.list create mode 100644 tools/build-wine/bitcoin_safe.nsi create mode 100755 tools/build-wine/build_exe.sh create mode 100644 tools/build-wine/deterministic.spec create mode 100644 tools/build-wine/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc create mode 100755 tools/build-wine/prepare-wine.sh create mode 100755 tools/build-wine/run_in_docker.sh create mode 100644 tools/deterministic-build/requirements-build-base.txt create mode 100644 tools/deterministic-build/requirements-build-wine.txt create mode 100755 tools/make_libsecp256k1.sh create mode 100755 tools/make_libusb.sh create mode 100755 tools/make_zbar.sh diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index f8ce1eb..639993b 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -49,7 +49,8 @@ jobs: libxrender-dev \ libxi-dev \ libxkbcommon-dev \ - libxkbcommon-x11-dev + libxkbcommon-x11-dev \ + libsecp256k1-0 sudo /usr/bin/Xvfb $DISPLAY -screen 0 1280x1024x24 & - name: Start Xvfb diff --git a/.gitignore b/.gitignore index 5814ca7..d92f5ec 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,6 @@ MANIFEST # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest -*.spec # Installer logs pip-log.txt @@ -198,6 +197,7 @@ bitcoin_safe.dist-info # translation files *.po *.csv +*.xcf screenshots*/ .DS_Store @@ -219,4 +219,5 @@ bitcoin_safe *.so.0 *.so.1 version -tools/libusb/ \ No newline at end of file +tools/libusb/ +*.dll \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 4b37a0a..d0759f6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -111,25 +111,50 @@ "request": "launch", "program": "tools/build.py", "args": [ - "--targets", - // // "deb", - // "appimage", - // "--sign", + "--targets", "appimage", "windows", ], "console": "integratedTerminal", "preLaunchTask": "Poetry Install" // label of the task },{ - "name": "Sign", + "name": "Build Linux (Current Files)", + "type": "python", + "request": "launch", + "program": "tools/build.py", + "args": [ + "--targets", "appimage", + "--commit", "None", +], + "console": "integratedTerminal", + "preLaunchTask": "Poetry Install" // label of the task +},{ + "name": "Build Windows (Current Files)", "type": "python", "request": "launch", "program": "tools/build.py", "args": [ - // "--targets", - // // "deb", - // "appimage", + "--targets", "windows", + "--commit", "None", +], + "console": "integratedTerminal", + "preLaunchTask": "Poetry Install" // label of the task +},{ + "name": "Sign", + "type": "python", + "request": "launch", + "program": "tools/build.py", + "args": [ "--sign", ], "console": "integratedTerminal" +},{ + "name": "Lock no cache", + "type": "python", + "request": "launch", + "program": "tools/build.py", + "args": [ + "--lock", +], + "console": "integratedTerminal" },{ "name": "Current File", "type": "python", diff --git a/README.md b/README.md index f2aec06..ce5d528 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - **Easy** Multisig-Wallet Setup - Step-by-Step instructions for a secure MultiSig setup with PDF backup sheets - Test transactions ensure that all hardware signers are ready - - Full support for [Coldcard](https://coldcard.com/), [Coldcard Q](https://coldcard.com/), [Bitbox02](https://shiftcrypto.ch/bitbox02/?ref=MOB4dk7gpm), [Blockstream Jade](https://store.blockstream.com/?code=XEocg5boS77D), and Specter DIY, supporting QR, USB, SD-card + - Full support for [Coldcard](https://store.coinkite.com/promo/8BFF877000C34A86F410), [Coldcard Q](https://store.coinkite.com/promo/8BFF877000C34A86F410), [Bitbox02](https://shiftcrypto.ch/bitbox02/?ref=MOB4dk7gpm), [Blockstream Jade](https://store.blockstream.com/?code=XEocg5boS77D), and Specter DIY, supporting QR, USB, SD-card - **Secure**: Hardware signers only - All wallets require hardware signers/wallets for safe seed storage - Powered by **[BDK](https://github.com/bitcoindevkit/bdk)** @@ -64,6 +64,7 @@ - CSV import for batch transactions - Label import and export using [BIP329](https://bip329.org/) - Label import from Electrum wallet + - Export of the money flow diagram to svg - Drag and drop for Transactions, PSBTs, and CSV files - **Wallet Features** @@ -88,7 +89,7 @@ - **Languages** - - 🇺🇸 English, 🇨🇳 Chinese - 简体中文, 🇪🇸 Spanish - español de España, 🇯🇵 Japanese - 日本語, 🇷🇺 Russian - русский, 🇵🇹 Portuguese - português europeu, 🇮🇳 Hindi - हिन्दी, Arabic - العربية, 🇮🇹 Italian - italiano, (more upon request) + - 🇺🇸 English, 🇨🇳 Chinese - 简体中文, 🇪🇸 Spanish - español de España, 🇯🇵 Japanese - 日本語, 🇷🇺 Russian - русский, 🇵🇹 Portuguese - português europeu, 🇮🇳 Hindi - हिन्दी, Arabic - العربية, 🇮🇹 Italian - italiano, 🇫🇷 French - Français, 🇩🇪 German - Deutsch, (more upon request) - **Transaction / PSBT Creation** diff --git a/bitcoin_safe/__init__.py b/bitcoin_safe/__init__.py index a52c7bf..af0dc56 100644 --- a/bitcoin_safe/__init__.py +++ b/bitcoin_safe/__init__.py @@ -1,2 +1,2 @@ # this is the source of the version information -__version__ = "1.0.0b2" +__version__ = "1.0.0b3" diff --git a/bitcoin_safe/__main__.py b/bitcoin_safe/__main__.py index 77a8f9a..87c41fd 100644 --- a/bitcoin_safe/__main__.py +++ b/bitcoin_safe/__main__.py @@ -1,9 +1,10 @@ import sys -from .dynamic_lib_load import ensure_pyzbar_works +# all import must be absolute, because this is the entry script for pyinstaller +from bitcoin_safe.dynamic_lib_load import ensure_pyzbar_works # this setsup the logging -from .logging_setup import setup_logging # type: ignore +from bitcoin_safe.logging_setup import setup_logging # type: ignore ensure_pyzbar_works() @@ -12,8 +13,8 @@ from PyQt6.QtWidgets import QApplication -from .gui.qt.main import MainWindow -from .gui.qt.util import custom_exception_handler +from bitcoin_safe.gui.qt.main import MainWindow +from bitcoin_safe.gui.qt.util import custom_exception_handler def parse_args() -> argparse.Namespace: @@ -23,6 +24,13 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--profile", action="store_true", help="Enable profiling. VIsualize with snakeviz .prof_stats" ) + parser.add_argument( + "open_files_at_startup", + metavar="FILE", + type=str, + nargs="*", + help="File to process, can be of type tx, psbt, wallet files.", + ) return parser.parse_args() diff --git a/bitcoin_safe/dynamic_lib_load.py b/bitcoin_safe/dynamic_lib_load.py index 8eee8b5..6a58a37 100644 --- a/bitcoin_safe/dynamic_lib_load.py +++ b/bitcoin_safe/dynamic_lib_load.py @@ -28,7 +28,6 @@ import logging -import os import platform import sys from ctypes.util import find_library @@ -39,7 +38,7 @@ import bitcoin_usb import bitcointx -from .html import link +from .html_utils import link from .i18n import translate logger = logging.getLogger(__name__) @@ -57,20 +56,6 @@ def show_message_before_quit(msg: str) -> None: sys.exit(app.exec()) -def get_libsecp256k1_electrumsv_path() -> str: - from electrumsv_secp256k1 import _libsecp256k1 - - # Get the platform-specific path to the binary library - if platform.system() == "Windows": - # On Windows, construct the path to the DLL - here = os.path.dirname(os.path.abspath(_libsecp256k1.__file__)) - lib_path = os.path.join(here, "libsecp256k1.dll") - else: - # On Linux and macOS, directly use the __file__ attribute - lib_path = _libsecp256k1.__file__ - return lib_path - - def get_libsecp256k1_os_path() -> str | None: "This cannot be used directly, because it doesnt return an absolute path" lib_name = "secp256k1" @@ -78,30 +63,37 @@ def get_libsecp256k1_os_path() -> str | None: def get_packaged_libsecp256k1_path() -> str | None: - # for apppimage it is - # __file__ = squashfs-root/usr/lib/python3.10/site-packages/bitcoin_safe/dynamic_lib_load.py - # and the lib is in - # squashfs-root/usr/lib/libsecp256k1.so.0.0.0 - packaged_lib_path = Path(__file__).parent.parent.parent.parent - for name in ["libsecp256k1.so.0.0.0", "libsecp256k1.so.0"]: - lib_path = packaged_lib_path / name - if lib_path.exists(): - return str(lib_path) + if platform.system() == "Linux": + # for apppimage it is + # __file__ = squashfs-root/usr/lib/python3.10/site-packages/bitcoin_safe/dynamic_lib_load.py + # and the lib is in + # squashfs-root/usr/lib/libsecp256k1.so.0.0.0 + + for name in ["libsecp256k1.so.0.0.0", "libsecp256k1.so.0"]: + lib_path = Path(__file__).parent.parent.parent.parent / name + logger.info(f"Searching for {name} in {lib_path.absolute()}") + if lib_path.exists(): + return str(lib_path) + + elif platform.system() == "Windows": + # for exe the dlls are packages in the same folder as dynamic_lib_load.py + # packaged in setup: __file__ = C:/Program Files/Bitcoin Safe/_internals/bitcoin_safe/dynamic_lib_load.pyc + # the dll is in: C:/Program Files/Bitcoin Safe/_internals/libsecp256k1-2.dll + for name in ["libsecp256k1-2.dll"]: + # logger.info(f"file in {Path(__file__).absolute()}") + lib_path = Path(__file__).parent.parent / name + logger.info(f"Searching for {name} in {lib_path.absolute()}") + if lib_path.exists(): + return str(lib_path) + return None def setup_libsecp256k1() -> None: - """The operating system might, or might not provide libsecp256k1 needed for bitcointx - - Therefore we require https://pypi.org/project/electrumsv-secp256k1/ in the build process as additional_requires - and point the bicointx library here to this binary. + """ + The packaged versions com with libsecp256k1 - This isn't ideal, but: - # electrumsv-secp256k1 offers libsecp256k1 prebuild for different platforms - # which is needed for bitcointx. - # bitcointx and with it the prebuild libsecp256k1 is not used for anything security critical - # key derivation with bitcointx is restricted to testnet/regtest/signet - # and the PSBTTools using bitcointx is safe because it handles no key material + Only if you install it via pip/git, libsecp256k1 is required to be on the system """ lib_path = None @@ -112,13 +104,6 @@ def setup_libsecp256k1() -> None: logger.info(f"libsecp256k1 found in package.: {packaged_libsecp256k1_path}") lib_path = packaged_libsecp256k1_path - # Fallback choice is the electrumsv version - if not lib_path: - binary_lib_path_from_electrumsv = get_libsecp256k1_electrumsv_path() - if binary_lib_path_from_electrumsv: - logger.info(f"libsecp256k1 found via fallbackmethod: {binary_lib_path_from_electrumsv}") - lib_path = binary_lib_path_from_electrumsv - if lib_path: logger.info(f"Setting libsecp256k1: {lib_path}") bitcoin_usb.set_custom_secp256k1_path(lib_path) diff --git a/bitcoin_safe/gui/icons/flows.png b/bitcoin_safe/gui/icons/flows.png deleted file mode 100644 index cebd7dc9574eb8d535448875214c9ac59ad14da3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5080 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 + +SVG Export by Bitcoin Safe +SVG Export by Bitcoin Safe + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bitcoin_safe/gui/qt/about_dialog.py b/bitcoin_safe/gui/qt/about_dialog.py index ff8b5db..e78fc2d 100644 --- a/bitcoin_safe/gui/qt/about_dialog.py +++ b/bitcoin_safe/gui/qt/about_dialog.py @@ -40,7 +40,7 @@ QWidget, ) -from bitcoin_safe.html import link +from bitcoin_safe.html_utils import link class LicenseDialog(QDialog): diff --git a/bitcoin_safe/gui/qt/address_list.py b/bitcoin_safe/gui/qt/address_list.py index 46821fd..4179091 100644 --- a/bitcoin_safe/gui/qt/address_list.py +++ b/bitcoin_safe/gui/qt/address_list.py @@ -89,7 +89,7 @@ from ...i18n import translate from ...rpc import send_rpc_command from ...signals import Signals, UpdateFilter, UpdateFilterReason, WalletSignals -from ...util import Satoshis, block_explorer_URL +from ...util import Satoshis, block_explorer_URL, time_logger from ...wallet import TxStatus, Wallet from .category_list import CategoryEditor from .my_treeview import ( @@ -327,6 +327,11 @@ def dropEvent(self, event: QDropEvent) -> None: # type: ignore[override] event.accept() return + elif mime_data.hasUrls(): + # Iterate through the list of dropped file URLs + for url in mime_data.urls(): + # Convert URL to local file path + self.signals.open_file_path.emit(url.toLocalFile()) event.ignore() def on_double_click(self, idx: QModelIndex) -> None: @@ -381,6 +386,7 @@ def on_update_fx_rates(self): update_filter = UpdateFilter(addresses=addresses_with_balance, reason=UpdateFilterReason.NewFxRates) self.update_with_filter(update_filter) + @time_logger def update_with_filter(self, update_filter: UpdateFilter) -> None: if update_filter.refresh_all: return self.update_content() diff --git a/bitcoin_safe/gui/qt/block_buttons.py b/bitcoin_safe/gui/qt/block_buttons.py index d8dad92..e3d4265 100644 --- a/bitcoin_safe/gui/qt/block_buttons.py +++ b/bitcoin_safe/gui/qt/block_buttons.py @@ -41,7 +41,7 @@ from bitcoin_safe.config import UserConfig from bitcoin_safe.util import block_explorer_URL_of_projected_block, unit_fee_str -from ...html import html_f +from ...html_utils import html_f from ...mempool import MempoolData, fee_to_color, mempoolFeeColors from .invisible_scroll_area import InvisibleScrollArea from .util import center_in_widget, open_website diff --git a/bitcoin_safe/gui/qt/data_tab_widget.py b/bitcoin_safe/gui/qt/data_tab_widget.py index 1df83f1..8ab62e5 100644 --- a/bitcoin_safe/gui/qt/data_tab_widget.py +++ b/bitcoin_safe/gui/qt/data_tab_widget.py @@ -40,6 +40,7 @@ from PyQt6.QtWidgets import QApplication, QWidget T = TypeVar("T") +T2 = TypeVar("T2") class DataTabWidget(Generic[T], HistTabWidget): diff --git a/bitcoin_safe/gui/qt/fee_group.py b/bitcoin_safe/gui/qt/fee_group.py index b79a49d..658e570 100644 --- a/bitcoin_safe/gui/qt/fee_group.py +++ b/bitcoin_safe/gui/qt/fee_group.py @@ -32,7 +32,7 @@ from bitcoin_safe.fx import FX from bitcoin_safe.gui.qt.notification_bar import NotificationBar from bitcoin_safe.gui.qt.util import icon_path -from bitcoin_safe.html import html_f, link +from bitcoin_safe.html_utils import html_f, link from bitcoin_safe.psbt_util import FeeInfo from ...config import FEE_RATIO_HIGH_WARNING, NO_FEE_WARNING_BELOW, UserConfig diff --git a/bitcoin_safe/gui/qt/hist_list.py b/bitcoin_safe/gui/qt/hist_list.py index d116eb7..b11551d 100644 --- a/bitcoin_safe/gui/qt/hist_list.py +++ b/bitcoin_safe/gui/qt/hist_list.py @@ -87,7 +87,12 @@ from ...i18n import translate from ...signals import Signals, UpdateFilter, UpdateFilterReason -from ...util import Satoshis, block_explorer_URL, confirmation_wait_formatted +from ...util import ( + Satoshis, + block_explorer_URL, + confirmation_wait_formatted, + time_logger, +) from ...wallet import ( ToolsTxUiInfo, TxConfirmationStatus, @@ -97,7 +102,6 @@ get_wallets, ) from .category_list import CategoryEditor -from .dialog_import import file_to_str from .my_treeview import ( MyItemDataRole, MySortModel, @@ -326,8 +330,7 @@ def dropEvent(self, event: QDropEvent | None) -> None: # Iterate through the list of dropped file URLs for url in mime_data.urls(): # Convert URL to local file path - file_path = url.toLocalFile() - self.signals.open_tx_like.emit(file_to_str(file_path)) + self.signals.open_file_path.emit(url.toLocalFile()) event.ignore() @@ -348,6 +351,7 @@ def toggle_used(self, state: int) -> None: self.show_used = AddressUsageStateFilter(state) self.update_content() + @time_logger def update_with_filter(self, update_filter: UpdateFilter) -> None: if update_filter.refresh_all: return self.update_content() diff --git a/bitcoin_safe/gui/qt/language_chooser.py b/bitcoin_safe/gui/qt/language_chooser.py index 652880f..5c543ec 100644 --- a/bitcoin_safe/gui/qt/language_chooser.py +++ b/bitcoin_safe/gui/qt/language_chooser.py @@ -148,7 +148,8 @@ def centerOnScreen(self) -> None: def setupComboBox(self, languages: Dict[str, str]) -> None: for lang, name in languages.items(): - self.comboBox.addItem(name, lang) + icon = LanguageChooser.create_flag_icon(FLAGS[lang]) if lang in FLAGS else QIcon() + self.comboBox.addItem(icon, name, lang) def choose_language(self) -> Optional[str]: if self.exec() == QDialog.DialogCode.Accepted: diff --git a/bitcoin_safe/gui/qt/main.py b/bitcoin_safe/gui/qt/main.py index 8c5ce50..8a28847 100644 --- a/bitcoin_safe/gui/qt/main.py +++ b/bitcoin_safe/gui/qt/main.py @@ -87,7 +87,7 @@ from ...tx import TxBuilderInfos, TxUiInfos, short_tx_id from ...wallet import ProtoWallet, ToolsTxUiInfo, Wallet from . import address_dialog -from .dialog_import import ImportDialog +from .dialog_import import ImportDialog, file_to_str from .dialogs import PasswordQuestion, WalletIdDialog, question_dialog from .extended_tabwidget import ExtendedTabWidget, LoadingWalletTab from .network_settings.main import NetworkSettingsUI @@ -110,10 +110,12 @@ def __init__( self, network: Literal["bitcoin", "regtest", "signet", "testnet"] | None = None, config: UserConfig | None = None, + open_files_at_startup: List[str] | None = None, **kwargs, ) -> None: "If netowrk == None, then the network from the user config will be taken" super().__init__() + self.open_files_at_startup = open_files_at_startup if open_files_at_startup else [] config_present = UserConfig.exists() or config self.config = config if config else UserConfig.from_file() self.config.network = bdk.Network[network.upper()] if network else self.config.network @@ -143,6 +145,7 @@ def __init__( self.last_qtwallet: Optional[QTWallet] = None # connect the listeners + self.signals.open_file_path.connect(self.open_file_path) self.signals.open_tx_like.connect(self.open_tx_like_in_tab) self.signals.get_network.connect(self.get_network) self.signals.get_mempool_url.connect(self.get_mempool_url) @@ -206,6 +209,15 @@ def load_last_state(self) -> None: self.welcome_screen.add_new_wallet_welcome_tab() self.open_last_opened_tx() + for file_path in self.open_files_at_startup: + self.open_file_path(file_path=file_path) + + def open_file_path(self, file_path: str): + if file_path and Path(file_path).exists(): + if file_path.endswith(".wallet"): + self.open_wallet(file_path=file_path) + else: + self.signals.open_tx_like.emit(file_to_str(file_path)) def set_title(self) -> None: title = "Bitcoin Safe" diff --git a/bitcoin_safe/gui/qt/my_treeview.py b/bitcoin_safe/gui/qt/my_treeview.py index 2702635..b509ea7 100644 --- a/bitcoin_safe/gui/qt/my_treeview.py +++ b/bitcoin_safe/gui/qt/my_treeview.py @@ -1034,8 +1034,12 @@ def _before_update_content(self): self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change def _after_update_content(self): - self.sortByColumn(self._current_column, self._current_order) + # the following 2 lines (in this order) + # call the sorting only once, in the default case + # since sorting is slow (~1s, for 3k entries), DO NOT CHANGE the order here, + # or you double the sorting time self.proxy.setDynamicSortFilter(True) + self.sortByColumn(self._current_column, self._current_order) # show/hide self.Columns self.filter() diff --git a/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py b/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py index ecf18dd..8bdfb71 100644 --- a/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py +++ b/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py @@ -30,7 +30,7 @@ import logging from bitcoin_safe.gui.qt.data_tab_widget import DataTabWidget -from bitcoin_safe.html import html_f +from bitcoin_safe.html_utils import html_f from bitcoin_safe.signals import Signals logger = logging.getLogger(__name__) diff --git a/bitcoin_safe/gui/qt/plot.py b/bitcoin_safe/gui/qt/plot.py index 05017a5..df932df 100644 --- a/bitcoin_safe/gui/qt/plot.py +++ b/bitcoin_safe/gui/qt/plot.py @@ -172,7 +172,7 @@ def update_chart(self, balance_data, project_until_now=True) -> None: max_balance, ) - buffer_time = 60 * 60 + buffer_time = (max_timestamp - min_timestamp) * 0.02 self.datetime_axis.setMin(QDateTime.fromSecsSinceEpoch(int(min_timestamp - buffer_time))) self.datetime_axis.setMax(QDateTime.fromSecsSinceEpoch(int(max_timestamp + buffer_time))) self.value_axis.setMin(min_balance) diff --git a/bitcoin_safe/gui/qt/sankey_bitcoin.py b/bitcoin_safe/gui/qt/sankey_bitcoin.py index 0f1edbb..b486654 100644 --- a/bitcoin_safe/gui/qt/sankey_bitcoin.py +++ b/bitcoin_safe/gui/qt/sankey_bitcoin.py @@ -35,7 +35,7 @@ from bitcoin_safe.gui.qt.address_edit import AddressEdit from bitcoin_safe.gui.qt.sankey_widget import FlowIndex, FlowType, SankeyWidget -from bitcoin_safe.html import html_f +from bitcoin_safe.html_utils import html_f from bitcoin_safe.psbt_util import FeeInfo from bitcoin_safe.pythonbdk_types import ( OutPoint, diff --git a/bitcoin_safe/gui/qt/sankey_widget.py b/bitcoin_safe/gui/qt/sankey_widget.py index 965686c..ce72f4e 100644 --- a/bitcoin_safe/gui/qt/sankey_widget.py +++ b/bitcoin_safe/gui/qt/sankey_widget.py @@ -31,10 +31,12 @@ import math import sys from dataclasses import dataclass +from pathlib import Path from typing import Dict, Iterable, List, Tuple -from PyQt6.QtCore import QPointF, QRectF, pyqtSignal +from PyQt6.QtCore import QPointF, QRect, QRectF, QSize, Qt, pyqtSignal from PyQt6.QtGui import ( + QAction, QColor, QLinearGradient, QMouseEvent, @@ -42,7 +44,15 @@ QPainterPath, QPen, ) -from PyQt6.QtWidgets import QApplication, QTabWidget, QToolTip, QWidget +from PyQt6.QtSvg import QSvgGenerator +from PyQt6.QtWidgets import ( + QApplication, + QFileDialog, + QMenu, + QTabWidget, + QToolTip, + QWidget, +) logger = logging.getLogger(__name__) @@ -82,6 +92,8 @@ def __init__(self, show_tooltips=True, parent: QWidget | None = None) -> None: # self.signal_on_label_click.connect(lambda flow_index: print(flow_index)) + self.gradient_dict: Dict[str, Tuple[QColor, QColor]] = {} + @property def image_width(self) -> float: return self.width() @@ -192,6 +204,7 @@ def _paint_one_side( end_y_positions: List[float], flow_type: FlowType, reverse=False, + workaround_for_svg=False, ): image_left = ( self.x_offset if not reverse else self.image_width + self.x_offset @@ -217,6 +230,7 @@ def _paint_one_side( direction, self.colors.get(flow_index, self.border_color), self.center_color, + workaround_for_svg=workaround_for_svg, ) # Draw text at the start point @@ -240,6 +254,7 @@ def draw_path( direction: int, start_color: QColor, end_color: QColor, + workaround_for_svg=False, ): path = QPainterPath() path.moveTo(start_x, start_y) @@ -252,11 +267,18 @@ def draw_path( math.ceil(end_y), ) - gradient = QLinearGradient(QPointF(start_x, start_y), QPointF(end_x, end_y)) - gradient.setColorAt(0, start_color) - gradient.setColorAt(1, end_color) pen = QPen() - pen.setBrush(gradient) + if workaround_for_svg: + color_name = QColor(len(self.gradient_dict)).name() + self.gradient_dict[color_name] = ( + (start_color, end_color) if direction == 1 else (end_color, start_color) + ) + pen.setBrush(QColor(color_name)) + else: + gradient = QLinearGradient(QPointF(start_x, start_y), QPointF(end_x, end_y)) + gradient.setColorAt(0, start_color) + gradient.setColorAt(1, end_color) + pen.setBrush(gradient) pen.setWidth(math.ceil(width)) painter.setPen(pen) painter.drawPath(path) @@ -319,9 +341,8 @@ def draw_text_with_outline(self, painter: QPainter, text: str, position: QPointF painter.setPen(text_color) painter.drawText(position, text) - def paintEvent(self, event): - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) + def draw_content(self, painter: QPainter, workaround_for_svg=False): + self.gradient_dict.clear() self.text_rects.clear() self._paint_one_side( @@ -330,6 +351,7 @@ def paintEvent(self, event): y_start_positions=self.in_flow_y_positions, end_y_positions=self.end_in_y_positions, flow_type=FlowType.InFlow, + workaround_for_svg=workaround_for_svg, ) self._paint_one_side( painter, @@ -338,9 +360,14 @@ def paintEvent(self, event): end_y_positions=self.end_out_y_positions, reverse=True, flow_type=FlowType.OutFlow, + workaround_for_svg=workaround_for_svg, ) painter.end() + def paintEvent(self, event): + painter = QPainter(self) + self.draw_content(painter) + def mouseMoveEvent(self, event: QMouseEvent | None) -> None: if not event: return @@ -359,10 +386,63 @@ def mouseMoveEvent(self, event: QMouseEvent | None) -> None: def mousePressEvent(self, event: QMouseEvent | None) -> None: if not event: return - for rect, flow_index in self.text_rects: - if rect.contains(event.position()): - self.signal_on_label_click.emit(flow_index) - break + + if event.button() == Qt.MouseButton.RightButton: + # Right-click detected, show context menu + menu = QMenu(self) + export_action = QAction("Export to svg", self) + menu.addAction(export_action) + # Connect the action to the export method + export_action.triggered.connect(self.export_to_svg) + # Show the menu at the cursor position + menu.exec(event.globalPosition().toPoint()) + else: + # Handle other mouse events (e.g., left-click) + for rect, flow_index in self.text_rects: + if rect.contains(event.position()): + self.signal_on_label_click.emit(flow_index) + break + + def export_to_svg(self) -> None: + file_path, _ = QFileDialog.getSaveFileName( + self, self.tr("Export svg"), "", self.tr("All Files (*);;Text Files (*.svg)") + ) + if not file_path: + logger.info("No file selected") + return + self._export_to_svg(Path(file_path)) + + def _export_to_svg(self, filename: Path) -> None: + self.workaround_for_svg = True + width, height = self.width(), self.height() + generator = QSvgGenerator() + generator.setFileName(str(filename)) + generator.setSize(QSize(width, height)) + generator.setViewBox(QRect(0, 0, width, height)) + + generator.setTitle("SVG Export by Bitcoin Safe") + generator.setDescription("SVG Export by Bitcoin Safe") + + self.draw_content(QPainter(generator), workaround_for_svg=True) + + with open(str(filename), "r") as file: + contents = file.read() + with open(str(filename), "w") as file: + defs = "" + for color_name, (start_color, end_color) in self.gradient_dict.items(): + gradient_name = f"linear{color_name.lstrip('#')}" + defs += f""" + + + + + """ + + contents = contents.replace(f'stroke="{color_name}"', f'stroke="url(#{gradient_name})"') + contents = contents.replace("\n", f"\n{defs}\n") + + file.write(contents) + self.workaround_for_svg = False if __name__ == "__main__": @@ -389,19 +469,26 @@ def mousePressEvent(self, event: QMouseEvent | None) -> None: 30.0, 5.0, ] - labels = { - FlowIndex(FlowType.InFlow, 0): "1\n1", - FlowIndex(FlowType.InFlow, 1): "2", - FlowIndex(FlowType.OutFlow, 0): "4", - FlowIndex(FlowType.OutFlow, 1): "4", - FlowIndex(FlowType.OutFlow, 2): "5", + labels: Dict[FlowIndex, str] = { + # FlowIndex(FlowType.InFlow, 0): "1\n1", + # FlowIndex(FlowType.InFlow, 1): "2", + # FlowIndex(FlowType.OutFlow, 0): "4", + # FlowIndex(FlowType.OutFlow, 1): "4", + # FlowIndex(FlowType.OutFlow, 2): "5", } app = QApplication(sys.argv) tabs = QTabWidget() sankey = SankeyWidget() - sankey.set(in_flows=in_flows, out_flows=out_flows, colors=colors, text_outline=True, labels=labels) + sankey.set( + in_flows=in_flows, + out_flows=out_flows, + colors=colors, + text_outline=True, + labels=labels, + space_fraction=0.3, + ) tabs.addTab(sankey, "sankey") tabs.show() sys.exit(app.exec()) diff --git a/bitcoin_safe/gui/qt/search_tree_view.py b/bitcoin_safe/gui/qt/search_tree_view.py index 18e4f5b..3c740a7 100644 --- a/bitcoin_safe/gui/qt/search_tree_view.py +++ b/bitcoin_safe/gui/qt/search_tree_view.py @@ -29,7 +29,7 @@ import logging -from bitcoin_safe.html import html_f +from bitcoin_safe.html_utils import html_f from bitcoin_safe.i18n import translate from bitcoin_safe.signals import SignalsMin diff --git a/bitcoin_safe/gui/qt/ui_tx.py b/bitcoin_safe/gui/qt/ui_tx.py index 8b27d3d..023e825 100644 --- a/bitcoin_safe/gui/qt/ui_tx.py +++ b/bitcoin_safe/gui/qt/ui_tx.py @@ -43,7 +43,7 @@ from bitcoin_safe.gui.qt.sankey_bitcoin import SankeyBitcoin from bitcoin_safe.gui.qt.spinning_button import SpinningButton from bitcoin_safe.gui.qt.tx_signing_steps import TxSigningSteps -from bitcoin_safe.html import html_f +from bitcoin_safe.html_utils import html_f from bitcoin_safe.keystore import KeyStore from bitcoin_safe.threading_manager import TaskThread, ThreadingManager @@ -101,6 +101,7 @@ clean_list, format_fee_rate, serialized_to_hex, + time_logger, ) from ...wallet import ( ToolsTxUiInfo, @@ -1014,7 +1015,7 @@ def on_done(success) -> None: def on_success(success) -> None: if success: self.tabs_inputs_outputs.addTab( - self.sankey_bitcoin, icon=read_QIcon("flows.png"), description=self.tr("Diagram") + self.sankey_bitcoin, icon=read_QIcon("flows.svg"), description=self.tr("Diagram") ) def on_error(packed_error_info) -> None: @@ -1230,6 +1231,7 @@ def __init__( self.signals.language_switch.connect(self.updateUi) self.signals.wallet_signals[self.wallet.id].updated.connect(self.update_with_filter) + @time_logger def update_with_filter(self, update_filter: UpdateFilter) -> None: should_update = False if should_update or update_filter.refresh_all: @@ -1351,6 +1353,7 @@ def clear_ui(self) -> None: self.utxo_list.update_content() self.tabs_inputs.setCurrentIndex(0) self.category_list.select_category(self.wallet.labels.get_default_category()) + self.update_amounts_and_categories() def create_tx(self) -> None: if ( diff --git a/bitcoin_safe/gui/qt/update_notification_bar.py b/bitcoin_safe/gui/qt/update_notification_bar.py index 2bc6e07..6ed3ee5 100644 --- a/bitcoin_safe/gui/qt/update_notification_bar.py +++ b/bitcoin_safe/gui/qt/update_notification_bar.py @@ -43,7 +43,7 @@ from bitcoin_safe.threading_manager import TaskThread, ThreadingManager from ... import __version__ -from ...html import html_f +from ...html_utils import html_f from ...signals import SignalsMin from ...signature_manager import ( Asset, diff --git a/bitcoin_safe/gui/qt/utxo_list.py b/bitcoin_safe/gui/qt/utxo_list.py index 1a47f0b..4c371df 100644 --- a/bitcoin_safe/gui/qt/utxo_list.py +++ b/bitcoin_safe/gui/qt/utxo_list.py @@ -72,7 +72,7 @@ from ...i18n import translate from ...signals import Signals, UpdateFilter, UpdateFilterReason -from ...util import Satoshis, block_explorer_URL, clean_list +from ...util import Satoshis, block_explorer_URL, clean_list, time_logger from ...wallet import TxStatus, Wallet, get_wallets from .category_list import CategoryEditor from .my_treeview import ( @@ -304,6 +304,7 @@ def get_headers(self): self.Columns.PARENTS: self.tr("Parents"), } + @time_logger def update_with_filter(self, update_filter: UpdateFilter) -> None: should_update = False if should_update or update_filter.refresh_all: diff --git a/bitcoin_safe/gui/qt/wallet_steps.py b/bitcoin_safe/gui/qt/wallet_steps.py index 47a3a8a..18da389 100644 --- a/bitcoin_safe/gui/qt/wallet_steps.py +++ b/bitcoin_safe/gui/qt/wallet_steps.py @@ -44,7 +44,7 @@ from bitcoin_safe.gui.qt.qr_types import QrType from bitcoin_safe.gui.qt.register_multisig import USBRegisterMultisigWidget from bitcoin_safe.gui.qt.wallet_steps_base import WalletStepsBase -from bitcoin_safe.html import html_f +from bitcoin_safe.html_utils import html_f from bitcoin_safe.i18n import translate from bitcoin_safe.signals import Signals, UpdateFilter, UpdateFilterReason from bitcoin_safe.threading_manager import ThreadingManager diff --git a/bitcoin_safe/html.py b/bitcoin_safe/html_utils.py similarity index 100% rename from bitcoin_safe/html.py rename to bitcoin_safe/html_utils.py diff --git a/bitcoin_safe/network_config.py b/bitcoin_safe/network_config.py index b5f56f4..8cf06c3 100644 --- a/bitcoin_safe/network_config.py +++ b/bitcoin_safe/network_config.py @@ -41,7 +41,7 @@ from bitcoin_safe.pythonbdk_types import BlockchainType, CBFServerType from bitcoin_safe.storage import BaseSaveableClass, filtered_for_init -from .html import link +from .html_utils import link from .i18n import translate MIN_RELAY_FEE = 1 diff --git a/bitcoin_safe/signals.py b/bitcoin_safe/signals.py index fc827ab..80effdc 100644 --- a/bitcoin_safe/signals.py +++ b/bitcoin_safe/signals.py @@ -217,6 +217,7 @@ class Signals(SignalsMin): I immediately break the rule however for pyqtSignal, which is a function call """ + open_file_path = pyqtSignal(object) open_tx_like = pyqtSignal(object) event_wallet_tab_closed = pyqtSignal() event_wallet_tab_added = pyqtSignal() diff --git a/bitcoin_safe/storage.py b/bitcoin_safe/storage.py index 7f96208..57bbe7b 100644 --- a/bitcoin_safe/storage.py +++ b/bitcoin_safe/storage.py @@ -35,7 +35,6 @@ logger = logging.getLogger(__name__) -import copy import enum import json import os @@ -288,7 +287,7 @@ def _from_file(cls, filename: str, password: Optional[str] = None, class_kwargs= class SaveAllClass(BaseSaveableClass): def dump(self): d = super().dump() - d.update(copy.deepcopy(self.__dict__)) + d.update(self.__dict__.copy()) return d @classmethod diff --git a/bitcoin_safe/util.py b/bitcoin_safe/util.py index 8728ad9..4d5d5f1 100644 --- a/bitcoin_safe/util.py +++ b/bitcoin_safe/util.py @@ -52,16 +52,19 @@ import json import logging +from concurrent.futures import ThreadPoolExecutor -from bitcoin_safe.gui.qt.data_tab_widget import T +import numpy as np -logger = logging.getLogger(__name__) +from bitcoin_safe.gui.qt.data_tab_widget import T2, T +logger = logging.getLogger(__name__) import builtins import logging import os import re import sys +import time from datetime import datetime, timedelta from pathlib import Path from typing import ( @@ -271,6 +274,45 @@ def wrapper(self, *args, **kwargs): return decorator +def time_logger(func): + @wraps(func) + def wrapper(*args, **kwargs): + start_time = time.time() + result = func(*args, **kwargs) + end_time = time.time() + duration = end_time - start_time + + message = f"Function {func.__qualname__} needed {duration:.3f}s" + if duration < 5e-2: + logger.debug(message) + else: + logger.info(message) + + return result + + return wrapper + + +def threadtable(f, arglist, max_workers=20): + with ThreadPoolExecutor(max_workers=int(max_workers)) as executor: + logger.debug("Starting {} threads {}({})".format(max_workers, str(f), str(arglist))) + res = [] + for arg in arglist: + res.append(executor.submit(f, arg)) + return [r.result() for r in res] + + +@time_logger +def threadtable_batched(f: Callable[[T], T2], txs: List[T], number_chunks=8) -> List[T2]: + chunks = np.array_split(np.array(txs), number_chunks) + + def batched_f(txs): + return [f(tx) for tx in txs] + + result = threadtable(batched_f, chunks, max_workers=number_chunks) + return sum(result, []) + + def clean_dict(d: Dict): return {k: v for k, v in d.items() if v} diff --git a/bitcoin_safe/wallet.py b/bitcoin_safe/wallet.py index 4cb5b9b..36e1a76 100644 --- a/bitcoin_safe/wallet.py +++ b/bitcoin_safe/wallet.py @@ -67,6 +67,7 @@ hash_string, instance_lru_cache, replace_non_alphanumeric, + time_logger, ) @@ -368,6 +369,7 @@ def peek_address( return self.peek_addressinfo(index, is_change=is_change).address.as_string() @instance_lru_cache() + @time_logger def list_unspent(self) -> List[bdk.LocalUtxo]: start_time = time() result: List[bdk.LocalUtxo] = super().list_unspent() @@ -947,6 +949,7 @@ def get_output_addresses(self, transaction: bdk.Transaction) -> List[str]: ] return [a for a in output_addresses if a] + @time_logger def fill_commonly_used_caches(self) -> None: i = 0 new_addresses_were_watched = True @@ -960,13 +963,15 @@ def fill_commonly_used_caches(self) -> None: self.get_height() advanced_tips = self.advance_tips_by_gap() - logger.info(f"{self.id} tips were advanced by {advanced_tips}") new_addresses_were_watched = any(advanced_tips) + if new_addresses_were_watched: + logger.info(f"{self.id} tips were advanced by {advanced_tips}") i += 1 if i > 100: break self.bdkwallet.list_unspent() self.get_dict_fulltxdetail() + self.get_all_txos_dict() @instance_lru_cache() def get_txs(self) -> Dict[str, bdk.TransactionDetails]: @@ -1200,6 +1205,7 @@ def get_involved_txids(self, address: str) -> Set[str]: return self.cache_address_to_txids.get(address, set()) @instance_lru_cache() + @time_logger def get_dict_fulltxdetail(self) -> Dict[str, FullTxDetail]: """ Createa a map of txid : to FullTxDetail @@ -1248,6 +1254,7 @@ def process_inputs(tx: bdk.TransactionDetails) -> Tuple[str, FullTxDetail]: # map : 2.714s # for loop: 2.76464 # multithreading : 6.3021s + # threadtable_batched: 4.1 s , this should perform best, however bdk is probably the bottleneck and not-multithreading capable key_value_pairs = list(map(process_inputs, txs)) for txid, fulltxdetail in key_value_pairs: append_dicts(txid, list(fulltxdetail.inputs.values())) diff --git a/poetry.lock b/poetry.lock index cfb849c..59d7360 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +[[package]] +name = "altgraph" +version = "0.17.4" +description = "Python graph (network) package" +optional = false +python-versions = "*" +files = [ + {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"}, + {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, +] + [[package]] name = "appdirs" version = "1.4.4" @@ -30,17 +41,6 @@ types-python-dateutil = ">=2.8.10" doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] -[[package]] -name = "asn1crypto" -version = "1.5.1" -description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" -optional = false -python-versions = "*" -files = [ - {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, - {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, -] - [[package]] name = "base58" version = "2.1.1" @@ -110,13 +110,13 @@ requests = ">=2.31.0,<3.0.0" [[package]] name = "bitcoin-qr-tools" -version = "0.14.9" +version = "0.14.10" description = "Python bitcoin qr reader and generator" optional = false python-versions = "<3.13,>=3.9" files = [ - {file = "bitcoin_qr_tools-0.14.9-py3-none-any.whl", hash = "sha256:e6a293affd98c77e182fd3f0f254f5581a6c35a9011d3336b2d248934681724f"}, - {file = "bitcoin_qr_tools-0.14.9.tar.gz", hash = "sha256:2e91b07b1cdb2c940c270ed8de70c89dec747e6806028f9ce7006bd3e7d2a2de"}, + {file = "bitcoin_qr_tools-0.14.10-py3-none-any.whl", hash = "sha256:4de08732af4859acb26105bfb9f883cc25a294f83a80807eb1c9ea1334c15fec"}, + {file = "bitcoin_qr_tools-0.14.10.tar.gz", hash = "sha256:562d3bf2a6d2c2051e742fc8b16a5c84d151081e05dd9ecbaeef8b3dcd873573"}, ] [package.dependencies] @@ -211,13 +211,13 @@ virtualenv = ["virtualenv (>=20.0.35)"] [[package]] name = "cachecontrol" -version = "0.14.0" +version = "0.14.1" description = "httplib2 caching for requests" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "cachecontrol-0.14.0-py3-none-any.whl", hash = "sha256:f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0"}, - {file = "cachecontrol-0.14.0.tar.gz", hash = "sha256:7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938"}, + {file = "cachecontrol-0.14.1-py3-none-any.whl", hash = "sha256:65e3abd62b06382ce3894df60dde9e0deb92aeb734724f68fa4f3b91e97206b9"}, + {file = "cachecontrol-0.14.1.tar.gz", hash = "sha256:06ef916a1e4eb7dba9948cdfc9c76e749db2e02104a9a1277e8b642591a0f717"}, ] [package.dependencies] @@ -226,7 +226,7 @@ msgpack = ">=0.5.2,<2.0.0" requests = ">=2.16.0" [package.extras] -dev = ["CacheControl[filecache,redis]", "black", "build", "cherrypy", "furo", "mypy", "pytest", "pytest-cov", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] +dev = ["CacheControl[filecache,redis]", "build", "cherrypy", "codespell[tomli]", "furo", "mypy", "pytest", "pytest-cov", "ruff", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] filecache = ["filelock (>=3.8.0)"] redis = ["redis (>=2.10.5)"] @@ -800,51 +800,6 @@ six = ">=1.9.0" gmpy = ["gmpy"] gmpy2 = ["gmpy2"] -[[package]] -name = "electrumsv-secp256k1" -version = "18.0.0" -description = "Cross-platform Python CFFI bindings for libsecp256k1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "electrumsv-secp256k1-18.0.0.tar.gz", hash = "sha256:a45b562dc1dbb3554d4c016a6fe0f28fc40d588a7c94f11d0702ad9817df0a63"}, - {file = "electrumsv_secp256k1-18.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87c496226a3dd25e990d1b01b53223788d307e9985c574b5e5aff653237cc886"}, - {file = "electrumsv_secp256k1-18.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:229b59f6beac5c4dd51026148f6ad4adb93aa6d4a673d4c02ed2626926679d00"}, - {file = "electrumsv_secp256k1-18.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc6dc31e00e4147ec37cff753fec7e4315f406689441f9ab37862ab3ec28f0b"}, - {file = "electrumsv_secp256k1-18.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fbf45c68b9099431c0aa4874b96990d97d77e5620de67b34fb2b1d35f354842"}, - {file = "electrumsv_secp256k1-18.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0b93ed635b1c4e1a18eef5fe1722a3ad7275bfa136099f5895a9b3266b778dac"}, - {file = "electrumsv_secp256k1-18.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8c10fca4a4460d56e20baec6a46237fe79378b314e70f5814dcfb73574f9dfd"}, - {file = "electrumsv_secp256k1-18.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31838e76269103c60c061aa6a9e4dc84def3fb9d42c21d22deeb1c26fdaead90"}, - {file = "electrumsv_secp256k1-18.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de791668636fb742a07b1bfddcd2a5188de883b50081dbe6a9e16a422a7cddc9"}, - {file = "electrumsv_secp256k1-18.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d074d99f099e117cfd8204e334efd5e754d8d039ae8ba9bac21565d26d1252b7"}, - {file = "electrumsv_secp256k1-18.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ccc8c884bd6da92e6fff37b00f9da39f8924940c47d96b4cd296e4044ad293e"}, - {file = "electrumsv_secp256k1-18.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:31deb26e4bb30dc76443f84775f5e8d38b3638e7b6190d15400961156f6897a7"}, - {file = "electrumsv_secp256k1-18.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7e37bf72a08ef2f3cb4bb0e9610bb34525c27cb1232a219d0f336ba7e1b61bc7"}, - {file = "electrumsv_secp256k1-18.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e09ea97e97df6c2ee794157d7eaf80634b6902287dacf92394bac4b6b769ae6f"}, - {file = "electrumsv_secp256k1-18.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b32190e3787beecb8c478d33b56a46777a60e154dfb7e5f4166d751dc4c6080c"}, - {file = "electrumsv_secp256k1-18.0.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5c6dbd50c4bff275b32fd85c6af5f2657a0c57ef5932263936924bd60e6bcbc"}, - {file = "electrumsv_secp256k1-18.0.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d87c927918202dec8f04e6befc1557a1418072f2c811a819dd187a017281ffed"}, - {file = "electrumsv_secp256k1-18.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:01699802080ebbb36bed0f1483a3ad6adc76480c70c4ffc5be1aa313d1093ba3"}, - {file = "electrumsv_secp256k1-18.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9c9d83757d68f615ea69be4baebe9f049571868f5bf1eadc58a2d8d48a0cb7f5"}, - {file = "electrumsv_secp256k1-18.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:41e5b409d55f1f4ebe9c4dc70046628fe418e2649c4ecab9b3eb418f9ead0766"}, - {file = "electrumsv_secp256k1-18.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c062621382cd43e95461953b874ccce118dea780196287fc9d9055289fefae"}, - {file = "electrumsv_secp256k1-18.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ce423e1dcfeba7573c71ca1abc2293436068b6448ff622af4cbf817f4103f5"}, - {file = "electrumsv_secp256k1-18.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8077021ffddf48911716ce631ec1b452e3290d70ed71daa14cd3443394506b4a"}, - {file = "electrumsv_secp256k1-18.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a302bfbc2a19747319dbfb6dcc7d85027fed29f0d062ba8a9248a7803debb4"}, - {file = "electrumsv_secp256k1-18.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2d69f345272de33d1afa7bfe949a6cbaa450a5c57a38ad011c0f16f126e4937d"}, - {file = "electrumsv_secp256k1-18.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6dcd860e46debec06c438a2c50c0806021670b73109fadbff581f79c355ed78"}, - {file = "electrumsv_secp256k1-18.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0948827d414ddeb73295a0467feb1107b2ea945554e104cdca19eeb7b755c0d6"}, - {file = "electrumsv_secp256k1-18.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffedd23dbec61379ea4e17f0199631c7ae4de169570f2880fd71b603cef6e190"}, - {file = "electrumsv_secp256k1-18.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4f756a189886bb637af150f043d6ff4e6192ee58ae6d282e80404a77980808d6"}, - {file = "electrumsv_secp256k1-18.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:90fdb3dcd5fe4f56835b083cac735c992801ddeeddc537db33d9700110cb44de"}, - {file = "electrumsv_secp256k1-18.0.0-py3-none-win32.whl", hash = "sha256:4840431aee8ad7b73198558fdbb033f213a6d6a8e80e293c131e3c973db87124"}, - {file = "electrumsv_secp256k1-18.0.0-py3-none-win_amd64.whl", hash = "sha256:a209338a901f84d87da8cf4e5ce8df18717aa89ffc70383d44ac2142e689039a"}, -] - -[package.dependencies] -asn1crypto = "*" -cffi = ">=1.3.0" - [[package]] name = "exceptiongroup" version = "1.2.2" @@ -1010,80 +965,90 @@ test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", [[package]] name = "hidapi" -version = "0.14.0.post2" +version = "0.14.0.post3" description = "A Cython interface to the hidapi from https://github.com/libusb/hidapi" optional = false python-versions = "*" files = [ - {file = "hidapi-0.14.0.post2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:023bca2a95856c185b01978b3794a8302e5a38cf1a8fa7b9c14c537b928ec762"}, - {file = "hidapi-0.14.0.post2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f6c27a5efd629dde57c97bb621bfe87fd40ece01c3c80ca8f937cfd8023adbe"}, - {file = "hidapi-0.14.0.post2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fe7645dfb3d577428023ea855664602d55dacdcf474c059217c6f5cc175bcd6"}, - {file = "hidapi-0.14.0.post2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b606a4123a296f63a0e8d9904ddbed6b90957f454cb3df315684eac43bedc1e6"}, - {file = "hidapi-0.14.0.post2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20b981c6828b92ac8c5e2fb3182fe95c528dc8355d298ec457ae8773dc1ffb2a"}, - {file = "hidapi-0.14.0.post2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8b000a44176e09ab2921aa1402efbaa84c708786c3e825b64fbac294353b373e"}, - {file = "hidapi-0.14.0.post2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd985ca84d7abd0d8fa57a5f1639d14292429028588b19f58b729b02c0ea9928"}, - {file = "hidapi-0.14.0.post2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad21f0da766df0177d2d8bc06c8fd65303a37f79d2890e8133980080e1159eb0"}, - {file = "hidapi-0.14.0.post2-cp310-cp310-win32.whl", hash = "sha256:4379f218712f9800c6d89c17902da5ebb06c1e6afde66aaf2d8bd0f5f6fb9232"}, - {file = "hidapi-0.14.0.post2-cp310-cp310-win_amd64.whl", hash = "sha256:4635c294a2ccfe1e862ba324dc4a5c068fd989c138b09880052a5903b37e0d54"}, - {file = "hidapi-0.14.0.post2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d3ca0ff9179bfa2337b36eaa6a4f1a3e4c8e643cf698adfef65700bffb6fe5c"}, - {file = "hidapi-0.14.0.post2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b1bca2741492c67a6cb4d7b57c0db734543556b369d3604505c62a47607767f"}, - {file = "hidapi-0.14.0.post2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef474da943187befd9a55270ba521f42930565b84bd59caa276adeda29669853"}, - {file = "hidapi-0.14.0.post2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a6991f55aebd3f84cc924fd838e0052422a5a3921f5d7df0ce6c9600f09db9a"}, - {file = "hidapi-0.14.0.post2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95340c0d69245820a2075d516df9a99c466d92db29fa78df5f13d9cfee4716d"}, - {file = "hidapi-0.14.0.post2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5ebdc20915c6256c738116a3491bc5194d74233bb6a4ad7d05983dfec54a22f2"}, - {file = "hidapi-0.14.0.post2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:04fd0c791af679dc6de02205f5fe321e46b7cae0e8d10d7f579abb8bcca0c595"}, - {file = "hidapi-0.14.0.post2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70038c3bd26c2ec7520964d9cfe5df81928ff34a2821e83914c28a9d4d8550b7"}, - {file = "hidapi-0.14.0.post2-cp311-cp311-win32.whl", hash = "sha256:bfe65ee33f0ecafde4e742fd7c8482a914bcd0bc69c04edf40788c714eec865d"}, - {file = "hidapi-0.14.0.post2-cp311-cp311-win_amd64.whl", hash = "sha256:c79d60d42b3437e0553052de108253db0e9e4a5c432996d95028d219afd4a3f3"}, - {file = "hidapi-0.14.0.post2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eaa04f8fb7b5d9d7e3eeb502317941559ecfc4a76d2527ee0d1d835794ac5aa"}, - {file = "hidapi-0.14.0.post2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6e81890f51bae29ae81c3ff02fed754d05f69a408fa038ff1056aec90c56333"}, - {file = "hidapi-0.14.0.post2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:511ccf1fa7f159b95d31d9fb718ebfc35322252d7355a3afaa724b65cdbbeff6"}, - {file = "hidapi-0.14.0.post2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eecc24722c561348c04f3f0819534028a7dd2e35a16f195625e2465f95fb8c6f"}, - {file = "hidapi-0.14.0.post2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b979408ba1c65ea7e39e35e6cb9d29705265ebfca568627333876f7a030818"}, - {file = "hidapi-0.14.0.post2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:556f99a3887065f0e86fb4b3968800c34fa25e6ea54047ef850ee639c69032da"}, - {file = "hidapi-0.14.0.post2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59465c87f028e70162f04d3e69014cd03bb2a4ceed7db6d0a160ec9627fb9324"}, - {file = "hidapi-0.14.0.post2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:de561a9336aa92263a4a5757deffdacb0e04de84590e15a7d5413f574790a210"}, - {file = "hidapi-0.14.0.post2-cp312-cp312-win32.whl", hash = "sha256:6f35ad2007e04d6dd80807d72a66781915da538818587f950cea3c3c36512aa7"}, - {file = "hidapi-0.14.0.post2-cp312-cp312-win_amd64.whl", hash = "sha256:493aad5fe1630f547857ef93a150eb99c07e836b82a340bd0cfdf87f495c983d"}, - {file = "hidapi-0.14.0.post2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6dba51ebd2c31c06f33fefc3cff0f48cb6a0e94015392f09e91a1504cb2b3a32"}, - {file = "hidapi-0.14.0.post2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:127e4f0b048ec9fc08e29596de411bbb856b6f224ecca26b32ded398340f9d4f"}, - {file = "hidapi-0.14.0.post2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f30df6f41d504a7bceeece097e2078ffccb91acdcdc0a553b27377c48f9cffbc"}, - {file = "hidapi-0.14.0.post2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:826dc8b82a094e80538c68a3e0d6b252b765ea49daa9bfbde0221fdb1eead058"}, - {file = "hidapi-0.14.0.post2-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:ad280e0db1496d14c471d83c0157f1ac440c06cb143e05096940d397b3ab04ba"}, - {file = "hidapi-0.14.0.post2-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:84e921d9de9dfb39a0cbce4db94d9ea00365f1137f12f24b22fa14170d0efb4b"}, - {file = "hidapi-0.14.0.post2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:b1c2eaa44f86700dba943bf6217157631e3ea836569b6494abfa3fff49f50b0e"}, - {file = "hidapi-0.14.0.post2-cp36-cp36m-win32.whl", hash = "sha256:a1cf34598f7642dc602de88ae1ed80ef5f4efb7e5b054528e33745bdfd93379e"}, - {file = "hidapi-0.14.0.post2-cp36-cp36m-win_amd64.whl", hash = "sha256:5bd9b996882d9542cd1a927fbfc6c5ac34fe3290362050ad68d379b6a100c470"}, - {file = "hidapi-0.14.0.post2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:251f93ae3e919bfc06217a2362239982b8d0913b38071b0a9253934275158e10"}, - {file = "hidapi-0.14.0.post2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39104841a2dce697cb4deeefe0ec08710dfb4258c6b078656989e11d1c62e71a"}, - {file = "hidapi-0.14.0.post2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a6e7570cee92762a9d6bc785d1ffb221081fc28ba2bcbbd89030cd2757b5773"}, - {file = "hidapi-0.14.0.post2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92ac5a88cc56f2b30d13cfb4aa208b6c4874a1d3fe80855d092e1c33207eaf27"}, - {file = "hidapi-0.14.0.post2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:fafef403f32514c069cefe9484f255ac6b2bfa11f9160f22da3ffae8c4b01dea"}, - {file = "hidapi-0.14.0.post2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:07841b71d78e21f15fd2ea9d7d313c7aa088c3415d6cefa1e932353fac6eb0b3"}, - {file = "hidapi-0.14.0.post2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:44438fff75b4ccd158a0ed692dba828a3735ce4963d0ae3a03c4b6dc2a020f18"}, - {file = "hidapi-0.14.0.post2-cp37-cp37m-win32.whl", hash = "sha256:209208db37a061ca08786184bc7060aace9e38bc45de268e60ac808e0f42952c"}, - {file = "hidapi-0.14.0.post2-cp37-cp37m-win_amd64.whl", hash = "sha256:466b4c2222ba58dd6603ca31777b8210d414751ae3a6e16d27d830e6d402a691"}, - {file = "hidapi-0.14.0.post2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6110c47d531f9d5e2947619f11623371d3efaade5b1ad81501ed0b6767296bbb"}, - {file = "hidapi-0.14.0.post2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:03926c1e788daff7e038f2d39460348ce5d624a98a72d9d291f619c9fbbb754e"}, - {file = "hidapi-0.14.0.post2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5526d812a53e3050317003bfa6a923ab1e8d4bf10a75a3ec4e2a6361b9967d2"}, - {file = "hidapi-0.14.0.post2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4b7c2e84ba004140d8555db302daf5d3f9d1d073cbc680b4cd384ee9a943395"}, - {file = "hidapi-0.14.0.post2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f87614ebc0cbc399eb7682a6f1f18f4afd522e2b76df888ddc4305f6ec3af186"}, - {file = "hidapi-0.14.0.post2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:857b81b124e6602f77f711b11ce08cb809a7e7e698751b0e09fff26f40571ffe"}, - {file = "hidapi-0.14.0.post2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:0eab40fb174db083233d105fdae17e5efaeb41d887b48fe4cfe98bfe48c03617"}, - {file = "hidapi-0.14.0.post2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9494327db1e6e5fd6630701cf4cc28da9dc2465d7903e9f2c1e9cf2228f3c50"}, - {file = "hidapi-0.14.0.post2-cp38-cp38-win32.whl", hash = "sha256:10f015eb35e28f5101af5ac16df0f08e0a31912dc8a5704c828d8ff0c3776b0d"}, - {file = "hidapi-0.14.0.post2-cp38-cp38-win_amd64.whl", hash = "sha256:2e604d59634a6162fcfbf16b07d2c6cc6dc278ba260246aa22f0b4ae6dc0e586"}, - {file = "hidapi-0.14.0.post2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc991af741ca3caf582ad1c717ce81c6c4021cbcb237eae534701beb819f867c"}, - {file = "hidapi-0.14.0.post2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8727975928662fa3d43abcc97acd581a3d4a62d6e701881e3296f40335f1778e"}, - {file = "hidapi-0.14.0.post2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e68dc10e6a06ba818d5f0cdc90045332784cf485279c18d7a054b22344814f"}, - {file = "hidapi-0.14.0.post2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eccc63cd08142791ffa2c213c6f47f3094a267a3ab2c10261704bdad08ecd806"}, - {file = "hidapi-0.14.0.post2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:386a9df574ae5715f811eafe5d8207c2eec986543146c27b4beea9957ec95567"}, - {file = "hidapi-0.14.0.post2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:91695c2f5ab3f91e27b4e1782cea57a517472b2d57e49a3eec5c86b5fbecd13f"}, - {file = "hidapi-0.14.0.post2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9de52268c4f29f23061d97020b46d56b03047834897be3bfcb95ebd89d32d636"}, - {file = "hidapi-0.14.0.post2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f1d6f11234b9a3fcfd1d81e328cd4af09ab4e7e9b251f9eb772542f1ae1fada"}, - {file = "hidapi-0.14.0.post2-cp39-cp39-win32.whl", hash = "sha256:d678979fcffd93d1d2f69ca080f3af994ce8be032bff3d929f45f1cbf2ce74a3"}, - {file = "hidapi-0.14.0.post2-cp39-cp39-win_amd64.whl", hash = "sha256:8e1d0396f415cb5d1cd3a1dfa8c0aa15d14f8849e7e0f9b4782e614128ec15ad"}, - {file = "hidapi-0.14.0.post2.tar.gz", hash = "sha256:6c0e97ba6b059a309d51b495a8f0d5efbcea8756b640d98b6f6bb9fdef2458ac"}, + {file = "hidapi-0.14.0.post3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aadb8d5014d3a7fb905149bca7ae491ede4f6ba72e7b1d500418a87ccc7c5ce7"}, + {file = "hidapi-0.14.0.post3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fce662f685c5361bbc1fe7c31bd13320c3224f8655b6bcecd04b00a364a5c2ba"}, + {file = "hidapi-0.14.0.post3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f28ff55dad8660ab44261c126a561ce4e4e2952d0e28a1fc7c6e417710b484ee"}, + {file = "hidapi-0.14.0.post3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:766a40995ab7c9232e339cd27ac13063b6d10fa3daec251771ea9176ae405af8"}, + {file = "hidapi-0.14.0.post3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec7370f30d1df903a96c62b15f58b9855fb7d907d7be1b885ec0fb14d7f55883"}, + {file = "hidapi-0.14.0.post3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b9b957d2306b33bc3db0fdc0698adf2c37fa21e354d54737c8b1818f4022a852"}, + {file = "hidapi-0.14.0.post3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a697af8e85095387423d57253ae72cce81212ba39925a6a22a7a1f820022e290"}, + {file = "hidapi-0.14.0.post3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0e16efef0786789afc493beeab1cb639bdca79938277482af03297be1ac319e0"}, + {file = "hidapi-0.14.0.post3-cp310-cp310-win32.whl", hash = "sha256:a81efa6ec967e8098d03de3ba0e9a08aefc100d8a44de4c701517ef02acda7a2"}, + {file = "hidapi-0.14.0.post3-cp310-cp310-win_amd64.whl", hash = "sha256:b001fee4bcc11a2768283f3f0b3bdbdc049c17d451e7f38fc3156ec04abac856"}, + {file = "hidapi-0.14.0.post3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1fffaa792caf7948c9248212fcf7a9a3377f064c2a46eeec78e09ad256ee1c93"}, + {file = "hidapi-0.14.0.post3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2808190710e8625111137c124d8d351791ade0aef0c9fa6d70f45eaaad1bd472"}, + {file = "hidapi-0.14.0.post3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75993cee698d22f53a695d7f8b769d63c8b9443d101dc1074965b4feb861641d"}, + {file = "hidapi-0.14.0.post3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:319fb4340b516d04b9aed164afd437ec8aff8463653f924351f582309c9cb942"}, + {file = "hidapi-0.14.0.post3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6801ac18c5cf4a6d20a755485ea99ba0d912cb0501ae119e94bca470b8aa99a"}, + {file = "hidapi-0.14.0.post3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0453507d326cf40348642c155d3be1159c7ac12c83a22b01d02581fe7743cc7"}, + {file = "hidapi-0.14.0.post3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6023087174626ea53b2759514a2bfce958316625406e804256e9da64a1e10039"}, + {file = "hidapi-0.14.0.post3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc26c64030153cab1c6bfecd8b42577143a78661c7c8adf4fcdaeff6ebe20368"}, + {file = "hidapi-0.14.0.post3-cp311-cp311-win32.whl", hash = "sha256:58bf07c9115a49e8cc4e571b192b690adc5d0324a7e1960d0823d0fa23a8ee23"}, + {file = "hidapi-0.14.0.post3-cp311-cp311-win_amd64.whl", hash = "sha256:6951cc24a7f9b247d7dfafad574c693e5227719acd35b619c7d08f6d8b8075af"}, + {file = "hidapi-0.14.0.post3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c1790261f1b62cf9f18bbe9fa4453097ddfba02405ca17e63bcc751d376147e5"}, + {file = "hidapi-0.14.0.post3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b2efb4eb24f16aceb0aca6215b80ce9d21135764e9fe145242d9b26f9029ec95"}, + {file = "hidapi-0.14.0.post3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5aa5971fddeebc976a36af6ed37f4a3ac203787a7281cdaffe4f0bbc0fc7042"}, + {file = "hidapi-0.14.0.post3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72bc7f8b97e2f11bc814294e327f4453f8739d0a349828a9a356310dfd7c2b08"}, + {file = "hidapi-0.14.0.post3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e51e3d63c36bf168f6030b00754b0c8085779e6d0c99aed59db38080b9481fd"}, + {file = "hidapi-0.14.0.post3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e0149c7ffc883a81eac0cfe0a66bcf2f5114eca47e52da17ed7e4b7a6c04c2b"}, + {file = "hidapi-0.14.0.post3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6b1552bf5cf4aa0fa68cb4d081804194197cf7900d73dad2f068240ee56f77af"}, + {file = "hidapi-0.14.0.post3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b0e5c2b7d15cc84612fd83414e7ab4301ac7041b9f0fe418580d1f4f25d13514"}, + {file = "hidapi-0.14.0.post3-cp312-cp312-win32.whl", hash = "sha256:177bf73d36f5ad54947cd61bdc9875f00c178d35cf9a8947be67ed953f958100"}, + {file = "hidapi-0.14.0.post3-cp312-cp312-win_amd64.whl", hash = "sha256:0bb1f373a70fe2639ebac2eaa5d800d1f727ce5a96e7fb88396e3bd02e4b5b9b"}, + {file = "hidapi-0.14.0.post3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:effc5c6b4d1f1964bb5f37fa86e9aeabf491f262c4536c52c22d11d5e5f7ab99"}, + {file = "hidapi-0.14.0.post3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3a2b1581b051a78e816aa054aedcb73f8c767402405a23192d2bef107dee81cd"}, + {file = "hidapi-0.14.0.post3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a10c598ea40a9cda57ba044d8e8065c8980f89abe41e7aca470bf029f13116"}, + {file = "hidapi-0.14.0.post3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15a6967aa3b17d095556856f48a08e46136b4189bc9a718d5b89b84c3d1149a0"}, + {file = "hidapi-0.14.0.post3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab07b231184850de083ecd4730ce5047d58882f69ff103381077601fbb8a2da"}, + {file = "hidapi-0.14.0.post3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:266ebbbe0cc6cd521b9da49516de3977433f38bcfd3f113b57696ff95dd8a1b9"}, + {file = "hidapi-0.14.0.post3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:177609d5ac39ae166669568866c5fec51b73f59468c71df620fe22594ea7d5f3"}, + {file = "hidapi-0.14.0.post3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:037cea3bec054030e8bfdacd6a865cf2e79cf3f777473a17539c30e871a273b3"}, + {file = "hidapi-0.14.0.post3-cp313-cp313-win32.whl", hash = "sha256:5ca54ea43ec3b6e79c6e951a64f399443ed2cb72913f38d11d874e1edac7402c"}, + {file = "hidapi-0.14.0.post3-cp313-cp313-win_amd64.whl", hash = "sha256:51e935c9662316dccf932d9f50e96872be10e2eb0ca23b20287a8a99e26034c5"}, + {file = "hidapi-0.14.0.post3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:347b76702a959a72578760bf686e79a6dfe9707c86ad8e34e3934158fc2f8a47"}, + {file = "hidapi-0.14.0.post3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118f9ef9e22140237e02551cf8cf462faa51147de9674cb2e46e459dfffe3809"}, + {file = "hidapi-0.14.0.post3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb97aeb21905a97ea060e1038c959e35de5840e063bccc7ea9138eb7b1b5ddbf"}, + {file = "hidapi-0.14.0.post3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65f823e1d17887adc529efed0a37ca4aec3d47f9e6d0397b0b36384b11c960c6"}, + {file = "hidapi-0.14.0.post3-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:a7eb026c9f12cd2ac30450117e50f1819fdced9c384d4a6af257e9ca7a5f97c3"}, + {file = "hidapi-0.14.0.post3-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:2042aa59dc99c6dab635bcf9fffcbc7377b0a2dd04c53d164712124ada6fcc7b"}, + {file = "hidapi-0.14.0.post3-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:479c390198104c6f972635675268ac23913ce83efd49a67136873d1862b1f53a"}, + {file = "hidapi-0.14.0.post3-cp36-cp36m-win32.whl", hash = "sha256:ddf6993dbc1909f6fd4646cb230635cfcff74ec4e1a91ca4195e815b24d73cbd"}, + {file = "hidapi-0.14.0.post3-cp36-cp36m-win_amd64.whl", hash = "sha256:4fa2529d57ff72194f5606656043b3062a1ff790ab67a873db7581fd4b665c68"}, + {file = "hidapi-0.14.0.post3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:45a2a3c30fdfbe711d9c4c659566ac531cdcea7a853faab0e3503d21b62971e9"}, + {file = "hidapi-0.14.0.post3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bb2addc8e480487a75bd546ea8c71c67146a898b3317b07b5c3e88fd90bf122"}, + {file = "hidapi-0.14.0.post3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48fe83e1a14935482c963cf531c38b3d6f2f05af1e974367127179870780d79b"}, + {file = "hidapi-0.14.0.post3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c2f966a277ba0e013e6bfe03d6cad393935be37cb00fabe271ef363e19f0b8c"}, + {file = "hidapi-0.14.0.post3-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:faa12f70f763530a53682b2ee25ca73471ef6d81a803c6f63d217a1903454a56"}, + {file = "hidapi-0.14.0.post3-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:5eae45927b3226fe4d748d86eba48a31b723a7e49700b97e7ef1222420abcee7"}, + {file = "hidapi-0.14.0.post3-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:984baad5a291c167b47c6e6761e7b13608eab52d2f1424e564df8e0133268414"}, + {file = "hidapi-0.14.0.post3-cp37-cp37m-win32.whl", hash = "sha256:c8a53ae9a43070ae72c8cbca0aff731c14a68a1334a5fcf5ff0b0bf1ff1fba80"}, + {file = "hidapi-0.14.0.post3-cp37-cp37m-win_amd64.whl", hash = "sha256:2d09457f38b5dd11066e81b85448031ca470aa266335ba1f0e1bc694c38fe7ab"}, + {file = "hidapi-0.14.0.post3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7c7d3ee5e7f9ce7dc51f765710c68a7d5c16e0f595f7fbbad8b97a9ddd31961d"}, + {file = "hidapi-0.14.0.post3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dcbd1241babe0e048b60872232da2a4bbdcd4844cf8cee08002632a3238d1b9f"}, + {file = "hidapi-0.14.0.post3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:868db38189602e3cbf54baa2b4494127a4d8e9fda894d1429c93850af309f360"}, + {file = "hidapi-0.14.0.post3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f255534268571b5f462d2d4e6ff4939735adc49530532ee50ee2603e47abd59"}, + {file = "hidapi-0.14.0.post3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c0b2f2e4e9115a94234aa061ca60a842b076d046cbcaeb783cd8fcccd4bcda3"}, + {file = "hidapi-0.14.0.post3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:93f90bd76648d435e6e1fe11c725c6f8ffe37031da597fb008bf7e1dbcc64da8"}, + {file = "hidapi-0.14.0.post3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:0d484f72f91954c87f788f776fddc0d90e568a9de3d33bdd7e8888fc7b2b9696"}, + {file = "hidapi-0.14.0.post3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7a3a21e3f6cbe0b939dfd05c4c931067a40aed37167d7fed3630ad59b72a5e82"}, + {file = "hidapi-0.14.0.post3-cp38-cp38-win32.whl", hash = "sha256:76beb265ec1f8519df289dd3a2d22574492e71cef57e10e779742f899a087a6d"}, + {file = "hidapi-0.14.0.post3-cp38-cp38-win_amd64.whl", hash = "sha256:03c193c429ad1c66b112c151e061af5b3e82fe37bf0107b42ce2b33f4409766e"}, + {file = "hidapi-0.14.0.post3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63244aeb04a4259e2ccb7474daffd4bd90e3fae126e39d0ad5eed6f284eccb40"}, + {file = "hidapi-0.14.0.post3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e3e6b242d20e7bfb01eaac0cfd0ac4daadae61a9e07bf93d69a172eff2a1ee16"}, + {file = "hidapi-0.14.0.post3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b87c9008a022b60198c3fffa6d7959ac5b65f89ac3562e80af92283896b17be2"}, + {file = "hidapi-0.14.0.post3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:663ef2de03c59be95abcf6a8a033b59047d4957406ae179e9068fa55af678e6c"}, + {file = "hidapi-0.14.0.post3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2182426cfcfba553463046b8f61876b862e5a613492eb71b4875f6ffbd6f744"}, + {file = "hidapi-0.14.0.post3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6fa07c3ec6136a5cced7c22e6b811d89243e99eb657c8d49952774c98c50ff50"}, + {file = "hidapi-0.14.0.post3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ce32266dfe1fdb19f59d1cc1f321e02a8538776de3148e28c7d563ef4e9ebde7"}, + {file = "hidapi-0.14.0.post3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f86b46741e4b5bcbebdf3f8570bacd75e69f5172e82fd1307affa83f496e2908"}, + {file = "hidapi-0.14.0.post3-cp39-cp39-win32.whl", hash = "sha256:39fa225b4db9cd076f52cd5aaf724746d25575f14475adfc8d634908d9b78d1f"}, + {file = "hidapi-0.14.0.post3-cp39-cp39-win_amd64.whl", hash = "sha256:b5010b88c82d4823a12712d47cadf1d3ec4a3dff7169e55a0015fe2507aa07d8"}, + {file = "hidapi-0.14.0.post3.tar.gz", hash = "sha256:cef8943ebb0cc26f4c13a5387910883c2c1a90ebd280dd2213794748da9bfda0"}, ] [package.dependencies] @@ -1445,6 +1410,20 @@ dev = ["pre-commit", "tox"] docs = ["sphinx", "sphinx-autobuild", "sphinx-rtd-theme"] test = ["pytest", "pytest-cov", "pytest-tldr"] +[[package]] +name = "macholib" +version = "1.16.3" +description = "Mach-O header analysis and editing" +optional = false +python-versions = "*" +files = [ + {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, + {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, +] + +[package.dependencies] +altgraph = ">=0.17" + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -1716,64 +1695,66 @@ files = [ [[package]] name = "numpy" -version = "2.1.2" +version = "2.1.3" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" files = [ - {file = "numpy-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30d53720b726ec36a7f88dc873f0eec8447fbc93d93a8f079dfac2629598d6ee"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d3ca0a72dd8846eb6f7dfe8f19088060fcb76931ed592d29128e0219652884"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:fc44e3c68ff00fd991b59092a54350e6e4911152682b4782f68070985aa9e648"}, - {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7c1c60328bd964b53f8b835df69ae8198659e2b9302ff9ebb7de4e5a5994db3d"}, - {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdb606a7478f9ad91c6283e238544451e3a95f30fb5467fbf715964341a8a86"}, - {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d666cb72687559689e9906197e3bec7b736764df6a2e58ee265e360663e9baf7"}, - {file = "numpy-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6eef7a2dbd0abfb0d9eaf78b73017dbfd0b54051102ff4e6a7b2980d5ac1a03"}, - {file = "numpy-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:12edb90831ff481f7ef5f6bc6431a9d74dc0e5ff401559a71e5e4611d4f2d466"}, - {file = "numpy-2.1.2-cp310-cp310-win32.whl", hash = "sha256:a65acfdb9c6ebb8368490dbafe83c03c7e277b37e6857f0caeadbbc56e12f4fb"}, - {file = "numpy-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:860ec6e63e2c5c2ee5e9121808145c7bf86c96cca9ad396c0bd3e0f2798ccbe2"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b42a1a511c81cc78cbc4539675713bbcf9d9c3913386243ceff0e9429ca892fe"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:faa88bc527d0f097abdc2c663cddf37c05a1c2f113716601555249805cf573f1"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c82af4b2ddd2ee72d1fc0c6695048d457e00b3582ccde72d8a1c991b808bb20f"}, - {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:13602b3174432a35b16c4cfb5de9a12d229727c3dd47a6ce35111f2ebdf66ff4"}, - {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebec5fd716c5a5b3d8dfcc439be82a8407b7b24b230d0ad28a81b61c2f4659a"}, - {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2b49c3c0804e8ecb05d59af8386ec2f74877f7ca8fd9c1e00be2672e4d399b1"}, - {file = "numpy-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cbba4b30bf31ddbe97f1c7205ef976909a93a66bb1583e983adbd155ba72ac2"}, - {file = "numpy-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e00ea6fc82e8a804433d3e9cedaa1051a1422cb6e443011590c14d2dea59146"}, - {file = "numpy-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5006b13a06e0b38d561fab5ccc37581f23c9511879be7693bd33c7cd15ca227c"}, - {file = "numpy-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:f1eb068ead09f4994dec71c24b2844f1e4e4e013b9629f812f292f04bd1510d9"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7bf0a4f9f15b32b5ba53147369e94296f5fffb783db5aacc1be15b4bf72f43b"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d0fcae4f0949f215d4632be684a539859b295e2d0cb14f78ec231915d644db"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f751ed0a2f250541e19dfca9f1eafa31a392c71c832b6bb9e113b10d050cb0f1"}, - {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bd33f82e95ba7ad632bc57837ee99dba3d7e006536200c4e9124089e1bf42426"}, - {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8cde4f11f0a975d1fd59373b32e2f5a562ade7cde4f85b7137f3de8fbb29a0"}, - {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d95f286b8244b3649b477ac066c6906fbb2905f8ac19b170e2175d3d799f4df"}, - {file = "numpy-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ab4754d432e3ac42d33a269c8567413bdb541689b02d93788af4131018cbf366"}, - {file = "numpy-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e585c8ae871fd38ac50598f4763d73ec5497b0de9a0ab4ef5b69f01c6a046142"}, - {file = "numpy-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9c6c754df29ce6a89ed23afb25550d1c2d5fdb9901d9c67a16e0b16eaf7e2550"}, - {file = "numpy-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:456e3b11cb79ac9946c822a56346ec80275eaf2950314b249b512896c0d2505e"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a84498e0d0a1174f2b3ed769b67b656aa5460c92c9554039e11f20a05650f00d"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4d6ec0d4222e8ffdab1744da2560f07856421b367928026fb540e1945f2eeeaf"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:259ec80d54999cc34cd1eb8ded513cb053c3bf4829152a2e00de2371bd406f5e"}, - {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:675c741d4739af2dc20cd6c6a5c4b7355c728167845e3c6b0e824e4e5d36a6c3"}, - {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b2d4e667895cc55e3ff2b56077e4c8a5604361fc21a042845ea3ad67465aa8"}, - {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43cca367bf94a14aca50b89e9bc2061683116cfe864e56740e083392f533ce7a"}, - {file = "numpy-2.1.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:76322dcdb16fccf2ac56f99048af32259dcc488d9b7e25b51e5eca5147a3fb98"}, - {file = "numpy-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:32e16a03138cabe0cb28e1007ee82264296ac0983714094380b408097a418cfe"}, - {file = "numpy-2.1.2-cp313-cp313-win32.whl", hash = "sha256:242b39d00e4944431a3cd2db2f5377e15b5785920421993770cddb89992c3f3a"}, - {file = "numpy-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f2ded8d9b6f68cc26f8425eda5d3877b47343e68ca23d0d0846f4d312ecaa445"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ffef621c14ebb0188a8633348504a35c13680d6da93ab5cb86f4e54b7e922b5"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad369ed238b1959dfbade9018a740fb9392c5ac4f9b5173f420bd4f37ba1f7a0"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d82075752f40c0ddf57e6e02673a17f6cb0f8eb3f587f63ca1eaab5594da5b17"}, - {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1600068c262af1ca9580a527d43dc9d959b0b1d8e56f8a05d830eea39b7c8af6"}, - {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a26ae94658d3ba3781d5e103ac07a876b3e9b29db53f68ed7df432fd033358a8"}, - {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13311c2db4c5f7609b462bc0f43d3c465424d25c626d95040f073e30f7570e35"}, - {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:2abbf905a0b568706391ec6fa15161fad0fb5d8b68d73c461b3c1bab6064dd62"}, - {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bdd407c40483463898b84490770199d5714dcc9dd9b792f6c6caccc523c00952"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:da65fb46d4cbb75cb417cddf6ba5e7582eb7bb0b47db4b99c9fe5787ce5d91f5"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c193d0b0238638e6fc5f10f1b074a6993cb13b0b431f64079a509d63d3aa8b7"}, - {file = "numpy-2.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a7d80b2e904faa63068ead63107189164ca443b42dd1930299e0d1cb041cec2e"}, - {file = "numpy-2.1.2.tar.gz", hash = "sha256:13532a088217fa624c99b843eeb54640de23b3414b14aa66d023805eb731066c"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd"}, + {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3"}, + {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098"}, + {file = "numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c"}, + {file = "numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4"}, + {file = "numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23"}, + {file = "numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09"}, + {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a"}, + {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b"}, + {file = "numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee"}, + {file = "numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0"}, + {file = "numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9"}, + {file = "numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564"}, + {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512"}, + {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b"}, + {file = "numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc"}, + {file = "numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0"}, + {file = "numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9"}, + {file = "numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe"}, + {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43"}, + {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56"}, + {file = "numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a"}, + {file = "numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef"}, + {file = "numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f"}, + {file = "numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0"}, + {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408"}, + {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6"}, + {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f"}, + {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17"}, + {file = "numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48"}, + {file = "numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb"}, + {file = "numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761"}, ] [[package]] @@ -1800,13 +1781,24 @@ numpy = [ [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pefile" +version = "2023.2.7" +description = "Python PE parsing module" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, + {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, ] [[package]] @@ -2244,6 +2236,55 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyinstaller" +version = "6.11.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +optional = false +python-versions = "<3.14,>=3.8" +files = [ + {file = "pyinstaller-6.11.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:6fd68a3c1207635c49326c54881b89d5c3bd9ba061bbc9daa58c0902db1be39e"}, + {file = "pyinstaller-6.11.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:eddd53f231e51adc65088eac4f40057ca803a990239828d4a9229407fb866239"}, + {file = "pyinstaller-6.11.0-py3-none-manylinux2014_i686.whl", hash = "sha256:e6d229009e815542833fe00332b589aa6984a06f794dc16f2ce1acab1c567590"}, + {file = "pyinstaller-6.11.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:7d2cd2ebdcd6860f8a4abe2977264a7b6d260a7147047008971c7cfc66a656a4"}, + {file = "pyinstaller-6.11.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:d9ec6d4398b4eebc1d4c00437716264ba8406bc2746f594e253070a82378a584"}, + {file = "pyinstaller-6.11.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:04f71828aa9531ab18c9656985c1f09b83d10332c73a1f4a113a48b491906955"}, + {file = "pyinstaller-6.11.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:a843d470768d68b05684ccf4860c45b2eb13727f41667c0b2cd8f57ae231bd18"}, + {file = "pyinstaller-6.11.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:963dedc1f37144a4385f58f7f65f1c69c004a67faae522a2085b5ddb230c908b"}, + {file = "pyinstaller-6.11.0-py3-none-win32.whl", hash = "sha256:c71024c8a19c7b221b9152b2baff4c3ba849cada68dcdd34382ba09f0107451f"}, + {file = "pyinstaller-6.11.0-py3-none-win_amd64.whl", hash = "sha256:0e229610c22b96d741d905706f9496af472c1a9216a118988f393c98ecc3f51f"}, + {file = "pyinstaller-6.11.0-py3-none-win_arm64.whl", hash = "sha256:a5f716bb507517912fda39d109dead91fc0dd2e7b2859562522b63c61aa21676"}, + {file = "pyinstaller-6.11.0.tar.gz", hash = "sha256:cb4d433a3db30d9d17cf5f2cf7bb4df80a788d493c1d67dd822dc5791d9864af"}, +] + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +packaging = ">=22.0" +pefile = {version = ">=2022.5.30,<2024.8.26 || >2024.8.26", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2024.8" +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} +setuptools = ">=42.0.0" + +[package.extras] +completion = ["argcomplete"] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2024.9" +description = "Community maintained hooks for PyInstaller" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyinstaller_hooks_contrib-2024.9-py3-none-any.whl", hash = "sha256:1ddf9ba21d586afa84e505bb5c65fca10b22500bf3fdb89ee2965b99da53b891"}, + {file = "pyinstaller_hooks_contrib-2024.9.tar.gz", hash = "sha256:4793869f370d1dc4806c101efd2890e3c3e703467d8d27bb5a3db005ebfb008d"}, +] + +[package.dependencies] +packaging = ">=22.0" +setuptools = ">=42.0.0" + [[package]] name = "pyprof2calltree" version = "1.4.5" @@ -3160,4 +3201,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.11" -content-hash = "5570d04d570c99269902d86ac981c29c3d78fe2c9dd95411510c67490458a306" +content-hash = "8166976c2f0e93f1de09b13fe3221201306e53088c21d4e46f6fb28147877b8e" diff --git a/pyproject.toml b/pyproject.toml index 9e3580f..1b32227 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ line-length = 110 name = "bitcoin-safe" # the version here and in all other places in this toml are updated automatically # from the source: bitcoin_safe/__init__.py -version = "1.0.0b2" +version = "1.0.0b3" description = "Long-term Bitcoin savings made Easy" authors = [ "andreasgriffin ",] license = "GPL-3.0" @@ -35,12 +35,11 @@ reportlab = "4.0.8" cbor2 = "^5.6.0" pyqt6 = "^6.6.1" pyqt6-charts = "^6.6.0" -electrumsv-secp256k1 = "^18.0.0" python-gnupg = "^0.5.2" # bitcoin-qr-tools = {path="../bitcoin-qr-tools"} # "^0.10.15" # bitcoin-nostr-chat = {path="../bitcoin-nostr-chat"} # "^0.2.6" # bitcoin-usb = {path="../bitcoin-usb"} # "^0.3.3" -bitcoin-qr-tools = "^0.14.9" +bitcoin-qr-tools = "^0.14.10" bitcoin-nostr-chat = "^0.4.1" bitcoin-usb = "^0.5.3" numpy = "^2.0.1" @@ -50,12 +49,13 @@ pgpy = "^0.6.0" [tool.briefcase] project_name = "Bitcoin-Safe" bundle = "org.bitcoin-safe" -version = "1.0.0b2" +version = "1.0.0b3" url = "https://github.com/andreasgriffin/bitcoin-safe" -license.file = "LICENSE" author = "Andreas Griffin" author_email = "andreasgriffin@proton.me" +[tool.briefcase.license] +file = "LICENSE" [tool.briefcase.app.bitcoin-safe] formal_name = "Bitcoin-Safe" @@ -66,7 +66,7 @@ test_sources = [ "tests",] test_requires = [ "pytest",] resources = [ "bitcoin_safe/gui/locales/*.qm",] # these requirements are updated from the poetry lock file automatically -requires = ["appdirs==1.4.4", "arrow==1.3.0", "asn1crypto==1.5.1", "base58==2.1.1", "bdkpython==0.31.0", "binaryornot==0.4.4", "bitcoin-nostr-chat==0.4.1", "bitcoin-qr-tools==0.14.9", "bitcoin-usb==0.5.3", "briefcase==0.3.19", "build==1.2.2.post1", "cachecontrol==0.14.0", "cbor2==5.6.5", "certifi==2024.8.30", "cffi==1.17.1", "cfgv==3.4.0", "chardet==5.2.0", "charset-normalizer==3.4.0", "cleo==2.1.0", "click==8.1.7", "colorama==0.4.6", "cookiecutter==2.6.0", "crashtest==0.4.1", "cryptography==43.0.3", "defusedxml==0.7.1", "distlib==0.3.9", "dmgbuild==1.6.2", "ds-store==1.3.1", "dulwich==0.21.7", "ecdsa==0.19.0", "electrumsv-secp256k1==18.0.0", "exceptiongroup==1.2.2", "fastjsonschema==2.20.0", "filelock==3.16.1", "fonttools==4.54.1", "fpdf2==2.8.1", "gitdb==4.0.11", "gitpython==3.1.43", "hidapi==0.14.0.post2", "hwi==3.1.0", "identify==2.6.1", "idna==3.10", "importlib-metadata==8.5.0", "iniconfig==2.0.0", "installer==0.7.0", "jaraco-classes==3.4.0", "jeepney==0.8.0", "jinja2==3.1.4", "keyring==24.3.1", "libusb1==3.1.0", "lxml==5.3.0", "mac-alias==2.2.2", "markdown-it-py==3.0.0", "markupsafe==3.0.2", "mdurl==0.1.2", "mnemonic==0.21", "more-itertools==10.5.0", "msgpack==1.1.0", "mss==9.0.2", "nodeenv==1.9.1", "noiseprotocol==0.3.1", "nostr-sdk==0.32.2", "numpy==2.1.2", "opencv-python-headless==4.10.0.84", "packaging==24.1", "pexpect==4.9.0", "pgpy==0.6.0", "pillow==10.4.0", "pip==24.3.1", "pkginfo==1.11.2", "platformdirs==4.3.6", "pluggy==1.5.0", "poetry-core==1.9.1", "poetry-plugin-export==1.8.0", "pre-commit==3.8.0", "protobuf==4.25.5", "psutil==5.9.8", "ptyprocess==0.7.0", "pyaes==1.6.1", "pyasn1==0.6.1", "pycparser==2.22", "pygame==2.6.1", "pygments==2.18.0", "pyprof2calltree==1.4.5", "pyproject-hooks==1.2.0", "pyqrcode==1.2.1", "pyqt6==6.7.1", "pyqt6-charts==6.7.0", "pyqt6-charts-qt6==6.7.3", "pyqt6-qt6==6.7.3", "pyqt6-sip==13.8.0", "pyserial==3.5", "pytest==8.3.3", "pytest-qt==4.4.0", "pytest-xvfb==3.0.0", "python-bitcointx==1.1.4", "python-dateutil==2.9.0.post0", "python-gnupg==0.5.3", "python-slugify==8.0.4", "pyvirtualdisplay==3.0", "pywin32-ctypes==0.2.3", "pyyaml==6.0.2", "pyzbar==0.1.9", "rapidfuzz==3.10.1", "reportlab==4.0.8", "requests==2.32.3", "requests-toolbelt==1.0.0", "rich==13.9.4", "secretstorage==3.3.3", "segno==1.6.1", "semver==3.0.2", "setuptools==75.3.0", "shellingham==1.5.4", "six==1.16.0", "smmap==5.0.1", "snakeviz==2.2.0", "text-unidecode==1.3", "tomli==2.0.2", "tomli-w==1.1.0", "tomlkit==0.13.2", "tornado==6.4.1", "translate-toolkit==3.14.1", "trove-classifiers==2024.10.21.16", "types-python-dateutil==2.9.0.20241003", "typing-extensions==4.12.2", "urllib3==2.2.3", "virtualenv==20.27.1", "wcwidth==0.2.13", "wheel==0.44.0", "zipp==3.20.2"] +requires = ["altgraph==0.17.4", "appdirs==1.4.4", "arrow==1.3.0", "base58==2.1.1", "bdkpython==0.31.0", "binaryornot==0.4.4", "bitcoin-nostr-chat==0.4.1", "bitcoin-qr-tools==0.14.10", "bitcoin-usb==0.5.3", "briefcase==0.3.19", "build==1.2.2.post1", "cachecontrol==0.14.1", "cbor2==5.6.5", "certifi==2024.8.30", "cffi==1.17.1", "cfgv==3.4.0", "chardet==5.2.0", "charset-normalizer==3.4.0", "cleo==2.1.0", "click==8.1.7", "colorama==0.4.6", "cookiecutter==2.6.0", "crashtest==0.4.1", "cryptography==43.0.3", "defusedxml==0.7.1", "distlib==0.3.9", "dmgbuild==1.6.2", "ds-store==1.3.1", "dulwich==0.21.7", "ecdsa==0.19.0", "exceptiongroup==1.2.2", "fastjsonschema==2.20.0", "filelock==3.16.1", "fonttools==4.54.1", "fpdf2==2.8.1", "gitdb==4.0.11", "gitpython==3.1.43", "hidapi==0.14.0.post3", "hwi==3.1.0", "identify==2.6.1", "idna==3.10", "importlib-metadata==8.5.0", "iniconfig==2.0.0", "installer==0.7.0", "jaraco-classes==3.4.0", "jeepney==0.8.0", "jinja2==3.1.4", "keyring==24.3.1", "libusb1==3.1.0", "lxml==5.3.0", "mac-alias==2.2.2", "macholib==1.16.3", "markdown-it-py==3.0.0", "markupsafe==3.0.2", "mdurl==0.1.2", "mnemonic==0.21", "more-itertools==10.5.0", "msgpack==1.1.0", "mss==9.0.2", "nodeenv==1.9.1", "noiseprotocol==0.3.1", "nostr-sdk==0.32.2", "numpy==2.1.3", "opencv-python-headless==4.10.0.84", "packaging==24.2", "pefile==2023.2.7", "pexpect==4.9.0", "pgpy==0.6.0", "pillow==10.4.0", "pip==24.3.1", "pkginfo==1.11.2", "platformdirs==4.3.6", "pluggy==1.5.0", "poetry-core==1.9.1", "poetry-plugin-export==1.8.0", "pre-commit==3.8.0", "protobuf==4.25.5", "psutil==5.9.8", "ptyprocess==0.7.0", "pyaes==1.6.1", "pyasn1==0.6.1", "pycparser==2.22", "pygame==2.6.1", "pygments==2.18.0", "pyinstaller==6.11.0", "pyinstaller-hooks-contrib==2024.9", "pyprof2calltree==1.4.5", "pyproject-hooks==1.2.0", "pyqrcode==1.2.1", "pyqt6==6.7.1", "pyqt6-charts==6.7.0", "pyqt6-charts-qt6==6.7.3", "pyqt6-qt6==6.7.3", "pyqt6-sip==13.8.0", "pyserial==3.5", "pytest==8.3.3", "pytest-qt==4.4.0", "pytest-xvfb==3.0.0", "python-bitcointx==1.1.4", "python-dateutil==2.9.0.post0", "python-gnupg==0.5.3", "python-slugify==8.0.4", "pyvirtualdisplay==3.0", "pywin32-ctypes==0.2.3", "pyyaml==6.0.2", "pyzbar==0.1.9", "rapidfuzz==3.10.1", "reportlab==4.0.8", "requests==2.32.3", "requests-toolbelt==1.0.0", "rich==13.9.4", "secretstorage==3.3.3", "segno==1.6.1", "semver==3.0.2", "setuptools==75.3.0", "shellingham==1.5.4", "six==1.16.0", "smmap==5.0.1", "snakeviz==2.2.0", "text-unidecode==1.3", "tomli==2.0.2", "tomli-w==1.1.0", "tomlkit==0.13.2", "tornado==6.4.1", "translate-toolkit==3.14.1", "trove-classifiers==2024.10.21.16", "types-python-dateutil==2.9.0.20241003", "typing-extensions==4.12.2", "urllib3==2.2.3", "virtualenv==20.27.1", "wcwidth==0.2.13", "wheel==0.44.0", "zipp==3.20.2"] @@ -83,6 +83,7 @@ pyprof2calltree = "^1.4.5" pytest-xvfb = "^3.0.0" tomlkit = "^0.13.2" poetry = "^1.8.4" +pyinstaller = "^6.11.0" [tool.briefcase.app.bitcoin-safe.macOS] universal_build = true @@ -117,7 +118,7 @@ NSCameraUsageDescription = "This application supports scanning QR-codes." manylinux = "manylinux_2_28" icon = "tools/resources/icon" resources = [ "tools/resources/icon/*.png", "tools/resources/icon/*.svg",] -version = "1.0.0b2" +version = "1.0.0b3" # system_requires = [ "cmake", "gcc", "gcc-c++", "make", "perl", "git", "libxcb", "libxcb-devel", "xcb-util", "xcb-util-devel", "mesa-libGL-devel", "openssl-devel", "bison", "flex", "gperf", "sqlite-devel", "libicu-devel"] @@ -127,7 +128,7 @@ flatpak_runtime = "org.kde.Platform" flatpak_runtime_version = "6.6" flatpak_sdk = "org.kde.Sdk" -version = "1.0.0b2" +version = "1.0.0b3" [tool.briefcase.app.bitcoin-safe.linux.system.debian] diff --git a/tools/build-linux/appimage/make_appimage.sh b/tools/build-linux/appimage/run_in_docker.sh similarity index 83% rename from tools/build-linux/appimage/make_appimage.sh rename to tools/build-linux/appimage/run_in_docker.sh index da9f0db..f9e9dd2 100755 --- a/tools/build-linux/appimage/make_appimage.sh +++ b/tools/build-linux/appimage/run_in_docker.sh @@ -5,11 +5,11 @@ set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../../.." CONTRIB="$PROJECT_ROOT/tools" CONTRIB_APPIMAGE="$CONTRIB/build-linux/appimage" -DISTDIR="$PROJECT_ROOT/dist" +DISTDIR="$CONTRIB_APPIMAGE/dist" BUILDDIR="$CONTRIB_APPIMAGE/build/appimage" APPDIR="$BUILDDIR/bitcoin_safe.AppDir" -CACHEDIR="$CONTRIB_APPIMAGE/.cache/appimage" -export DLL_TARGET_DIR="$CACHEDIR/dlls" +BUILD_CACHEDIR="$CONTRIB_APPIMAGE/.cache/appimage" +export DLL_TARGET_DIR="$BUILD_CACHEDIR/dlls" PIP_CACHE_DIR="$CONTRIB_APPIMAGE/.cache/pip_cache" POETRY_WHEEL_DIR="$CONTRIB_APPIMAGE/.cache/poetry_wheel" POETRY_CACHE_DIR="$CONTRIB_APPIMAGE/.cache/poetry_cache" @@ -29,38 +29,39 @@ VERSION=$(git describe --tags --dirty --always) APPIMAGE="$DISTDIR/bitcoin_safe-$VERSION-x86_64.AppImage" rm -rf "$BUILDDIR" -mkdir -p "$APPDIR" "$CACHEDIR" "$PIP_CACHE_DIR" "$DISTDIR" "$DLL_TARGET_DIR" +rm -rf "$POETRY_WHEEL_DIR" # delete whl +mkdir -p "$APPDIR" "$BUILD_CACHEDIR" "$PIP_CACHE_DIR" "$DISTDIR" "$DLL_TARGET_DIR" "$POETRY_WHEEL_DIR" # potential leftover from setuptools that might make pip put garbage in binary rm -rf "$PROJECT_ROOT/build" info "downloading some dependencies." -download_if_not_exist "$CACHEDIR/functions.sh" "https://raw.githubusercontent.com/AppImage/pkg2appimage/$PKG2APPIMAGE_COMMIT/functions.sh" -verify_hash "$CACHEDIR/functions.sh" "8f67711a28635b07ce539a9b083b8c12d5488c00003d6d726c7b134e553220ed" +download_if_not_exist "$BUILD_CACHEDIR/functions.sh" "https://raw.githubusercontent.com/AppImage/pkg2appimage/$PKG2APPIMAGE_COMMIT/functions.sh" +verify_hash "$BUILD_CACHEDIR/functions.sh" "8f67711a28635b07ce539a9b083b8c12d5488c00003d6d726c7b134e553220ed" -download_if_not_exist "$CACHEDIR/appimagetool" "https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage" -verify_hash "$CACHEDIR/appimagetool" "df3baf5ca5facbecfc2f3fa6713c29ab9cefa8fd8c1eac5d283b79cab33e4acb" +download_if_not_exist "$BUILD_CACHEDIR/appimagetool" "https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage" +verify_hash "$BUILD_CACHEDIR/appimagetool" "df3baf5ca5facbecfc2f3fa6713c29ab9cefa8fd8c1eac5d283b79cab33e4acb" -download_if_not_exist "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tar.xz" -verify_hash "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "afb74bf19130e7a47d10312c8f5e784f24e0527981eab68e20546cfb865830b8" +download_if_not_exist "$BUILD_CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tar.xz" +verify_hash "$BUILD_CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "afb74bf19130e7a47d10312c8f5e784f24e0527981eab68e20546cfb865830b8" info "building python." -tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$CACHEDIR" +tar xf "$BUILD_CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$BUILD_CACHEDIR" ( - if [ -f "$CACHEDIR/Python-$PYTHON_VERSION/python" ]; then + if [ -f "$BUILD_CACHEDIR/Python-$PYTHON_VERSION/python" ]; then info "python already built, skipping" exit 0 fi - cd "$CACHEDIR/Python-$PYTHON_VERSION" + cd "$BUILD_CACHEDIR/Python-$PYTHON_VERSION" LC_ALL=C export BUILD_DATE=$(date -u -d "@$SOURCE_DATE_EPOCH" "+%b %d %Y") LC_ALL=C export BUILD_TIME=$(date -u -d "@$SOURCE_DATE_EPOCH" "+%H:%M:%S") # Patch taken from Ubuntu http://archive.ubuntu.com/ubuntu/pool/main/p/python3.10/python3.10_3.10.12-1~22.04.6.debian.tar.xz patch -p1 < "$CONTRIB_APPIMAGE/patches/python-3.10-reproducible-buildinfo.diff" ./configure \ - --cache-file="$CACHEDIR/python.config.cache" \ + --cache-file="$BUILD_CACHEDIR/python.config.cache" \ --prefix="$APPDIR/usr" \ --enable-ipv6 \ --enable-shared \ @@ -69,7 +70,7 @@ tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$CACHEDIR" ) info "installing python." ( - cd "$CACHEDIR/Python-$PYTHON_VERSION" + cd "$BUILD_CACHEDIR/Python-$PYTHON_VERSION" make -s install > /dev/null || fail "Could not install Python" # When building in docker on macOS, python builds with .exe extension because the # case insensitive file system of macOS leaks into docker. This causes the build @@ -148,7 +149,7 @@ cp "$CONTRIB_APPIMAGE/apprun.sh" "$APPDIR/AppRun" info "finalizing AppDir." ( export PKG2AICOMMIT="$PKG2APPIMAGE_COMMIT" - . "$CACHEDIR/functions.sh" + . "$BUILD_CACHEDIR/functions.sh" cd "$APPDIR" # copy system dependencies @@ -236,11 +237,11 @@ find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + info "creating the AppImage." ( cd "$BUILDDIR" - cp "$CACHEDIR/appimagetool" "$CACHEDIR/appimagetool_copy" + cp "$BUILD_CACHEDIR/appimagetool" "$BUILD_CACHEDIR/appimagetool_copy" # zero out "appimage" magic bytes, as on some systems they confuse the linker - sed -i 's|AI\x02|\x00\x00\x00|' "$CACHEDIR/appimagetool_copy" - chmod +x "$CACHEDIR/appimagetool_copy" - "$CACHEDIR/appimagetool_copy" --appimage-extract + sed -i 's|AI\x02|\x00\x00\x00|' "$BUILD_CACHEDIR/appimagetool_copy" + chmod +x "$BUILD_CACHEDIR/appimagetool_copy" + "$BUILD_CACHEDIR/appimagetool_copy" --appimage-extract # We build a small wrapper for mksquashfs that removes the -mkfs-time option # as it conflicts with SOURCE_DATE_EPOCH. mv "$BUILDDIR/squashfs-root/usr/lib/appimagekit/mksquashfs" "$BUILDDIR/squashfs-root/usr/lib/appimagekit/mksquashfs_orig" diff --git a/tools/build-wine/.dockerignore b/tools/build-wine/.dockerignore new file mode 100644 index 0000000..a3e70a0 --- /dev/null +++ b/tools/build-wine/.dockerignore @@ -0,0 +1,5 @@ +tmp/ +build/ +.cache/ +dist/ +signed/ diff --git a/tools/build-wine/Dockerfile b/tools/build-wine/Dockerfile new file mode 100644 index 0000000..3d1cfbe --- /dev/null +++ b/tools/build-wine/Dockerfile @@ -0,0 +1,74 @@ +FROM debian:bookworm@sha256:b37bc259c67238d814516548c17ad912f26c3eed48dd9bb54893eafec8739c89 + +# need ca-certificates before using snapshot packages +RUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \ + ca-certificates + +# pin the distro packages. +COPY apt.sources.list /etc/apt/sources.list +COPY apt.preferences /etc/apt/preferences.d/snapshot + +ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 +ENV DEBIAN_FRONTEND=noninteractive + +RUN dpkg --add-architecture i386 && \ + apt-get update -q && \ + apt-get install -qy --allow-downgrades \ + wget \ + gnupg2 \ + dirmngr \ + python3-software-properties \ + software-properties-common \ + git \ + p7zip-full \ + make \ + mingw-w64 \ + mingw-w64-tools \ + autotools-dev \ + autoconf \ + autopoint \ + libtool \ + gettext \ + sudo \ + nsis \ + libudev-dev \ + && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get autoremove -y && \ + apt-get clean + +RUN DEBIAN_CODENAME=$(lsb_release --codename --short) && \ + WINEVERSION="9.0.0.0~${DEBIAN_CODENAME}-1" && \ + wget -nc https://dl.winehq.org/wine-builds/winehq.key && \ + echo "d965d646defe94b3dfba6d5b4406900ac6c81065428bf9d9303ad7a72ee8d1b8 winehq.key" | sha256sum -c - && \ + cat winehq.key | gpg --dearmor -o /etc/apt/keyrings/winehq.gpg && \ + echo deb [signed-by=/etc/apt/keyrings/winehq.gpg] https://dl.winehq.org/wine-builds/debian/ ${DEBIAN_CODENAME} main >> /etc/apt/sources.list.d/winehq.list && \ + rm winehq.key && \ + apt-get update -q && \ + apt-get install -qy --allow-downgrades \ + wine-stable-amd64:amd64=${WINEVERSION} \ + wine-stable-i386:i386=${WINEVERSION} \ + wine-stable:amd64=${WINEVERSION} \ + winehq-stable:amd64=${WINEVERSION} \ + libvkd3d1:amd64=1.3~${DEBIAN_CODENAME}-1 \ + libvkd3d1:i386=1.3~${DEBIAN_CODENAME}-1 \ + && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get autoremove -y && \ + apt-get clean + +# create new user to avoid using root; but with sudo access and no password for convenience. +ARG UID=1000 +ENV USER="user" +ENV HOME_DIR="/home/${USER}" +ENV WORK_DIR="${HOME_DIR}/wspace" \ + PATH="${HOME_DIR}/.local/bin:${PATH}" +RUN useradd --uid $UID --create-home --shell /bin/bash ${USER} +RUN usermod -append --groups sudo ${USER} +RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers +WORKDIR ${WORK_DIR} +RUN chown --recursive ${USER} ${WORK_DIR} +RUN chown ${USER} /opt +USER ${USER} + +RUN mkdir --parents "/opt/wine64/drive_c/bitcoin_safe" diff --git a/tools/build-wine/apt.preferences b/tools/build-wine/apt.preferences new file mode 100644 index 0000000..d861cd8 --- /dev/null +++ b/tools/build-wine/apt.preferences @@ -0,0 +1,3 @@ +Package: * +Pin: origin "snapshot.debian.org" +Pin-Priority: 1001 diff --git a/tools/build-wine/apt.sources.list b/tools/build-wine/apt.sources.list new file mode 100644 index 0000000..b2128cd --- /dev/null +++ b/tools/build-wine/apt.sources.list @@ -0,0 +1,2 @@ +deb https://snapshot.debian.org/archive/debian/20240419T084725Z/ bookworm main +deb-src https://snapshot.debian.org/archive/debian/20240419T084725Z/ bookworm main \ No newline at end of file diff --git a/tools/build-wine/bitcoin_safe.nsi b/tools/build-wine/bitcoin_safe.nsi new file mode 100644 index 0000000..b68d6ce --- /dev/null +++ b/tools/build-wine/bitcoin_safe.nsi @@ -0,0 +1,173 @@ +;-------------------------------- +;Include Modern UI + !include "TextFunc.nsh" ;Needed for the $GetSize function. I know, doesn't sound logical, it isn't. + !include "MUI2.nsh" + +;-------------------------------- +;Variables + + !define PRODUCT_NAME "Bitcoin Safe" + !define PRODUCT_WEB_SITE "https://github.com/andreasgriffin/bitcoin-safe" + !define PRODUCT_PUBLISHER "Andreas Griffin" + !define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" + +;-------------------------------- +;General + + ;Name and file + Name "${PRODUCT_NAME}" + OutFile "dist/bitcoin_safe-setup.exe" + + ;Default installation folder + InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}" + + ;Get installation folder from registry if available + InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" "" + + ;Request application privileges for Windows Vista + RequestExecutionLevel admin + + ;Specifies whether or not the installer will perform a CRC on itself before allowing an install + CRCCheck on + + ;Sets whether or not the details of the install are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them. + ShowInstDetails show + + ;Sets whether or not the details of the uninstall are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them. + ShowUninstDetails show + + ;Sets the colors to use for the install info screen (the default is 00FF00 000000. Use the form RRGGBB (in hexadecimal, as in HTML, only minus the leading '#', since # can be used for comments). Note that if "/windows" is specified as the only parameter, the default windows colors will be used. + InstallColors /windows + + ;This command sets the compression algorithm used to compress files/data in the installer. (http://nsis.sourceforge.net/Reference/SetCompressor) + SetCompressor /SOLID lzma + + ;Sets the dictionary size in megabytes (MB) used by the LZMA compressor (default is 8 MB). + SetCompressorDictSize 64 + + ;Sets the text that is shown (by default it is 'Nullsoft Install System vX.XX') in the bottom of the install window. Setting this to an empty string ("") uses the default; to set the string to blank, use " " (a space). + BrandingText "${PRODUCT_NAME} Installer v${PRODUCT_VERSION}" + + ;Sets what the titlebars of the installer will display. By default, it is 'Name Setup', where Name is specified with the Name command. You can, however, override it with 'MyApp Installer' or whatever. If you specify an empty string (""), the default will be used (you can however specify " " to achieve a blank string) + Caption "${PRODUCT_NAME}" + + ;Adds the Product Version on top of the Version Tab in the Properties of the file. + VIProductVersion 1.0.0.0 + + ;VIAddVersionKey - Adds a field in the Version Tab of the File Properties. This can either be a field provided by the system or a user defined field. + VIAddVersionKey ProductName "${PRODUCT_NAME} Installer" + VIAddVersionKey Comments "The installer for ${PRODUCT_NAME}" + VIAddVersionKey CompanyName "${PRODUCT_NAME}" + VIAddVersionKey LegalCopyright "2013-2018 ${PRODUCT_PUBLISHER}" + VIAddVersionKey FileDescription "${PRODUCT_NAME} Installer" + VIAddVersionKey FileVersion ${PRODUCT_VERSION} + VIAddVersionKey ProductVersion ${PRODUCT_VERSION} + VIAddVersionKey InternalName "${PRODUCT_NAME} Installer" + VIAddVersionKey LegalTrademarks "${PRODUCT_NAME} is a trademark of ${PRODUCT_PUBLISHER}" + VIAddVersionKey OriginalFilename "${PRODUCT_NAME}.exe" + +;-------------------------------- +;Interface Settings + + !define MUI_ABORTWARNING + !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort the installation of ${PRODUCT_NAME}?" + + !define MUI_ICON "..\..\tools\resources\icon.ico" + +;-------------------------------- +;Pages + + !insertmacro MUI_PAGE_DIRECTORY + !insertmacro MUI_PAGE_INSTFILES + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + +;-------------------------------- +;Languages + + !insertmacro MUI_LANGUAGE "English" + +;-------------------------------- +;Installer Sections + +;Check if we have Administrator rights +Function .onInit + UserInfo::GetAccountType + pop $0 + ${If} $0 != "admin" ;Require admin rights on NT4+ + MessageBox mb_iconstop "Administrator rights required!" + SetErrorLevel 740 ;ERROR_ELEVATION_REQUIRED + Quit + ${EndIf} +FunctionEnd + +Section + SetOutPath $INSTDIR + + ;Uninstall previous version files + RMDir /r "$INSTDIR\*.*" + Delete "$DESKTOP\${PRODUCT_NAME}.lnk" + Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*" + + ;Files to pack into the installer + File /r "dist\bitcoin_safe\*.*" + File "..\..\tools\resources\icon.ico" + + ;Store installation folder + WriteRegStr HKCU "Software\${PRODUCT_NAME}" "" $INSTDIR + + ;Create uninstaller + DetailPrint "Creating uninstaller..." + WriteUninstaller "$INSTDIR\Uninstall.exe" + + ;Create desktop shortcut + DetailPrint "Creating desktop shortcut..." + CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\bitcoin_safe-${PRODUCT_VERSION}.exe" "" + + ;Create start-menu items + DetailPrint "Creating start-menu items..." + CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\bitcoin_safe-${PRODUCT_VERSION}.exe" "" "$INSTDIR\bitcoin_safe-${PRODUCT_VERSION}.exe" 0 + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME} Testnet.lnk" "$INSTDIR\bitcoin_safe-${PRODUCT_VERSION}.exe" "--testnet" "$INSTDIR\bitcoin_safe-${PRODUCT_VERSION}.exe" 0 + + + ;Links bitcoin: URIs to Bitcoin Safe + WriteRegStr HKCU "Software\Classes\bitcoin" "" "URL:bitcoin Protocol" + WriteRegStr HKCU "Software\Classes\bitcoin" "URL Protocol" "" + WriteRegStr HKCU "Software\Classes\bitcoin" "DefaultIcon" "$\"$INSTDIR\icon.ico, 0$\"" + WriteRegStr HKCU "Software\Classes\bitcoin\shell\open\command" "" "$\"$INSTDIR\bitcoin_safe-${PRODUCT_VERSION}.exe$\" $\"%1$\"" + + ;Adds an uninstaller possibility to Windows Uninstall or change a program section + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe" + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}" + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}" + WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\icon.ico" + + ;Fixes Windows broken size estimates + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKCU "${PRODUCT_UNINST_KEY}" "EstimatedSize" "$0" +SectionEnd + +;-------------------------------- +;Descriptions + +;-------------------------------- +;Uninstaller Section + +Section "Uninstall" + RMDir /r "$INSTDIR\*.*" + + RMDir "$INSTDIR" + + Delete "$DESKTOP\${PRODUCT_NAME}.lnk" + Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*" + RMDir "$SMPROGRAMS\${PRODUCT_NAME}" + + DeleteRegKey HKCU "Software\Classes\bitcoin" + DeleteRegKey HKCU "Software\${PRODUCT_NAME}" + DeleteRegKey HKCU "${PRODUCT_UNINST_KEY}" +SectionEnd diff --git a/tools/build-wine/build_exe.sh b/tools/build-wine/build_exe.sh new file mode 100755 index 0000000..cf8a79e --- /dev/null +++ b/tools/build-wine/build_exe.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +NAME_ROOT=bitcoin_safe +WINE_ROOT_PACKAGE="$WINEPREFIX/drive_c/python3/Lib/site-packages/$NAME_ROOT/" +export PYTHONDONTWRITEBYTECODE=1 # don't create __pycache__/ folders with .pyc files + + +# Let's begin! +set -e + +. "$CONTRIB"/build_tools_util.sh + +pushd "$WINEPREFIX/drive_c/$NAME_ROOT" + +VERSION=$(git describe --tags --dirty --always) +info "Last commit: $VERSION" + +find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + +popd + + +# opt out of compiling C extensions +export AIOHTTP_NO_EXTENSIONS=1 +export YARL_NO_EXTENSIONS=1 +export MULTIDICT_NO_EXTENSIONS=1 +export FROZENLIST_NO_EXTENSIONS=1 +export ELECTRUM_ECC_DONT_COMPILE=1 + +POETRY_WHEEL_DIR="$BUILD_CACHEDIR/poetry_wheel" +WINE_POETRY_WHEEL_DIR=$(win_path "$POETRY_WHEEL_DIR") + +APPDIR="$WINEPREFIX/drive_c/$NAME_ROOT" +WINE_APPDIR=$(win_path "$APPDIR") +L_POETRY_CACHE_DIR="$BUILD_CACHEDIR/poetry" # needs the L_, because later I need to do export POETRY_CACHE_DIR=WINE_POETRY_CACHE_DIR +WINE_POETRY_CACHE_DIR=$(win_path "$L_POETRY_CACHE_DIR") +PIP_CACHE_DIR="$BUILD_CACHEDIR/pip" +WINE_PIP_CACHE_DIR=$(win_path "$PIP_CACHE_DIR") + + +rm -rf "$POETRY_WHEEL_DIR" # delete whl +mkdir -p "$POETRY_WHEEL_DIR" "$APPDIR" "$PIP_CACHE_DIR" "$L_POETRY_CACHE_DIR" + +info "Installing requirements..." + +info "Installing poetry" +$WINE_PYTHON -m pip install --no-build-isolation --no-warn-script-location \ + --cache-dir "$WINE_PIP_CACHE_DIR" poetry==1.8.4 + + +info "Installing build dependencies using poetry" +# ln -s $WINE_PYHOME/python.exe "/usr/bin/python.exe" +# export PATH="$APPDIR/usr/bin:$PATH" +# for poetry to install into the system python environment +# we have to also remove the .venv folder. Otherwise it will use it +export POETRY_VIRTUALENVS_CREATE=false +export POETRY_CACHE_DIR="$WINE_POETRY_CACHE_DIR" +mkdir -p "$PROJECT_ROOT/.venv" +rm -rf "$PROJECT_ROOT/.original.venv" # delete old moved directory if exists +mv "$PROJECT_ROOT/.venv" "$PROJECT_ROOT/.original.venv" # moving this out of the may so poetry doesnt detect it +$WINE_PYTHON -m poetry install --only main --no-interaction + +info "now install the root package" +$WINE_PYTHON -m poetry build -f wheel --output="$WINE_POETRY_WHEEL_DIR" +info "ls of output directory: {$POETRY_WHEEL_DIR} $(ls $POETRY_WHEEL_DIR)" +$WINE_PYTHON -m pip install --no-dependencies --no-warn-script-location \ + --cache-dir "$WINE_PIP_CACHE_DIR" "$POETRY_WHEEL_DIR/"*.whl + + +# # was only needed during build time, not runtime +$WINE_PYTHON -m pip uninstall -y poetry pip + + +mv "$PROJECT_ROOT/.original.venv" "$PROJECT_ROOT/.venv" # moving the .venv back + + + + +cp "$PROJECT_ROOT/bitcoin_safe/"libsecp256k1-*.dll "$WINE_ROOT_PACKAGE" + + + +rm -rf dist/ + +# build standalone and portable versions +info "Running pyinstaller..." +bitcoin_safe_CMDLINE_NAME="$NAME_ROOT-$VERSION" wine "$WINE_PYHOME/scripts/pyinstaller.exe" --noconfirm --clean deterministic.spec + +# set timestamps in dist, in order to make the installer reproducible +pushd dist +find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + +popd + +info "building NSIS installer" +# $VERSION could be passed to the bitcoin_safe.nsi script, but this would require some rewriting in the script itself. + +makensis -DPRODUCT_VERSION=$VERSION bitcoin_safe.nsi + +cd dist +mv bitcoin_safe-setup.exe $NAME_ROOT-$VERSION-setup.exe +cd .. + +info "Padding binaries to 8-byte boundaries, and fixing COFF image checksum in PE header" +# note: 8-byte boundary padding is what osslsigncode uses: +# https://github.com/mtrojnar/osslsigncode/blob/6c8ec4427a0f27c145973450def818e35d4436f6/osslsigncode.c#L3047 +( + cd dist + for binary_file in ./*.exe; do + info ">> fixing $binary_file..." + # code based on https://github.com/erocarrera/pefile/blob/bbf28920a71248ed5c656c81e119779c131d9bd4/pefile.py#L5877 + python3 <> 32) + if checksum > 2 ** 32: + checksum = (checksum & 0xffffffff) + (checksum >> 32) + +checksum = (checksum & 0xffff) + (checksum >> 16) +checksum = (checksum) + (checksum >> 16) +checksum = checksum & 0xffff +checksum += len(binary) + +# Set the checksum +binary[checksum_offset : checksum_offset + 4] = int.to_bytes(checksum, byteorder="little", length=4) + +with open(pe_file, "wb") as f: + f.write(binary) +EOF + done +) + +sha256sum dist/bitcoin_safe*.exe diff --git a/tools/build-wine/deterministic.spec b/tools/build-wine/deterministic.spec new file mode 100644 index 0000000..5f26d55 --- /dev/null +++ b/tools/build-wine/deterministic.spec @@ -0,0 +1,170 @@ +# -*- mode: python -*- + +from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs +from PyInstaller.building.api import COLLECT, EXE, PYZ +from PyInstaller.building.build_main import Analysis + +import sys, os + +PYPKG="bitcoin_safe" +PROJECT_ROOT = "C:/bitcoin_safe" +ICONS_FILE=f"{PROJECT_ROOT}/tools/resources/icon.ico" + +cmdline_name = os.environ.get("bitcoin_safe_CMDLINE_NAME") +if not cmdline_name: + raise Exception('no name') + +# see https://github.com/pyinstaller/pyinstaller/issues/2005 +hiddenimports = [] +hiddenimports += collect_submodules('pkg_resources') # workaround for https://github.com/pypa/setuptools/issues/1963 + + +binaries = [] +# Workaround for "Retro Look": +binaries += [b for b in collect_dynamic_libs('PyQt6') if 'qwindowsvista' in b[0]] +binaries += collect_dynamic_libs('bdkpython') +binaries += collect_dynamic_libs('nostr_sdk') +binaries += collect_dynamic_libs('pyzbar') +binaries += collect_dynamic_libs('electrumsv_secp256k1') +# add libsecp256k1, libusb, etc: +binaries += [(f"{PROJECT_ROOT}/{PYPKG}/*.dll", '.')] + + +datas = [ + (f"{PROJECT_ROOT}/{PYPKG}/gui/icons/*", f"{PYPKG}/gui/icons"), + (f"{PROJECT_ROOT}/{PYPKG}/gui/screenshots/*", f"{PYPKG}/gui/screenshots"), + (f"{PROJECT_ROOT}/{PYPKG}/gui/locales/*", f"{PYPKG}/gui/locales"), + # (f"{PROJECT_ROOT}/{PYPKG}/lnwire/*.csv", f"{PYPKG}/lnwire"), + # (f"{PROJECT_ROOT}/{PYPKG}/wordlist/english.txt", f"{PYPKG}/wordlist"), + # (f"{PROJECT_ROOT}/{PYPKG}/wordlist/slip39.txt", f"{PYPKG}/wordlist"), + # (f"{PROJECT_ROOT}/{PYPKG}/locale", f"{PYPKG}/locale"), + # (f"{PROJECT_ROOT}/{PYPKG}/plugins", f"{PYPKG}/plugins"), + # (f"{PROJECT_ROOT}/{PYPKG}/gui/icons", f"{PYPKG}/gui/icons"), +] + +# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports +a = Analysis([f"../../{PYPKG}/__main__.py" ], + pathex=[f"{PROJECT_ROOT}/{PYPKG}"], + binaries=binaries, + datas=datas, + hiddenimports=hiddenimports, + hookspath=[]) + + +# http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal +for d in a.datas: + if 'pyconfig' in d[0]: + a.datas.remove(d) + break + +# Strip out parts of Qt that we never use. Reduces binary size by tens of MBs. see #4815 +qt_bins2remove=( + r'pyqt6\qt6\qml', + r'pyqt6\qt6\bin\qt6quick', + r'pyqt6\qt6\bin\qt6qml', + r'pyqt6\qt6\bin\qt6multimediaquick', + r'pyqt6\qt6\bin\qt6pdfquick', + r'pyqt6\qt6\bin\qt6positioning', + r'pyqt6\qt6\bin\qt6spatialaudio', + r'pyqt6\qt6\bin\qt6shadertools', + r'pyqt6\qt6\bin\qt6sensors', + r'pyqt6\qt6\bin\qt6web', + r'pyqt6\qt6\bin\qt6test', +) +print("Removing Qt binaries:", *qt_bins2remove) +for x in a.binaries.copy(): + for r in qt_bins2remove: + if x[0].lower().startswith(r): + a.binaries.remove(x) + print('----> Removed x =', x) + +qt_data2remove=( + r'pyqt6\qt6\translations\qtwebengine_locales', + r'pyqt6\qt6\qml', +) +print("Removing Qt datas:", *qt_data2remove) +for x in a.datas.copy(): + for r in qt_data2remove: + if x[0].lower().startswith(r): + a.datas.remove(x) + print('----> Removed x =', x) + +# not reproducible (see #7739): +print("Removing *.dist-info/ from datas:") +for x in a.datas.copy(): + if ".dist-info\\" in x[0].lower(): + a.datas.remove(x) + print('----> Removed x =', x) + + +# hotfix for #3171 (pre-Win10 binaries) +a.binaries = [x for x in a.binaries if not x[1].lower().startswith(r'c:\windows')] + +pyz = PYZ(a.pure) + + +##### +# "standalone" exe with all dependencies packed into it + +# exe_standalone = EXE( +# pyz, +# a.scripts, +# a.binaries, +# a.datas, +# name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}.exe"), +# debug=False, +# strip=False, +# upx=False, +# icon=ICONS_FILE, +# console=False) + # console=True makes an annoying black box pop up, but it does make bitcoin_safe output command line commands, with this turned off no output will be given but commands can still be used + +exe_portable = EXE( + pyz, + a.scripts, + a.binaries, + a.datas + [('is_portable', '../../README.md', 'DATA')], + name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}-portable.exe"), + debug=False, + strip=False, + upx=False, + icon=ICONS_FILE, + console=False) + +##### +# exe and separate files that NSIS uses to build installer "setup" exe + +exe_inside_setup_noconsole = EXE( + pyz, + a.scripts, + exclude_binaries=True, + name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}.exe"), + debug=False, + strip=False, + upx=False, + icon=ICONS_FILE, + console=False) + +exe_inside_setup_console = EXE( + pyz, + a.scripts, + exclude_binaries=True, + name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}-debug.exe"), + debug=False, + strip=False, + upx=False, + icon=ICONS_FILE, + console=True) + +coll = COLLECT( + exe_inside_setup_noconsole, + exe_inside_setup_console, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + debug=False, + icon=ICONS_FILE, + console=False, + name=os.path.join('dist', PYPKG)) diff --git a/tools/build-wine/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc b/tools/build-wine/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc new file mode 100644 index 0000000..a87dbe1 --- /dev/null +++ b/tools/build-wine/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc @@ -0,0 +1,108 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: User-ID: Steve Dower (Python Release Signing) +Comment: Created: 2015-04-06 02:32 +Comment: Type: 4096-bit RSA +Comment: Usage: Signing, Encryption, Certifying User-IDs +Comment: Fingerprint: 7ED10B6531D7C8E1BC296021FC624643487034E5 + + +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 +=fjOq +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tools/build-wine/prepare-wine.sh b/tools/build-wine/prepare-wine.sh new file mode 100755 index 0000000..05dc391 --- /dev/null +++ b/tools/build-wine/prepare-wine.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +PYINSTALLER_REPO="https://github.com/pyinstaller/pyinstaller.git" +PYINSTALLER_COMMIT="5d7a0449ecea400eccbbb30d5fcef27d72f8f75d" +# ^ tag "v6.6.0" + +PYTHON_VERSION=3.10.4 +WINE_PIP_CACHE_DIR=$(win_path "$PIP_CACHE_DIR") + + +# Let's begin! +set -e + +here="$(dirname "$(readlink -e "$0")")" + +. "$CONTRIB"/build_tools_util.sh + +info "Booting wine." +wine 'wineboot' + + +cd "$BUILD_CACHEDIR" +mkdir -p $WINEPREFIX/drive_c/tmp + +info "Installing Python." +# note: you might need "sudo apt-get install dirmngr" for the following +# keys from https://www.python.org/downloads/#pubkeys +KEYRING_PYTHON_DEV="keyring-bitcoin_safe-build-python-dev.gpg" +gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --import "$here"/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc +if [ "$WIN_ARCH" = "win32" ] ; then + PYARCH="win32" +elif [ "$WIN_ARCH" = "win64" ] ; then + PYARCH="amd64" +else + fail "unexpected WIN_ARCH: $WIN_ARCH" +fi +PYTHON_DOWNLOADS="$BUILD_CACHEDIR/python$PYTHON_VERSION" +mkdir -p "$PYTHON_DOWNLOADS" +for msifile in core dev exe lib pip tools; do + echo "Installing $msifile..." + download_if_not_exist "$PYTHON_DOWNLOADS/${msifile}.msi" "https://www.python.org/ftp/python/$PYTHON_VERSION/$PYARCH/${msifile}.msi" + download_if_not_exist "$PYTHON_DOWNLOADS/${msifile}.msi.asc" "https://www.python.org/ftp/python/$PYTHON_VERSION/$PYARCH/${msifile}.msi.asc" + verify_signature "$PYTHON_DOWNLOADS/${msifile}.msi.asc" $KEYRING_PYTHON_DEV || fail "invalid sig for ${msifile}.msi" + wine msiexec /i "$PYTHON_DOWNLOADS/${msifile}.msi" /qb TARGETDIR=$WINE_PYHOME || fail "wine msiexec failed for ${msifile}.msi" +done + +break_legacy_easy_install + +info "Installing build dependencies." +$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ + --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-build-base.txt +$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \ + --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-build-wine.txt + + +# copy already built DLLs +cp "$DLL_TARGET_DIR"/libsecp256k1-*.dll $WINEPREFIX/drive_c/bitcoin_safe/bitcoin_safe/ || fail "Could not copy libsecp to its destination" +cp "$DLL_TARGET_DIR/libzbar-0.dll" $WINEPREFIX/drive_c/bitcoin_safe/bitcoin_safe/ || fail "Could not copy libzbar to its destination" +cp "$DLL_TARGET_DIR/libusb-1.0.dll" $WINEPREFIX/drive_c/bitcoin_safe/bitcoin_safe/ || fail "Could not copy libusb to its destination" + + +info "Building PyInstaller." +# we build our own PyInstaller boot loader as the default one has high +# anti-virus false positives +( + if [ "$WIN_ARCH" = "win32" ] ; then + PYINST_ARCH="32bit" + elif [ "$WIN_ARCH" = "win64" ] ; then + PYINST_ARCH="64bit" + else + fail "unexpected WIN_ARCH: $WIN_ARCH" + fi + if [ -f "$BUILD_CACHEDIR/pyinstaller/PyInstaller/bootloader/Windows-$PYINST_ARCH-intel/runw.exe" ]; then + info "pyinstaller already built, skipping" + exit 0 + fi + cd "$WINEPREFIX/drive_c/bitcoin_safe" + ELECTRUM_COMMIT_HASH=$(git rev-parse HEAD) + cd "$BUILD_CACHEDIR" + rm -rf pyinstaller + mkdir pyinstaller + cd pyinstaller + # Shallow clone + git init + git remote add origin $PYINSTALLER_REPO + git fetch --depth 1 origin $PYINSTALLER_COMMIT + git checkout -b pinned "${PYINSTALLER_COMMIT}^{commit}" + rm -fv PyInstaller/bootloader/Windows-*/run*.exe || true + # add reproducible randomness. this ensures we build a different bootloader for each commit. + # if we built the same one for all releases, that might also get anti-virus false positives + echo "const char *bitcoin_safe_tag = \"tagged by Bitcoin_Safe@$ELECTRUM_COMMIT_HASH\";" >> ./bootloader/src/pyi_main.c + pushd bootloader + # cross-compile to Windows using host python + python3 ./waf all CC="${GCC_TRIPLET_HOST}-gcc" \ + CFLAGS="-static" + popd + # sanity check bootloader is there: + [[ -e "PyInstaller/bootloader/Windows-$PYINST_ARCH-intel/runw.exe" ]] || fail "Could not find runw.exe in target dir!" +) || fail "PyInstaller build failed" +info "Installing PyInstaller." +$WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location ./pyinstaller + +info "Wine is configured." diff --git a/tools/build-wine/run_in_docker.sh b/tools/build-wine/run_in_docker.sh new file mode 100755 index 0000000..6c5f666 --- /dev/null +++ b/tools/build-wine/run_in_docker.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +set -e + +here="$(dirname "$(readlink -e "$0")")" +test -n "$here" -a -d "$here" || exit +# here = /opt/wine64/drive_c/bitcoin_safe/tools/build-wine + + +if [ -z "$WIN_ARCH" ] ; then + export WIN_ARCH="win64" # default +fi +if [ "$WIN_ARCH" = "win32" ] ; then + export GCC_TRIPLET_HOST="i686-w64-mingw32" +elif [ "$WIN_ARCH" = "win64" ] ; then + export GCC_TRIPLET_HOST="x86_64-w64-mingw32" +else + echo "unexpected WIN_ARCH: $WIN_ARCH" + exit 1 +fi + + +export CONTRIB="$here/.." +export PROJECT_ROOT="$CONTRIB/.." + +export BUILD_TYPE="wine" +export GCC_TRIPLET_BUILD="x86_64-pc-linux-gnu" +export GCC_STRIP_BINARIES="1" + + +export BUILD_CACHEDIR="$here/.cache/$WIN_ARCH" +export PIP_CACHE_DIR="$here/.cache/$WIN_ARCH/pip" +export DLL_TARGET_DIR="$BUILD_CACHEDIR/dlls" + +export WINEPREFIX="/opt/wine64" +export WINEDEBUG=-all +export WINE_PYHOME="c:/python3" +export WINE_PYTHON="wine $WINE_PYHOME/python.exe -B" + +. "$CONTRIB"/build_tools_util.sh + +git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." + +info "Clearing $here/build and $here/dist..." +rm "$here"/build/* -rf +rm "$here"/dist/* -rf + +mkdir -p "$BUILD_CACHEDIR" "$DLL_TARGET_DIR" "$PIP_CACHE_DIR" + + + + +################# +#### build libs +################# +if [ -f "$DLL_TARGET_DIR/libsecp256k1-2.dll" ]; then + info "libsecp256k1 already built, skipping" +else + "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" +fi + +if [ -f "$DLL_TARGET_DIR/libzbar-0.dll" ]; then + info "libzbar already built, skipping" +else + ( + # As debian bullseye doesn't provide win-iconv-mingw-w64-dev, we need to build it: + WIN_ICONV_COMMIT="9f98392dfecadffd62572e73e9aba878e03496c4" + # ^ tag "v0.0.8" + info "Building win-iconv..." + cd "$BUILD_CACHEDIR" + if [ ! -d win-iconv ]; then + git clone https://github.com/win-iconv/win-iconv.git + fi + cd win-iconv + if ! $(git cat-file -e ${WIN_ICONV_COMMIT}) ; then + info "Could not find requested version $WIN_ICONV_COMMIT in local clone; fetching..." + git fetch --all + fi + git reset --hard + git clean -dfxq + git checkout "${WIN_ICONV_COMMIT}^{commit}" + + # note: "-j1" as parallel jobs lead to non-reproducibility seemingly due to ordering issues + # see https://github.com/win-iconv/win-iconv/issues/42 + CC="${GCC_TRIPLET_HOST}-gcc" make -j1 || fail "Could not build win-iconv" + # FIXME avoid using sudo + sudo make install prefix="/usr/${GCC_TRIPLET_HOST}" || fail "Could not install win-iconv" + ) + "$CONTRIB"/make_zbar.sh || fail "Could not build zbar" +fi + +if [ -f "$DLL_TARGET_DIR/libusb-1.0.dll" ]; then + info "libusb already built, skipping" +else + "$CONTRIB"/make_libusb.sh || fail "Could not build libusb" +fi + +"$here/prepare-wine.sh" || fail "prepare-wine failed" + +info "Resetting modification time in C:\Python..." +# (Because of some bugs in pyinstaller) +pushd /opt/wine64/drive_c/python* +find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + +popd +ls -l /opt/wine64/drive_c/python* + +"$here/build_exe.sh" || fail "build_exe failed" + +info "Done." diff --git a/tools/build.py b/tools/build.py index f32bae0..726746d 100644 --- a/tools/build.py +++ b/tools/build.py @@ -51,6 +51,9 @@ logging.basicConfig(level=logging.DEBUG) +TARGET_LITERAL = Literal["windows", "mac", "appimage", "deb", "flatpak"] + + class Builder: build_dir = "build" @@ -82,11 +85,6 @@ def update_briefcase_requires( self, pyproject_path="pyproject.toml", poetry_lock_path="poetry.lock", - # electrumsv-secp256k1 offers libsecp256k1 prebuild for different platforms - # which is needed for bitcointx. - # bitcointx and with it the prebuild libsecp256k1 is not used for anything security critical - # key derivation with bitcointx is restricted to testnet/regtest/signet - # and the PSBTTools using bitcointx is safe because it handles no key material additional_requires=[], ): @@ -128,33 +126,59 @@ def update_briefcase_requires( tomlkit.dump(pyproject_data, file) def build_appimage_docker( - self, docker_no_cache=False, build_commit: None | str | Literal["current_commit"] = "current_commit" + self, no_cache=False, build_commit: None | str | Literal["current_commit"] = "current_commit" + ): + self.build_in_docker( + "bitcoin_safe-appimage-builder-img", + Path("tools/build-linux/appimage"), + no_cache=no_cache, + build_commit=build_commit, + ) + + def build_windows_exe_and_installer_docker( + self, no_cache=False, build_commit: None | str | Literal["current_commit"] = "current_commit" + ): + self.build_in_docker( + "bitcoin_safe-wine-builder-img", + Path("tools/build-wine"), + no_cache=no_cache, + build_commit=build_commit, + ) + + def build_in_docker( + self, + docker_image: str, + build_folder: Path, + no_cache=False, + build_commit: None | str | Literal["current_commit"] = "current_commit", ): """_summary_ Args: + docker_image (str): Example: "bitcoin_safe-wine-builder-img" + build_folder (Path): Example: Path("tools/build-wine"), or Path("tools/build-linux/appimage") no_cache (bool, optional): _description_. Defaults to False. build_commit (None | str | Literal['current_commit'], optional): _description_. Defaults to 'current_commit'. 'current_commit' = which means it will build the current HEAD. None = uses the cwd commit_hash = clones this commit hash into /tmp """ - PROJECT_ROOT = Path(".").resolve() + PROJECT_ROOT = Path(".").resolve().absolute() PROJECT_ROOT_OR_FRESHCLONE_ROOT = PROJECT_ROOT - CONTRIB_APPIMAGE = PROJECT_ROOT / "tools" / "build-linux" / "appimage" + path_build = PROJECT_ROOT / build_folder DISTDIR = PROJECT_ROOT / "dist" BUILD_UID = PROJECT_ROOT.stat().st_uid - CACHEDIR = CONTRIB_APPIMAGE / ".cache" / "appimage" - # Note: Sourcing 'build_tools_util.sh' is omitted; ensure equivalent functions are defined if needed. + BUILD_CACHEDIR = path_build / ".cache" + original_dir = os.getcwd() # Initialize DOCKER_BUILD_FLAGS DOCKER_BUILD_FLAGS = "" - if docker_no_cache: + if no_cache: logger.info("BITCOINSAFE_DOCKER_NOCACHE is set. Forcing rebuild of docker image.") DOCKER_BUILD_FLAGS = "--pull --no-cache" - logger.info(f"BITCOINSAFE_DOCKER_NOCACHE is set. Deleting {CACHEDIR}") - run_local(f'rm -rf "{CACHEDIR}"') + logger.info(f"BITCOINSAFE_DOCKER_NOCACHE is set. Deleting {BUILD_CACHEDIR}") + run_local(f'rm -rf "{BUILD_CACHEDIR}"') if build_commit == "current_commit": # Get the current git HEAD commit @@ -166,17 +190,16 @@ def build_appimage_docker( if not build_commit: # Local development build DOCKER_BUILD_FLAGS += f" --build-arg UID={BUILD_UID}" + logger.info(f"Building within current project") logger.info("Building docker image.") - run_local( - f'docker build {DOCKER_BUILD_FLAGS} -t bitcoin_safe-appimage-builder-img "{CONTRIB_APPIMAGE}"' - ) + run_local(f'docker build {DOCKER_BUILD_FLAGS} -t {docker_image} "{path_build}"') # Possibly do a fresh clone FRESH_CLONE = False if build_commit: logger.info(f"BITCOINSAFE_BUILD_COMMIT={build_commit}. Doing fresh clone and git checkout.") - FRESH_CLONE = Path("/tmp/bitcoin_safe_build/appimage/bitcoin_safe_clone/bitcoin_safe") + FRESH_CLONE = Path(f"/tmp/{docker_image.replace(' ','')}/fresh_clone/bitcoin_safe") try: run_local(f'rm -rf "{FRESH_CLONE}"') except subprocess.CalledProcessError: @@ -186,10 +209,13 @@ def build_appimage_docker( run_local(f'git clone "{PROJECT_ROOT}" "{FRESH_CLONE}"') os.chdir(str(FRESH_CLONE)) run_local(f'git checkout "{build_commit}"') + os.chdir(original_dir) PROJECT_ROOT_OR_FRESHCLONE_ROOT = FRESH_CLONE else: logger.info("Not doing fresh clone.") + Source_Dist_dir = PROJECT_ROOT_OR_FRESHCLONE_ROOT / build_folder / "dist" + logger.info("Building binary...") # Check UID and possibly chown if build_commit: @@ -199,22 +225,30 @@ def build_appimage_docker( run_local( f"docker run -it " - f"--name bitcoin_safe-appimage-builder-cont " - f'-v "{PROJECT_ROOT_OR_FRESHCLONE_ROOT}":/opt/bitcoin_safe ' + f"--name {docker_image}-container " + f'-v "{PROJECT_ROOT_OR_FRESHCLONE_ROOT}":/opt/wine64/drive_c/bitcoin_safe ' f"--rm " - f"--workdir /opt/bitcoin_safe/tools/build-linux/appimage " - f"bitcoin_safe-appimage-builder-img " - f"./make_appimage.sh" + f"--workdir /opt/wine64/drive_c/bitcoin_safe/{build_folder} " + f"{docker_image} " + f"./run_in_docker.sh" ) # Ensure the resulting binary location is independent of fresh_clone - if FRESH_CLONE: + if Source_Dist_dir != DISTDIR: os.makedirs(DISTDIR, exist_ok=True) - shutil.copytree(FRESH_CLONE / "dist", DISTDIR, dirs_exist_ok=True) + for file in Source_Dist_dir.iterdir(): + if not file.is_file(): + continue + logger.info(f"Moving {file} --> {DISTDIR / file.name}") + shutil.move( + file, + DISTDIR + / (file.name.replace(self.module_name, self.app_name_formatter(self.module_name))), + ) def briefcase_appimage(self, **kwargs): # briefcase appimage building works on some systems, but not on others... unknown why. - # so we build using the electrum docker by default + # so we build using the bitcoin_safe docker by default run_local("poetry run briefcase -u package linux appimage") def briefcase_windows(self, **kwargs): @@ -235,14 +269,14 @@ def briefcase_flatpak(self, **kwargs): def package_application( self, - targets: List[Literal["windows", "mac", "appimage", "deb", "flatpak"]], - build_commit: None | str | Literal["current_commit"] = "current_commit", + targets: List[TARGET_LITERAL], + build_commit: None | str | Literal["current_commit"] = None, ): self.update_briefcase_requires() f_map = { "appimage": self.build_appimage_docker, - "windows": self.briefcase_windows, + "windows": self.build_windows_exe_and_installer_docker, "mac": self.briefcase_mac, "deb": self.briefcase_deb, "flatpak": self.briefcase_flatpak, @@ -274,6 +308,9 @@ def sign(self): signed_files = manager.sign_files(KnownGPGKeys.andreasgriffin) assert self.verify(signed_files), "Error: Signature Verification failed!!!!" + def lock(self): + run_local("poetry lock --no-cache --no-update") + def verify(self, signed_files: List[Path]): manager = SignatureVerifyer( list_of_known_keys=[KnownGPGKeys.andreasgriffin], @@ -323,7 +360,7 @@ def build_snap(self, **kwargs): Version=1.0 Type=Application Name={app_name} -Exec={app_name} +Exec={app_name} %F Icon={app_name}/gui/icons/logo.svg Comment={app_name} application Terminal=false @@ -374,6 +411,7 @@ def build_snap(self, **kwargs): """ ) + original_dir = os.getcwd() # Check if Snapcraft is installed os.chdir(build_snap_dir) run_local("snapcraft") @@ -391,10 +429,10 @@ def build_snap(self, **kwargs): raise RuntimeError("Snap package build was successful, but the .snap file was not found.") # Return to the original directory - os.chdir(Path.cwd().parent.parent.parent) + os.chdir(original_dir) -def get_default_targets() -> List[str]: +def get_default_targets() -> List[TARGET_LITERAL]: if platform.system() == "Windows": return ["windows"] elif platform.system() == "Linux": @@ -412,10 +450,17 @@ def get_default_targets() -> List[str]: parser = argparse.ArgumentParser(description="Package the Python application.") parser.add_argument("--clean", action="store_true", help=f"Removes the {Builder.build_dir} folder") + parser.add_argument( + "--commit", + type=str, + help=f"The commit to be build. tag|commit_hash|'None'|'current_commit' . The default is 'current_commit'. None, will build within the current project.", + default="current_commit", + ) parser.add_argument( "--targets", nargs="*", help=f"The target formats. The default is {get_default_targets()}", + default=None, ) parser.add_argument("--sign", action="store_true", help="Signs all files in dist") parser.add_argument("--verify", action="store_true", help="Signs all files in dist") @@ -423,28 +468,40 @@ def get_default_targets() -> List[str]: "--update_translations", action="store_true", help="Updates the translation locales files" ) parser.add_argument("--csv_to_ts", action="store_true", help="Overwrites the ts files with csv as source") + parser.add_argument( + "--lock", + action="store_true", + help="poetry lock --no-update --no-cache. This is important to ensure all hashes are included in the lockfile. ", + ) args = parser.parse_args() - # clean args - targets = args.targets - # clean targets - if targets is None: - print("No --targets argument was given.") - targets = [] - elif not targets: - print("--targets was given without any values.") - targets = get_default_targets() - else: - print(f"--targets was given with the values: {args.targets}") - targets = [t.replace(",", "") for t in targets] - - builder = Builder(module_name="bitcoin_safe", clean_all=args.clean) - builder.package_application(targets=targets) - - translation_handler = TranslationHandler(module_name="bitcoin_safe") + + if args.lock: + builder = Builder(module_name="bitcoin_safe", clean_all=args.clean) + builder.lock() + + if args.commit == "None": + args.commit = None + + if args.targets is not None: + # clean args + targets: List[TARGET_LITERAL] = args.targets + if not targets: + print("--targets was given without any values.") + targets = get_default_targets() + else: + print(f"--targets was given with the values: {args.targets}") + targets = [t.replace(",", "") for t in targets] + + builder = Builder(module_name="bitcoin_safe", clean_all=args.clean) + builder.package_application(targets=targets, build_commit=args.commit) + if args.update_translations: + translation_handler = TranslationHandler(module_name="bitcoin_safe") translation_handler.update_translations_from_py() if args.csv_to_ts: + translation_handler = TranslationHandler(module_name="bitcoin_safe") translation_handler.csv_to_ts() if args.sign: + builder = Builder(module_name="bitcoin_safe", clean_all=args.clean) builder.sign() diff --git a/tools/build_tools_util.sh b/tools/build_tools_util.sh index cc2a0cb..c7475d8 100755 --- a/tools/build_tools_util.sh +++ b/tools/build_tools_util.sh @@ -160,3 +160,53 @@ find_links = '' EOF } + + + + +function replace_once() { + local text="$1" + local search_str="$2" + local replace_str="$3" + + # Check if the search string is at the beginning of the text + if [[ "$text" == "$search_str"* ]]; then + # Remove the search string and check the remainder + local prefix=${text#$search_str} + + # If the prefix still starts with the search string or is the same as before removing, + # it means 'search_str' appears more than once at the start or not at all after the first + if [[ "$prefix" == "$text" ]] || [[ "$prefix" == "$search_str"* ]]; then + echo "Error: '$search_str' does not appear exactly once at the left side of '$text'" >&2 + exit 1 + else + # Replace the first occurrence of search_str with replace_str + local new_text="${text/#$search_str/$replace_str}" + echo "$new_text" + fi + else + echo "Error: '$search_str' is not at the left side of '$text'" >&2 + exit 1 + fi +} + +# replaces /opt/wine64/drive_c/ --> c:/ +function win_path() { + local text="$1" + + + + here="$(dirname "$(readlink -e "$0")")" + test -n "$here" -a -d "$here" || exit + # here = /opt/wine64/drive_c/bitcoin_safe/tools/build-wine + CONTRIB="$here/.." + PROJECT_ROOT="$CONTRIB/.." + + # Correctly capturing the output of realpath into a variable + local search_path=$(realpath "$PROJECT_ROOT/..") + + # Assuming replace_once function exists and is used to replace the first occurrence + # This will echo the result after replacing the first occurrence of search_path in text with "c:" + echo $(replace_once "$text" "$search_path" "c:") +} + diff --git a/tools/deterministic-build/requirements-build-base.txt b/tools/deterministic-build/requirements-build-base.txt new file mode 100644 index 0000000..20c1d92 --- /dev/null +++ b/tools/deterministic-build/requirements-build-base.txt @@ -0,0 +1,27 @@ +flit-core==3.9.0 \ + --hash=sha256:72ad266176c4a3fcfab5f2930d76896059851240570ce9a98733b658cb786eba \ + --hash=sha256:7aada352fb0c7f5538c4fafeddf314d3a6a92ee8e2b1de70482329e42de70301 +packaging==23.2 \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ + --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 +pip==22.3.1 \ + --hash=sha256:65fd48317359f3af8e593943e6ae1506b66325085ea64b706a998c6e83eeaf38 \ + --hash=sha256:908c78e6bc29b676ede1c4d57981d490cb892eb45cd8c214ab6298125119e077 +poetry-core==1.9.0 \ + --hash=sha256:4e0c9c6ad8cf89956f03b308736d84ea6ddb44089d16f2adc94050108ec1f5a1 \ + --hash=sha256:fa7a4001eae8aa572ee84f35feb510b321bd652e5cf9293249d62853e1f935a2 +setuptools==65.5.1 \ + --hash=sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31 \ + --hash=sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f +setuptools-scm==8.0.4 \ + --hash=sha256:b47844cd2a84b83b3187a5782c71128c28b4c94cad8bfb871da2784a5cb54c4f \ + --hash=sha256:b5f43ff6800669595193fd09891564ee9d1d7dcb196cab4b2506d53a2e1c95c7 +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f +typing-extensions==4.9.0 \ + --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ + --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd +wheel==0.38.4 \ + --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \ + --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8 \ No newline at end of file diff --git a/tools/deterministic-build/requirements-build-wine.txt b/tools/deterministic-build/requirements-build-wine.txt new file mode 100644 index 0000000..e6037ab --- /dev/null +++ b/tools/deterministic-build/requirements-build-wine.txt @@ -0,0 +1,20 @@ +altgraph==0.17.4 \ + --hash=sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406 +importlib-metadata==7.0.1 \ + --hash=sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc +packaging==23.2 \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 +pefile==2023.2.7 \ + --hash=sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc +pip==22.3.1 \ + --hash=sha256:65fd48317359f3af8e593943e6ae1506b66325085ea64b706a998c6e83eeaf38 +pyinstaller-hooks-contrib==2024.1 \ + --hash=sha256:51a51ea9e1ae6bd5ffa7ec45eba7579624bf4f2472ff56dba0edc186f6ed46a6 +pywin32-ctypes==0.2.2 \ + --hash=sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60 +setuptools==65.5.1 \ + --hash=sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f +wheel==0.38.4 \ + --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac +zipp==3.17.0 \ + --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 \ No newline at end of file diff --git a/tools/make_libsecp256k1.sh b/tools/make_libsecp256k1.sh new file mode 100755 index 0000000..c083e72 --- /dev/null +++ b/tools/make_libsecp256k1.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# This script was tested on Linux and MacOS hosts, where it can be used +# to build native libsecp256k1 binaries. +# +# It can also be used to cross-compile to Windows: +# $ sudo apt-get install mingw-w64 +# For a Windows x86 (32-bit) target, run: +# $ GCC_TRIPLET_HOST="i686-w64-mingw32" ./tools/make_libsecp256k1.sh +# Or for a Windows x86_64 (64-bit) target, run: +# $ GCC_TRIPLET_HOST="x86_64-w64-mingw32" ./tools/make_libsecp256k1.sh +# +# To cross-compile to Linux x86: +# sudo apt-get install gcc-multilib g++-multilib +# $ AUTOCONF_FLAGS="--host=i686-linux-gnu CFLAGS=-m32 CXXFLAGS=-m32 LDFLAGS=-m32" ./tools/make_libsecp256k1.sh + +LIBSECP_VERSION="642c885b6102725e25623738529895a95addc4f4" +# ^ tag "v0.5.1" +# note: this version is duplicated in tools/android/p4a_recipes/libsecp256k1/__init__.py +# (and also in bitcoin_safe-ecc, for the "secp256k1" git submodule) + +set -e + +. $(dirname "$0")/build_tools_util.sh || (echo "Could not source build_tools_util.sh" && exit 1) + +here="$(dirname "$(realpath "$0" 2> /dev/null || grealpath "$0")")" +CONTRIB="$here" +PROJECT_ROOT="$CONTRIB/.." + +pkgname="secp256k1" +info "Building $pkgname..." + +( + cd "$CONTRIB" + if [ ! -d secp256k1 ]; then + git clone https://github.com/bitcoin-core/secp256k1.git + fi + cd secp256k1 + if ! $(git cat-file -e ${LIBSECP_VERSION}) ; then + info "Could not find requested version $LIBSECP_VERSION in local clone; fetching..." + git fetch --all + fi + git reset --hard + git clean -dfxq + git checkout "${LIBSECP_VERSION}^{commit}" + + if ! [ -x configure ] ; then + echo "LDFLAGS = -no-undefined" >> Makefile.am + ./autogen.sh || fail "Could not run autogen for $pkgname. Please make sure you have automake and libtool installed, and try again." + fi + if ! [ -r config.status ] ; then + ./configure \ + $AUTOCONF_FLAGS \ + --prefix="$here/$pkgname/dist" \ + --enable-module-recovery \ + --enable-module-extrakeys \ + --enable-module-schnorrsig \ + --enable-experimental \ + --enable-module-ecdh \ + --disable-benchmark \ + --disable-tests \ + --disable-exhaustive-tests \ + --disable-static \ + --enable-shared || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again." + fi + make "-j$CPU_COUNT" || fail "Could not build $pkgname" + make install || fail "Could not install $pkgname" + . "$here/$pkgname/dist/lib/libsecp256k1.la" + host_strip "$here/$pkgname/dist/lib/$dlname" + if [ -n "$DLL_TARGET_DIR" ] ; then + cp -fpv "$here/$pkgname/dist/lib/$dlname" "$DLL_TARGET_DIR/" || fail "Could not copy the $pkgname binary to DLL_TARGET_DIR" + else + cp -fpv "$here/$pkgname/dist/lib/$dlname" "$PROJECT_ROOT/bitcoin_safe" || fail "Could not copy the $pkgname binary to its destination" + info "$dlname has been placed in the 'bitcoin_safe' folder." + fi +) diff --git a/tools/make_libusb.sh b/tools/make_libusb.sh new file mode 100755 index 0000000..092e0e2 --- /dev/null +++ b/tools/make_libusb.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +LIBUSB_VERSION="4239bc3a50014b8e6a5a2a59df1fff3b7469543b" +# ^ tag v1.0.26 + +set -e + +. $(dirname "$0")/build_tools_util.sh || (echo "Could not source build_tools_util.sh" && exit 1) + +here="$(dirname "$(realpath "$0" 2> /dev/null || grealpath "$0")")" +CONTRIB="$here" +PROJECT_ROOT="$CONTRIB/.." + +pkgname="libusb" +info "Building $pkgname..." + +( + cd "$CONTRIB" + if [ ! -d libusb ]; then + git clone https://github.com/libusb/libusb.git + fi + cd libusb + if ! $(git cat-file -e ${LIBUSB_VERSION}) ; then + info "Could not find requested version $LIBUSB_VERSION in local clone; fetching..." + git fetch --all + fi + git reset --hard + git clean -dfxq + git checkout "${LIBUSB_VERSION}^{commit}" + + if [ "$BUILD_TYPE" = "wine" ] ; then + echo "libusb_1_0_la_LDFLAGS += -Wc,-static" >> libusb/Makefile.am + fi + ./bootstrap.sh || fail "Could not bootstrap libusb" + if ! [ -r config.status ] ; then + if [ "$BUILD_TYPE" = "wine" ] ; then + # windows target + LDFLAGS="-Wl,--no-insert-timestamp" + elif [ $(uname) == "Darwin" ]; then + # macos target + LDFLAGS="-Wl -lm" + else + # linux target + LDFLAGS="" + fi + LDFLAGS="$LDFLAGS" ./configure \ + $AUTOCONF_FLAGS \ + || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again." + fi + make "-j$CPU_COUNT" || fail "Could not build $pkgname" + make install || warn "Could not install $pkgname" + . "$here/$pkgname/libusb/.libs/libusb-1.0.la" + host_strip "$here/$pkgname/libusb/.libs/$dlname" + TARGET_NAME="$dlname" + if [ $(uname) == "Darwin" ]; then # on mac, dlname is "libusb-1.0.0.dylib" + TARGET_NAME="libusb-1.0.dylib" + fi + cp -fpv "$here/$pkgname/libusb/.libs/$dlname" "$PROJECT_ROOT/bitcoin_safe/$TARGET_NAME" || fail "Could not copy the $pkgname binary to its destination" + info "$TARGET_NAME has been placed in the inner 'bitcoin_safe' folder." + if [ -n "$DLL_TARGET_DIR" ] ; then + cp -fpv "$here/$pkgname/libusb/.libs/$dlname" "$DLL_TARGET_DIR/$TARGET_NAME" || fail "Could not copy the $pkgname binary to DLL_TARGET_DIR" + fi +) diff --git a/tools/make_zbar.sh b/tools/make_zbar.sh new file mode 100755 index 0000000..eb77b9e --- /dev/null +++ b/tools/make_zbar.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# This script can be used on Linux hosts to build native libzbar binaries. +# sudo apt-get install pkg-config libx11-dev libx11-6 libv4l-dev libxv-dev libxext-dev libjpeg-dev +# +# It can also be used to cross-compile to Windows: +# $ sudo apt-get install mingw-w64 mingw-w64-tools win-iconv-mingw-w64-dev +# For a Windows x86 (32-bit) target, run: +# $ GCC_TRIPLET_HOST="i686-w64-mingw32" BUILD_TYPE="wine" ./contrib/make_zbar.sh +# Or for a Windows x86_64 (64-bit) target, run: +# $ GCC_TRIPLET_HOST="x86_64-w64-mingw32" BUILD_TYPE="wine" ./contrib/make_zbar.sh + +ZBAR_VERSION="bb05ec54eec57f8397cb13fb9161372a281a1219" +# ^ tag 0.23.93 + +set -e + +. $(dirname "$0")/build_tools_util.sh || (echo "Could not source build_tools_util.sh" && exit 1) + +here="$(dirname "$(realpath "$0" 2> /dev/null || grealpath "$0")")" +CONTRIB="$here" +PROJECT_ROOT="$CONTRIB/.." + +pkgname="zbar" +info "Building $pkgname..." + +( + cd "$CONTRIB" + if [ ! -d zbar ]; then + git clone https://github.com/mchehab/zbar.git + fi + cd zbar + if ! $(git cat-file -e ${ZBAR_VERSION}) ; then + info "Could not find requested version $ZBAR_VERSION in local clone; fetching..." + git fetch --all + fi + git reset --hard + git clean -dfxq + git checkout "${ZBAR_VERSION}^{commit}" + + if [ "$BUILD_TYPE" = "wine" ] ; then + echo "libzbar_la_LDFLAGS += -Wc,-static" >> zbar/Makefile.am + echo "LDFLAGS += -Wc,-static" >> Makefile.am + fi + if ! [ -x configure ] ; then + autoreconf -vfi || fail "Could not run autoreconf for $pkgname. Please make sure you have automake and libtool installed, and try again." + fi + if ! [ -r config.status ] ; then + if [ "$BUILD_TYPE" = "wine" ] ; then + # windows target + AUTOCONF_FLAGS="$AUTOCONF_FLAGS \ + --with-x=no \ + --enable-video=yes \ + --with-jpeg=no \ + --with-directshow=yes \ + --disable-dependency-tracking" + elif [ $(uname) == "Darwin" ]; then + # macos target + AUTOCONF_FLAGS="$AUTOCONF_FLAGS \ + --with-x=no \ + --enable-video=no \ + --with-jpeg=no" + else + # linux target + AUTOCONF_FLAGS="$AUTOCONF_FLAGS \ + --with-x=yes \ + --enable-video=yes \ + --with-jpeg=yes" + fi + ./configure \ + $AUTOCONF_FLAGS \ + --prefix="$here/$pkgname/dist" \ + --enable-pthread=no \ + --enable-doc=no \ + --with-python=no \ + --with-gtk=no \ + --with-qt=no \ + --with-java=no \ + --with-imagemagick=no \ + --with-dbus=no \ + --enable-codes=qrcode \ + --disable-static \ + --enable-shared || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again." + fi + make "-j$CPU_COUNT" || fail "Could not build $pkgname" + make install || fail "Could not install $pkgname" + . "$here/$pkgname/dist/lib/libzbar.la" + host_strip "$here/$pkgname/dist/lib/$dlname" + cp -fpv "$here/$pkgname/dist/lib/$dlname" "$PROJECT_ROOT/bitcoin_safe" || fail "Could not copy the $pkgname binary to its destination" + info "$dlname has been placed in the inner 'bitcoin_safe' folder." + if [ -n "$DLL_TARGET_DIR" ] ; then + cp -fpv "$here/$pkgname/dist/lib/$dlname" "$DLL_TARGET_DIR/" || fail "Could not copy the $pkgname binary to DLL_TARGET_DIR" + fi +) diff --git a/tools/translation_handler.py b/tools/translation_handler.py index b04811f..ec1a6fa 100644 --- a/tools/translation_handler.py +++ b/tools/translation_handler.py @@ -36,19 +36,9 @@ from subprocess import CompletedProcess from typing import List, Tuple, Union -logger = logging.getLogger(__name__) - - -from concurrent.futures.thread import ThreadPoolExecutor +from bitcoin_safe.util import threadtable - -def threadtable(f, arglist, max_workers=20): - with ThreadPoolExecutor(max_workers=int(max_workers)) as executor: - logger.debug("Starting {} threads {}({})".format(max_workers, str(f), str(arglist))) - res = [] - for arg in arglist: - res.append(executor.submit(f, arg)) - return [r.result() for r in res] +logger = logging.getLogger(__name__) def run_local(cmd) -> CompletedProcess: