diff --git a/README.md b/README.md index fc98367..b4c6c4d 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,14 @@ OS specifics are spelled out per step and in *Platform notes* below. On **ARM64 (aarch64)** there are no pyamg wheels — `requirements.txt` skips pyamg there and the solver falls back to 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 diff --git a/fill_resistance/main.py b/fill_resistance/main.py index e702c8e..30cb9db 100644 --- a/fill_resistance/main.py +++ b/fill_resistance/main.py @@ -39,9 +39,24 @@ def _fail(message: str, outdir) -> None: def main() -> None: outdir = None try: - from kipy.errors import ApiError + try: + 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: kicad, board = board_io.connect() stackup = board_io.get_stackup_info(board) diff --git a/fill_resistance/plots.py b/fill_resistance/plots.py index e7815ad..39dfe9f 100644 --- a/fill_resistance/plots.py +++ b/fill_resistance/plots.py @@ -24,10 +24,16 @@ def _pick_backend(): dependency and the selection dialog / progress window put a Qt event loop in this process, after which matplotlib refuses TkAgg ("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"): try: - __import__(qt) + __import__(qt + ".QtCore") return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg" except Exception: continue diff --git a/tests/test_platform_fallbacks.py b/tests/test_platform_fallbacks.py index 25528d8..e898b13 100644 --- a/tests/test_platform_fallbacks.py +++ b/tests/test_platform_fallbacks.py @@ -20,6 +20,25 @@ def test_backend_prefers_qt_over_tk(): 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( tmp_path, monkeypatch, capsys): board_dir = tmp_path / "board"