Fail usefully on NixOS: probe QtCore, explain the missing-.so error
Build PCM package / build (push) Successful in 6s

First NixOS attempt: pip wheel PySide6 cannot load libgthread-2.0.so.0
because NixOS has no FHS library paths. Nothing inside the venv can
fix that (KiCad installs wheels only) - but the plugin made it worse
twice over:

- The backend probe imported bare PySide6, whose pure-Python __init__
  succeeds even when QtCore's .so cannot load - so matplotlib was
  promised QtAgg and the error figure died at switch_backend, taking
  the failure report with it. The probe now imports <binding>.QtCore
  and falls through to Tk/Agg on a broken Qt.
- The user got a raw ImportError traceback. A cannot-open-shared-object
  failure during the kipy/dialog imports now raises a UserFacingError
  that names the actual fixes: run KiCad in an FHS environment
  (steam-run) or enable nix-ld with Qt runtime libraries. The README
  Linux notes carry the same guidance.

141 passed on the dev stack and the Python 3.9 mac-equivalent stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
janik
2026-07-23 15:22:22 +07:00
parent 24fed64f83
commit 7604e18587
4 changed files with 52 additions and 4 deletions
+8
View File
@@ -92,6 +92,14 @@ OS specifics are spelled out per step and in *Platform notes* below.
On **ARM64 (aarch64)** there are no pyamg wheels — On **ARM64 (aarch64)** there are no pyamg wheels —
`requirements.txt` skips pyamg there and the solver falls back to `requirements.txt` skips pyamg there and the solver falls back to
Jacobi-CG: same results, noticeably slower on large grids. Jacobi-CG: same results, noticeably slower on large grids.
**NixOS**: pip's Linux wheels link against standard FHS library
paths, which NixOS does not provide — PySide6 fails with
`libgthread-2.0.so.0: cannot open shared object file`. The plugin
cannot fix this from inside its venv (KiCad installs wheels only):
run KiCad inside an FHS environment (e.g. `steam-run kicad`) or
enable `programs.nix-ld` with Qt's runtime libraries (glib,
fontconfig, freetype, dbus, libGL, libxkbcommon and the X11/xcb
set).
## Usage ## Usage
+15
View File
@@ -38,10 +38,25 @@ def _fail(message: str, outdir) -> None:
def main() -> None: def main() -> None:
outdir = None outdir = None
try:
try: try:
from kipy.errors import ApiError from kipy.errors import ApiError
from . import board_io, dialog from . import board_io, dialog
except ImportError as e:
if "cannot open shared object file" not in str(e):
raise
# pip's Linux wheels link against FHS system libraries;
# on NixOS those paths don't exist and PySide6/pynng die
# exactly like this. Nothing inside the venv can fix it.
raise UserFacingError(
f"A compiled dependency cannot load its system "
f"libraries: {e}\nThe plugin venv is built from pip "
f"wheels, which expect standard (FHS) library paths. "
f"On NixOS, run KiCad inside an FHS environment (e.g. "
f"steam-run) or enable programs.nix-ld with Qt's "
f"runtime libraries - see README, Platform notes."
)
try: try:
kicad, board = board_io.connect() kicad, board = board_io.connect()
stackup = board_io.get_stackup_info(board) stackup = board_io.get_stackup_info(board)
+8 -2
View File
@@ -24,10 +24,16 @@ def _pick_backend():
dependency and the selection dialog / progress window put a Qt event dependency and the selection dialog / progress window put a Qt event
loop in this process, after which matplotlib refuses TkAgg loop in this process, after which matplotlib refuses TkAgg
("Cannot load backend 'TkAgg' ... as 'qt' is currently running") - ("Cannot load backend 'TkAgg' ... as 'qt' is currently running") -
exactly what happened on macOS, whose bundled Python ships tkinter.""" exactly what happened on macOS, whose bundled Python ships tkinter.
The probe must import the binding's native core, not just the
package: on NixOS `import PySide6` succeeds (pure __init__) while
QtCore's .so cannot find the system libraries pip wheels expect
("libgthread-2.0.so.0: cannot open shared object file") - promising
QtAgg then kills even the error figure at switch_backend time."""
for qt in ("PySide6", "PyQt6", "PyQt5", "PySide2"): for qt in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
try: try:
__import__(qt) __import__(qt + ".QtCore")
return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg" return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg"
except Exception: except Exception:
continue continue
+19
View File
@@ -20,6 +20,25 @@ def test_backend_prefers_qt_over_tk():
assert plots._pick_backend() in ("QtAgg", "Qt5Agg") assert plots._pick_backend() in ("QtAgg", "Qt5Agg")
def test_backend_probe_requires_working_qtcore(monkeypatch):
# NixOS: `import PySide6` succeeds (a pure-Python __init__) while
# QtCore's .so cannot load the FHS system libraries pip wheels
# expect. The probe must import the native core and fall through -
# promising QtAgg kills even the error figure at switch_backend
# time, and the failure report with it.
import builtins
real_import = builtins.__import__
def broken_qt(name, *args, **kwargs):
if name.split(".")[0] in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
raise ImportError("libgthread-2.0.so.0: cannot open shared "
"object file: No such file or directory")
return real_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", broken_qt)
assert plots._pick_backend() in ("TkAgg", None)
def test_output_dir_falls_back_when_board_dir_unwritable( def test_output_dir_falls_back_when_board_dir_unwritable(
tmp_path, monkeypatch, capsys): tmp_path, monkeypatch, capsys):
board_dir = tmp_path / "board" board_dir = tmp_path / "board"