fix(packaging): harden PyInstaller CLI bundling on Linux and Windows
- Add explicit hidden imports/collect rules for coincurve, bip_utils and src modules - Introduce Docker smoke tests for hd_generate and p2pkh to fail fast on missing runtime deps - Refactor cli.py to lazy-load command modules and avoid global startup crashes from optional deps - Use pseudo-TTY script wrapper for Wine smoke tests to prevent Invalid handle failures
This commit is contained in:
@@ -17,7 +17,30 @@ RUN python3 -m venv venv && \
|
|||||||
|
|
||||||
# Compile Python CLI into a standalone binary
|
# Compile Python CLI into a standalone binary
|
||||||
RUN venv/bin/pip install --no-cache-dir pyinstaller && \
|
RUN venv/bin/pip install --no-cache-dir pyinstaller && \
|
||||||
venv/bin/pyinstaller --onefile --name cli src/cli.py && \
|
venv/bin/pyinstaller \
|
||||||
|
--onefile --name cli \
|
||||||
|
--hidden-import _cffi_backend \
|
||||||
|
--hidden-import coincurve._cffi_backend \
|
||||||
|
--hidden-import src.crypto \
|
||||||
|
--hidden-import src.hd_wallet \
|
||||||
|
--hidden-import src.p2pk \
|
||||||
|
--hidden-import src.p2pkh \
|
||||||
|
--hidden-import src.p2sh \
|
||||||
|
--hidden-import src.p2wpkh \
|
||||||
|
--hidden-import src.p2tr \
|
||||||
|
--hidden-import src.single_wallet \
|
||||||
|
--collect-data bip_utils \
|
||||||
|
--collect-submodules src \
|
||||||
|
--collect-submodules coincurve \
|
||||||
|
--collect-binaries coincurve \
|
||||||
|
--collect-data coincurve \
|
||||||
|
src/cli.py && \
|
||||||
|
dist/cli hd_generate '{}' > /tmp/cli-hd-generate.json && \
|
||||||
|
cat /tmp/cli-hd-generate.json && \
|
||||||
|
grep -Eq '"ok"[[:space:]]*:[[:space:]]*true' /tmp/cli-hd-generate.json && \
|
||||||
|
dist/cli p2pkh '{}' > /tmp/cli-p2pkh.json && \
|
||||||
|
cat /tmp/cli-p2pkh.json && \
|
||||||
|
grep -Eq '"ok"[[:space:]]*:[[:space:]]*true' /tmp/cli-p2pkh.json && \
|
||||||
mkdir -p frontend/resources && \
|
mkdir -p frontend/resources && \
|
||||||
cp dist/cli frontend/resources/cli
|
cp dist/cli frontend/resources/cli
|
||||||
|
|
||||||
|
|||||||
@@ -78,11 +78,26 @@ RUN xvfb-run -a wine "C:\\Python311\\python.exe" -m PyInstaller \
|
|||||||
--onefile --name cli \
|
--onefile --name cli \
|
||||||
--hidden-import _cffi_backend \
|
--hidden-import _cffi_backend \
|
||||||
--hidden-import coincurve._cffi_backend \
|
--hidden-import coincurve._cffi_backend \
|
||||||
|
--hidden-import src.crypto \
|
||||||
|
--hidden-import src.hd_wallet \
|
||||||
|
--hidden-import src.p2pk \
|
||||||
|
--hidden-import src.p2pkh \
|
||||||
|
--hidden-import src.p2sh \
|
||||||
|
--hidden-import src.p2wpkh \
|
||||||
|
--hidden-import src.p2tr \
|
||||||
|
--hidden-import src.single_wallet \
|
||||||
--collect-data bip_utils \
|
--collect-data bip_utils \
|
||||||
|
--collect-submodules src \
|
||||||
--collect-submodules coincurve \
|
--collect-submodules coincurve \
|
||||||
--collect-binaries coincurve \
|
--collect-binaries coincurve \
|
||||||
--collect-data coincurve \
|
--collect-data coincurve \
|
||||||
src/cli.py && \
|
src/cli.py && \
|
||||||
|
script -qec 'xvfb-run -a wine cmd /c "Z:\\build\\dist\\cli.exe hd_generate {}"' /tmp/cli-hd-generate.log && \
|
||||||
|
cat /tmp/cli-hd-generate.log && \
|
||||||
|
grep -Eq '"ok"[[:space:]]*:[[:space:]]*true' /tmp/cli-hd-generate.log && \
|
||||||
|
script -qec 'xvfb-run -a wine cmd /c "Z:\\build\\dist\\cli.exe p2pkh {}"' /tmp/cli-p2pkh.log && \
|
||||||
|
cat /tmp/cli-p2pkh.log && \
|
||||||
|
grep -Eq '"ok"[[:space:]]*:[[:space:]]*true' /tmp/cli-p2pkh.log && \
|
||||||
mkdir -p frontend/resources && \
|
mkdir -p frontend/resources && \
|
||||||
cp dist/cli.exe frontend/resources/cli.exe
|
cp dist/cli.exe frontend/resources/cli.exe
|
||||||
|
|
||||||
|
|||||||
96
src/cli.py
96
src/cli.py
@@ -4,44 +4,84 @@ Usage: python src/cli.py <command> <json_args>
|
|||||||
Prints a single JSON line to stdout.
|
Prints a single JSON line to stdout.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
try:
|
def _load_function(module_name, function_name):
|
||||||
from src.hd_wallet import generate_hd_wallet, encrypt_wallet, decrypt_wallet
|
"""
|
||||||
from src.p2pk import generate_p2pk
|
Import only what is needed for the selected command.
|
||||||
from src.p2pkh import generate_legacy_address
|
This avoids hard-failing all commands when an optional dependency is missing.
|
||||||
from src.p2sh import generate_p2sh_multisig
|
"""
|
||||||
from src.p2wpkh import generate_segwit_address
|
try:
|
||||||
from src.p2tr import generate_p2tr_address
|
module = importlib.import_module(f"src.{module_name}")
|
||||||
from src.single_wallet import encrypt_single_wallet, decrypt_single_wallet
|
except ModuleNotFoundError as err:
|
||||||
except ImportError:
|
if err.name not in {f"src.{module_name}", "src"}:
|
||||||
from hd_wallet import generate_hd_wallet, encrypt_wallet, decrypt_wallet
|
raise
|
||||||
from p2pk import generate_p2pk
|
module = importlib.import_module(module_name)
|
||||||
from p2pkh import generate_legacy_address
|
return getattr(module, function_name)
|
||||||
from p2sh import generate_p2sh_multisig
|
|
||||||
from p2wpkh import generate_segwit_address
|
|
||||||
from p2tr import generate_p2tr_address
|
def _run_hd_generate(args):
|
||||||
from single_wallet import encrypt_single_wallet, decrypt_single_wallet
|
return _load_function("hd_wallet", "generate_hd_wallet")(**args)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_hd_encrypt(args):
|
||||||
|
return _load_function("hd_wallet", "encrypt_wallet")(args["wallet"], args["password"])
|
||||||
|
|
||||||
|
|
||||||
|
def _run_hd_decrypt(args):
|
||||||
|
return _load_function("hd_wallet", "decrypt_wallet")(args["wallet"], args["password"])
|
||||||
|
|
||||||
|
|
||||||
|
def _run_p2pk(args):
|
||||||
|
return _load_function("p2pk", "generate_p2pk")(**args)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_p2pkh(args):
|
||||||
|
return _load_function("p2pkh", "generate_legacy_address")(**args)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_p2sh(args):
|
||||||
|
return _load_function("p2sh", "generate_p2sh_multisig")(**args)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_p2wpkh(args):
|
||||||
|
return _load_function("p2wpkh", "generate_segwit_address")(**args)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_p2tr(args):
|
||||||
|
return _load_function("p2tr", "generate_p2tr_address")(**args)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_single_encrypt(args):
|
||||||
|
return _load_function("single_wallet", "encrypt_single_wallet")(args["wallet"], args["password"])
|
||||||
|
|
||||||
|
|
||||||
|
def _run_single_decrypt(args):
|
||||||
|
return _load_function("single_wallet", "decrypt_single_wallet")(args["wallet"], args["password"])
|
||||||
|
|
||||||
|
|
||||||
COMMANDS = {
|
COMMANDS = {
|
||||||
'hd_generate': lambda a: generate_hd_wallet(**a),
|
"hd_generate": _run_hd_generate,
|
||||||
'hd_encrypt': lambda a: encrypt_wallet(a['wallet'], a['password']),
|
"hd_encrypt": _run_hd_encrypt,
|
||||||
'hd_decrypt': lambda a: decrypt_wallet(a['wallet'], a['password']),
|
"hd_decrypt": _run_hd_decrypt,
|
||||||
'p2pk': lambda a: generate_p2pk(**a),
|
"p2pk": _run_p2pk,
|
||||||
'p2pkh': lambda a: generate_legacy_address(**a),
|
"p2pkh": _run_p2pkh,
|
||||||
'p2sh': lambda a: generate_p2sh_multisig(**a),
|
"p2sh": _run_p2sh,
|
||||||
'p2wpkh': lambda a: generate_segwit_address(**a),
|
"p2wpkh": _run_p2wpkh,
|
||||||
'p2tr': lambda a: generate_p2tr_address(**a),
|
"p2tr": _run_p2tr,
|
||||||
'single_encrypt': lambda a: encrypt_single_wallet(a['wallet'], a['password']),
|
"single_encrypt": _run_single_encrypt,
|
||||||
'single_decrypt': lambda a: decrypt_single_wallet(a['wallet'], a['password']),
|
"single_decrypt": _run_single_decrypt,
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
command = sys.argv[1]
|
command = sys.argv[1]
|
||||||
args = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {}
|
args = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {}
|
||||||
try:
|
try:
|
||||||
|
if command not in COMMANDS:
|
||||||
|
raise ValueError(f"Unsupported command: {command}")
|
||||||
result = COMMANDS[command](args)
|
result = COMMANDS[command](args)
|
||||||
print(json.dumps({'ok': True, 'data': result}))
|
print(json.dumps({"ok": True, "data": result}))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(json.dumps({'ok': False, 'error': str(e)}))
|
print(json.dumps({"ok": False, "error": str(e)}))
|
||||||
|
|||||||
Reference in New Issue
Block a user