fix: drop netifaces (unbuildable on runner), harden build install

The packaged exe crashed with ModuleNotFoundError: No module named 'PySide6'.
Root cause: `pip install -e .` aborted because netifaces has no wheel for the
build's Python and needs MSVC to compile from source, so NO dependencies were
installed — and the workflow didn't catch it (native-command failures don't
trip $ErrorActionPreference, and the next pip command succeeded).

- Replace netifaces with a dependency-free socket-based LAN IP detection in the
  GUI; remove netifaces from pyproject and all PyInstaller specs.
- Make pip failures fatal (check $LASTEXITCODE) and add a "Verify runtime
  imports" step that fails the build before bundling if any dep is missing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 22:40:34 -07:00
parent 95552ca7c9
commit 2f855bf720
7 changed files with 42 additions and 16 deletions
+12
View File
@@ -87,9 +87,21 @@ jobs:
run: |
$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Native command failures do NOT trip $ErrorActionPreference, so check
# $LASTEXITCODE explicitly — otherwise a broken install ships silently.
& $env:PYTHON -m pip install --upgrade pip
if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed ($LASTEXITCODE)" }
& $env:PYTHON -m pip install -e .
if ($LASTEXITCODE -ne 0) { throw "pip install -e . failed ($LASTEXITCODE)" }
& $env:PYTHON -m pip install pyinstaller
if ($LASTEXITCODE -ne 0) { throw "pip install pyinstaller failed ($LASTEXITCODE)" }
- name: Verify runtime imports
shell: powershell
run: |
# Fail fast if any runtime dependency is missing before we bundle.
& $env:PYTHON -c "import PySide6.QtWidgets, fastapi, uvicorn, aiohttp, PIL, pystray, qrcode, pyautogui, pyperclip, websockets, multipart; print('deps OK')"
if ($LASTEXITCODE -ne 0) { throw "dependency import smoke test failed ($LASTEXITCODE)" }
- name: Build Windows executable
shell: powershell
-1
View File
@@ -39,7 +39,6 @@ A cross-platform macro management application with desktop and web interfaces. C
- PyAutoGUI (Keyboard automation)
- Pillow (Image processing)
- pystray (System tray)
- netifaces (Network detection)
- qrcode (QR code generation)
- aiohttp (Relay server client)
+30 -10
View File
@@ -469,19 +469,39 @@ class MainWindow(QMainWindow):
# QR/copied URL lets a LAN device authenticate against the API.
token = self.settings_manager.get_web_auth_token()
token_qs = f"?token={token}" if token else ""
ip = self._detect_lan_ip()
if ip:
self.ip_label.setText(f"http://{ip}:{DEFAULT_PORT}{token_qs}")
return
self.ip_label.setText(f"http://localhost:{DEFAULT_PORT}{token_qs}")
@staticmethod
def _detect_lan_ip():
"""Best-effort primary LAN IPv4, with no third-party dependency.
Uses a UDP socket to discover which local interface would be used to
reach the internet (no packets are actually sent), then falls back to
resolving the hostname. Returns None if only loopback is available.
"""
import socket
try:
import netifaces
for iface in netifaces.interfaces():
addrs = netifaces.ifaddresses(iface)
if netifaces.AF_INET in addrs:
for addr in addrs[netifaces.AF_INET]:
ip = addr.get('addr', '')
if ip and not ip.startswith('127.'):
self.ip_label.setText(f"http://{ip}:{DEFAULT_PORT}{token_qs}")
return
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
finally:
s.close()
if ip and not ip.startswith("127."):
return ip
except Exception:
pass
self.ip_label.setText(f"http://localhost:{DEFAULT_PORT}{token_qs}")
try:
for ip in socket.gethostbyname_ex(socket.gethostname())[2]:
if not ip.startswith("127."):
return ip
except Exception:
pass
return None
def copy_url_to_clipboard(self):
"""Copy the web interface URL to clipboard."""
-1
View File
@@ -45,7 +45,6 @@ a = Analysis(
'pyautogui',
'pyperclip',
'pystray',
'netifaces',
'websockets',
'multipart',
# Relay client (imported lazily in the GUI, so declare explicitly)
-1
View File
@@ -49,7 +49,6 @@ a = Analysis(
'pyperclip',
'pystray',
'pystray._base',
'netifaces',
'websockets',
'multipart',
# Linux system tray
-1
View File
@@ -45,7 +45,6 @@ a = Analysis(
'pyautogui',
'pyperclip',
'pystray',
'netifaces',
'websockets',
'multipart',
],
-2
View File
@@ -23,8 +23,6 @@ dependencies = [
"uvicorn>=0.24.0",
"websockets>=12.0",
"python-multipart>=0.0.6", # For file uploads
# Network utilities
"netifaces>=0.11.0",
# QR code generation
"qrcode>=7.4.2",
# Desktop GUI