diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index d772ffc12..15731e02a 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -50,6 +50,9 @@ pushd $WINEPREFIX/drive_c/electrum # see https://github.com/pypa/pip/issues/2195 -- pip makes a copy of the entire directory info "Pip installing Electrum. This might take a long time if the project folder is large." $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location . +# pyinstaller needs to be able to "import electrum", for which we need libsecp256k1: +# (or could try "pip install -e" instead) +cp electrum/libsecp256k1-*.dll "$WINEPREFIX/drive_c/python3/Lib/site-packages/electrum/" popd diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index c133de4e6..013c55a97 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -4,47 +4,40 @@ from PyInstaller.utils.hooks import collect_data_files, collect_submodules, coll import sys, os +PYPKG="electrum" +MAIN_SCRIPT="run_electrum" +PROJECT_ROOT = "C:/electrum" +ICONS_FILE=f"{PROJECT_ROOT}/{PYPKG}/gui/icons/electrum.ico" + cmdline_name = os.environ.get("ELECTRUM_CMDLINE_NAME") if not cmdline_name: raise Exception('no name') -home = 'C:\\electrum\\' # see https://github.com/pyinstaller/pyinstaller/issues/2005 hiddenimports = [] hiddenimports += collect_submodules('pkg_resources') # workaround for https://github.com/pypa/setuptools/issues/1963 -hiddenimports += collect_submodules('trezorlib') -hiddenimports += collect_submodules('safetlib') -hiddenimports += collect_submodules('btchip') # device plugin: ledger -hiddenimports += collect_submodules('ledger_bitcoin') # device plugin: ledger -hiddenimports += collect_submodules('keepkeylib') -hiddenimports += collect_submodules('websocket') -hiddenimports += collect_submodules('ckcc') -hiddenimports += collect_submodules('bitbox02') -hiddenimports += ['electrum.plugins.jade.jade'] -hiddenimports += ['electrum.plugins.jade.jadepy.jade'] -hiddenimports += ['PyQt5.QtPrintSupport'] # needed by Revealer +hiddenimports += collect_submodules(f"{PYPKG}.plugins") binaries = [] - # Workaround for "Retro Look": binaries += [b for b in collect_dynamic_libs('PyQt5') if 'qwindowsvista' in b[0]] +# add libsecp256k1, libusb, etc: +binaries += [(f"{PROJECT_ROOT}/{PYPKG}/*.dll", '.')] -binaries += [('C:/tmp/libsecp256k1-2.dll', '.')] -binaries += [('C:/tmp/libusb-1.0.dll', '.')] -binaries += [('C:/tmp/libzbar-0.dll', '.')] datas = [ - (home+'electrum/*.json', 'electrum'), - (home+'electrum/lnwire/*.csv', 'electrum/lnwire'), - (home+'electrum/wordlist/english.txt', 'electrum/wordlist'), - (home+'electrum/wordlist/slip39.txt', 'electrum/wordlist'), - (home+'electrum/locale', 'electrum/locale'), - (home+'electrum/plugins', 'electrum/plugins'), - (home+'electrum/gui/icons', 'electrum/gui/icons'), + (f"{PROJECT_ROOT}/{PYPKG}/*.json", PYPKG), + (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"), ] -datas += collect_data_files('trezorlib') +datas += collect_data_files(f"{PYPKG}.plugins") +datas += collect_data_files('trezorlib') # TODO is this needed? and same question for other hww libs datas += collect_data_files('safetlib') datas += collect_data_files('btchip') datas += collect_data_files('keepkeylib') @@ -52,29 +45,19 @@ datas += collect_data_files('ckcc') datas += collect_data_files('bitbox02') # 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([home+'run_electrum', - home+'electrum/gui/qt/main_window.py', - home+'electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py', - home+'electrum/gui/text.py', - home+'electrum/util.py', - home+'electrum/wallet.py', - home+'electrum/simple_config.py', - home+'electrum/bitcoin.py', - home+'electrum/dnssec.py', - home+'electrum/commands.py', - home+'electrum/plugins/cosigner_pool/qt.py', - home+'electrum/plugins/trezor/qt.py', - home+'electrum/plugins/safe_t/client.py', - home+'electrum/plugins/safe_t/qt.py', - home+'electrum/plugins/keepkey/qt.py', - home+'electrum/plugins/ledger/qt.py', - home+'electrum/plugins/coldcard/qt.py', - home+'electrum/plugins/jade/qt.py', - #home+'packages/requests/utils.py' +a = Analysis([f"{PROJECT_ROOT}/{MAIN_SCRIPT}", + f"{PROJECT_ROOT}/{PYPKG}/gui/qt/main_window.py", + f"{PROJECT_ROOT}/{PYPKG}/gui/qt/qrreader/qtmultimedia/camera_dialog.py", + f"{PROJECT_ROOT}/{PYPKG}/gui/text.py", + f"{PROJECT_ROOT}/{PYPKG}/util.py", + f"{PROJECT_ROOT}/{PYPKG}/wallet.py", + f"{PROJECT_ROOT}/{PYPKG}/simple_config.py", + f"{PROJECT_ROOT}/{PYPKG}/bitcoin.py", + f"{PROJECT_ROOT}/{PYPKG}/dnssec.py", + f"{PROJECT_ROOT}/{PYPKG}/commands.py", ], binaries=binaries, datas=datas, - #pathex=[home+'lib', home+'gui', home+'plugins'], hiddenimports=hiddenimports, hookspath=[]) @@ -125,11 +108,11 @@ exe_standalone = EXE( a.scripts, a.binaries, a.datas, - name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + ".exe"), + name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}.exe"), debug=False, strip=None, upx=False, - icon=home+'electrum/gui/icons/electrum.ico', + icon=ICONS_FILE, console=False) # console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used @@ -138,11 +121,11 @@ exe_portable = EXE( a.scripts, a.binaries, a.datas + [('is_portable', 'README.md', 'DATA')], - name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + "-portable.exe"), + name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}-portable.exe"), debug=False, strip=None, upx=False, - icon=home+'electrum/gui/icons/electrum.ico', + icon=ICONS_FILE, console=False) ##### @@ -152,22 +135,22 @@ exe_inside_setup_noconsole = EXE( pyz, a.scripts, exclude_binaries=True, - name=os.path.join('build\\pyi.win32\\electrum', cmdline_name), + name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}.exe"), debug=False, strip=None, upx=False, - icon=home+'electrum/gui/icons/electrum.ico', + icon=ICONS_FILE, console=False) exe_inside_setup_console = EXE( pyz, a.scripts, exclude_binaries=True, - name=os.path.join('build\\pyi.win32\\electrum', cmdline_name+"-debug"), + name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}-debug.exe"), debug=False, strip=None, upx=False, - icon=home+'electrum/gui/icons/electrum.ico', + icon=ICONS_FILE, console=True) coll = COLLECT( @@ -179,6 +162,6 @@ coll = COLLECT( strip=None, upx=True, debug=False, - icon=home+'electrum/gui/icons/electrum.ico', + icon=ICONS_FILE, console=False, - name=os.path.join('dist', 'electrum')) + name=os.path.join('dist', PYPKG)) diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index a65945983..28bc48e02 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -53,9 +53,9 @@ $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-binary : # copy already built DLLs -cp "$DLL_TARGET_DIR"/libsecp256k1-*.dll $WINEPREFIX/drive_c/tmp/ || fail "Could not copy libsecp to its destination" -cp "$DLL_TARGET_DIR/libzbar-0.dll" $WINEPREFIX/drive_c/tmp/ || fail "Could not copy libzbar to its destination" -cp "$DLL_TARGET_DIR/libusb-1.0.dll" $WINEPREFIX/drive_c/tmp/ || fail "Could not copy libusb to its destination" +cp "$DLL_TARGET_DIR"/libsecp256k1-*.dll $WINEPREFIX/drive_c/electrum/electrum/ || fail "Could not copy libsecp to its destination" +cp "$DLL_TARGET_DIR/libzbar-0.dll" $WINEPREFIX/drive_c/electrum/electrum/ || fail "Could not copy libzbar to its destination" +cp "$DLL_TARGET_DIR/libusb-1.0.dll" $WINEPREFIX/drive_c/electrum/electrum/ || fail "Could not copy libusb to its destination" info "Building PyInstaller." diff --git a/contrib/osx/make_osx.sh b/contrib/osx/make_osx.sh index 254eb3ccb..308ac16cb 100755 --- a/contrib/osx/make_osx.sh +++ b/contrib/osx/make_osx.sh @@ -223,6 +223,9 @@ python3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: info "Building $PACKAGE..." python3 -m pip install --no-build-isolation --no-dependencies \ --no-warn-script-location . > /dev/null || fail "Could not build $PACKAGE" +# pyinstaller needs to be able to "import electrum", for which we need libsecp256k1: +# (or could try "pip install -e" instead) +cp "$PROJECT_ROOT/electrum"/libsecp256k1.*.dylib "$VENV_DIR/lib/python$PY_VER_MAJOR/site-packages/electrum/" # strip debug symbols of some compiled libs # - hidapi (hid.cpython-39-darwin.so) in particular is not reproducible without this diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec index b4fb53df9..bf15dce3c 100644 --- a/contrib/osx/osx.spec +++ b/contrib/osx/osx.spec @@ -4,83 +4,67 @@ from PyInstaller.utils.hooks import collect_data_files, collect_submodules, coll import sys, os -PACKAGE='Electrum' +PACKAGE_NAME='Electrum.app' PYPKG='electrum' MAIN_SCRIPT='run_electrum' -ICONS_FILE=PYPKG + '/gui/icons/electrum.icns' +PROJECT_ROOT = os.path.abspath(".") +ICONS_FILE=f"{PROJECT_ROOT}/{PYPKG}/gui/icons/electrum.icns" VERSION = os.environ.get("ELECTRUM_VERSION") if not VERSION: raise Exception('no version') -electrum = os.path.abspath(".") + "/" block_cipher = None # see https://github.com/pyinstaller/pyinstaller/issues/2005 hiddenimports = [] hiddenimports += collect_submodules('pkg_resources') # workaround for https://github.com/pypa/setuptools/issues/1963 -hiddenimports += collect_submodules('trezorlib') -hiddenimports += collect_submodules('safetlib') -hiddenimports += collect_submodules('btchip') # device plugin: ledger -hiddenimports += collect_submodules('ledger_bitcoin') # device plugin: ledger -hiddenimports += collect_submodules('keepkeylib') -hiddenimports += collect_submodules('websocket') -hiddenimports += collect_submodules('ckcc') -hiddenimports += collect_submodules('bitbox02') -hiddenimports += ['electrum.plugins.jade.jade'] -hiddenimports += ['electrum.plugins.jade.jadepy.jade'] -hiddenimports += ['PyQt5.QtPrintSupport'] # needed by Revealer +hiddenimports += collect_submodules(f"{PYPKG}.plugins") + + +binaries = [] +# Workaround for "Retro Look": +binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]] +# add libsecp256k1, libusb, etc: +binaries += [(f"{PROJECT_ROOT}/{PYPKG}/*.dylib", ".")] + datas = [ - (electrum + PYPKG + '/*.json', PYPKG), - (electrum + PYPKG + '/lnwire/*.csv', PYPKG + '/lnwire'), - (electrum + PYPKG + '/wordlist/english.txt', PYPKG + '/wordlist'), - (electrum + PYPKG + '/wordlist/slip39.txt', PYPKG + '/wordlist'), - (electrum + PYPKG + '/locale', PYPKG + '/locale'), - (electrum + PYPKG + '/plugins', PYPKG + '/plugins'), - (electrum + PYPKG + '/gui/icons', PYPKG + '/gui/icons'), + (f"{PROJECT_ROOT}/{PYPKG}/*.json", PYPKG), + (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"), ] -datas += collect_data_files('trezorlib') +datas += collect_data_files(f"{PYPKG}.plugins") +datas += collect_data_files('trezorlib') # TODO is this needed? and same question for other hww libs datas += collect_data_files('safetlib') datas += collect_data_files('btchip') datas += collect_data_files('keepkeylib') datas += collect_data_files('ckcc') datas += collect_data_files('bitbox02') -# Add libusb so Trezor and Safe-T mini will work -binaries = [(electrum + "electrum/libusb-1.0.dylib", ".")] -binaries += [(electrum + "electrum/libsecp256k1.2.dylib", ".")] -binaries += [(electrum + "electrum/libzbar.0.dylib", ".")] - -# Workaround for "Retro Look": -binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]] - # 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([electrum+ MAIN_SCRIPT, - electrum+'electrum/gui/qt/main_window.py', - electrum+'electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py', - electrum+'electrum/gui/text.py', - electrum+'electrum/util.py', - electrum+'electrum/wallet.py', - electrum+'electrum/simple_config.py', - electrum+'electrum/bitcoin.py', - electrum+'electrum/dnssec.py', - electrum+'electrum/commands.py', - electrum+'electrum/plugins/cosigner_pool/qt.py', - electrum+'electrum/plugins/trezor/qt.py', - electrum+'electrum/plugins/safe_t/client.py', - electrum+'electrum/plugins/safe_t/qt.py', - electrum+'electrum/plugins/keepkey/qt.py', - electrum+'electrum/plugins/ledger/qt.py', - electrum+'electrum/plugins/coldcard/qt.py', - electrum+'electrum/plugins/jade/qt.py', +a = Analysis([f"{PROJECT_ROOT}/{MAIN_SCRIPT}", + f"{PROJECT_ROOT}/{PYPKG}/gui/qt/main_window.py", + f"{PROJECT_ROOT}/{PYPKG}/gui/qt/qrreader/qtmultimedia/camera_dialog.py", + f"{PROJECT_ROOT}/{PYPKG}/gui/text.py", + f"{PROJECT_ROOT}/{PYPKG}/util.py", + f"{PROJECT_ROOT}/{PYPKG}/wallet.py", + f"{PROJECT_ROOT}/{PYPKG}/simple_config.py", + f"{PROJECT_ROOT}/{PYPKG}/bitcoin.py", + f"{PROJECT_ROOT}/{PYPKG}/dnssec.py", + f"{PROJECT_ROOT}/{PYPKG}/commands.py", ], 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]: @@ -106,7 +90,7 @@ exe = EXE( debug=False, strip=False, upx=True, - icon=electrum+ICONS_FILE, + icon=ICONS_FILE, console=False, target_arch='x86_64', # TODO investigate building 'universal2' ) @@ -116,9 +100,9 @@ app = BUNDLE( a.binaries, a.zipfiles, a.datas, - version = VERSION, - name=PACKAGE + '.app', - icon=electrum+ICONS_FILE, + version=VERSION, + name=PACKAGE_NAME, + icon=ICONS_FILE, bundle_identifier=None, info_plist={ 'NSHighResolutionCapable': 'True', diff --git a/electrum/gui/common_qt/__init__.py b/electrum/gui/common_qt/__init__.py index e69de29bb..b2be0c9a4 100644 --- a/electrum/gui/common_qt/__init__.py +++ b/electrum/gui/common_qt/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2023 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php + +import sys + + +# FIXME: remove when both desktop and mobile are Qt6 +def get_qt_major_version() -> int: + _GUI_QT_VERSION = getattr(sys, '_GUI_QT_VERSION', None) + if _GUI_QT_VERSION is None: + # used by pyinstaller when building (analysis phase) + _GUI_QT_VERSION = 5 + if _GUI_QT_VERSION in (5, 6): + return _GUI_QT_VERSION + raise Exception(f"unexpected {_GUI_QT_VERSION=}") diff --git a/electrum/gui/common_qt/plugins.py b/electrum/gui/common_qt/plugins.py index 0934477b4..9edaa9b19 100644 --- a/electrum/gui/common_qt/plugins.py +++ b/electrum/gui/common_qt/plugins.py @@ -1,9 +1,13 @@ import sys -if getattr(sys, '_GUI_QT_VERSION') == 5: # FIXME: remove when both desktop and mobile are Qt6 +from . import get_qt_major_version + +if (qt_ver := get_qt_major_version()) == 5: from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject -else: +elif qt_ver == 6: from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject +else: + raise Exception(f"unexpected {qt_ver=}") from electrum.logging import get_logger diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index 35926cb70..7e28f61f5 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -6,13 +6,19 @@ from typing import TYPE_CHECKING try: import PyQt6 -except Exception: - sys.exit("Error: Could not import PyQt6. On Linux systems, you may try 'sudo apt-get install python3-pyqt6'") +except Exception as e: + from electrum import GuiImportError + raise GuiImportError( + "Error: Could not import PyQt6. On Linux systems, " + "you may try 'sudo apt-get install python3-pyqt6'") from e try: import PyQt6.QtQml -except Exception: - sys.exit("Error: Could not import PyQt6.QtQml. On Linux systems, you may try 'sudo apt-get install python3-pyqt6.qtquick'") +except Exception as e: + from electrum import GuiImportError + raise GuiImportError( + "Error: Could not import PyQt6.QtQml. On Linux systems, " + "you may try 'sudo apt-get install python3-pyqt6.qtquick'") from e from PyQt6.QtCore import (Qt, QCoreApplication, QLocale, QTranslator, QTimer, QT_VERSION_STR, PYQT_VERSION_STR) from PyQt6.QtGui import QGuiApplication diff --git a/electrum/plugin.py b/electrum/plugin.py index 631f6a388..e24a8effb 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -80,7 +80,13 @@ class Plugins(DaemonThread): """ if cls._all_found_plugins is None: cls._all_found_plugins = dict() - for loader, name, ispkg in pkgutil.iter_modules([cls.pkgpath]): + iter_modules = list(pkgutil.iter_modules([cls.pkgpath])) + for loader, name, ispkg in iter_modules: + # FIXME pyinstaller binaries are packaging each built-in plugin twice: + # once as data and once as code. To honor the "no duplicates" rule below, + # we exclude the ones packaged as *code*, here: + if loader.__class__.__qualname__ == "FrozenImporter": + continue full_name = f'electrum.plugins.{name}' spec = importlib.util.find_spec(full_name) if spec is None: # pkgutil found it but importlib can't ?! @@ -94,7 +100,9 @@ class Plugins(DaemonThread): except Exception as e: raise Exception(f"Error pre-loading {full_name}: {repr(e)}") from e d = module.__dict__ - assert name not in cls._all_found_plugins + if name in cls._all_found_plugins: + _logger.info(f"Found the following plugin modules: {iter_modules=}") + raise Exception(f"duplicate plugins? for {name=}") cls._all_found_plugins[name] = d return cls._all_found_plugins diff --git a/electrum/plugins/trustedcoin/common_qt.py b/electrum/plugins/trustedcoin/common_qt.py index 31b9954e7..01836d972 100644 --- a/electrum/plugins/trustedcoin/common_qt.py +++ b/electrum/plugins/trustedcoin/common_qt.py @@ -4,10 +4,14 @@ import base64 import sys from typing import TYPE_CHECKING -if getattr(sys, '_GUI_QT_VERSION') == 5: # FIXME: remove when both desktop and mobile are Qt6 +from electrum.gui.common_qt import get_qt_major_version + +if (qt_ver := get_qt_major_version()) == 5: from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot -else: +elif qt_ver == 6: from PyQt6.QtCore import pyqtSignal, pyqtProperty, pyqtSlot +else: + raise Exception(f"unexpected {qt_ver=}") from electrum.i18n import _ from electrum.bip32 import BIP32Node