8 Commits

Author SHA1 Message Date
grabowski 1cc48993af CI: fit the dependency set on the runner's disk
tests / macos-latest · py3.11 (push) Waiting to run
tests / macos-latest · py3.13 (push) Waiting to run
tests / windows-latest · py3.11 (push) Waiting to run
tests / windows-latest · py3.13 (push) Waiting to run
Build PCM package / build (push) Successful in 17s
tests / ubuntu-latest · py3.11 (push) Successful in 1m36s
tests / ubuntu-latest · py3.13 (push) Successful in 2m7s
tests / archlinux:latest (push) Successful in 3m0s
tests / debian:12 (push) Successful in 49s
tests / fedora:latest (push) Successful in 1m22s
tests / ubuntu:24.04 (push) Successful in 1m5s
tests / NixOS (FHS wrapper from docs/NIXOS.md) (push) Has been skipped
uv's cache moves into the workspace so it shares a filesystem with
.venv and hardlinks instead of double-storing ~1.3 GB per job (the
'Failed to hardlink, falling back to full copy' warning was the tell
before ENOSPC), and both matrices run max-parallel 1 so the host
holds one dependency set at a time instead of six.
2026-07-23 17:35:56 +07:00
grabowski 72bb46e9b5 Harden the container checkout: no template expansion inside run
tests / windows-latest · py3.11 (push) Waiting to run
tests / windows-latest · py3.13 (push) Waiting to run
tests / macos-latest · py3.11 (push) Waiting to run
tests / macos-latest · py3.13 (push) Waiting to run
Build PCM package / build (push) Successful in 6s
tests / ubuntu-latest · py3.11 (push) Successful in 1m59s
tests / ubuntu-latest · py3.13 (push) Successful in 1m8s
tests / debian:12 (push) Successful in 53s
tests / archlinux:latest (push) Successful in 45s
tests / fedora:latest (push) Successful in 1m2s
tests / ubuntu:24.04 (push) Successful in 2m7s
tests / NixOS (FHS wrapper from docs/NIXOS.md) (push) Has been skipped
github.ref reaches the shell via env instead of ${{ }} interpolation
- a crafted PR branch name could otherwise inject commands into the
runner - and ref/sha formats are validated before git sees them.
2026-07-23 17:33:50 +07:00
grabowski f68c14b01c Distro CI containers: check out with plain git, not actions/checkout
tests / windows-latest · py3.13 (push) Waiting to run
tests / macos-latest · py3.11 (push) Waiting to run
tests / macos-latest · py3.13 (push) Waiting to run
tests / windows-latest · py3.11 (push) Waiting to run
Build PCM package / build (push) Successful in 6s
tests / ubuntu-latest · py3.11 (push) Failing after 40s
tests / ubuntu-latest · py3.13 (push) Failing after 41s
tests / archlinux:latest (push) Failing after 26s
tests / debian:12 (push) Failing after 46s
tests / fedora:latest (push) Failing after 41s
tests / ubuntu:24.04 (push) Successful in 1m18s
tests / NixOS (FHS wrapper from docs/NIXOS.md) (push) Has been skipped
act_runner injects no node runtime into job containers the way
GitHub's runners do, so the JS checkout action dies in plain distro
images with 'node: executable file not found in $PATH'. A git
init/fetch/checkout needs only git, which each distro setup step now
installs before checkout runs.
2026-07-23 17:32:35 +07:00
grabowski dfba5561bb CI: run the suite on every platform the README now advertises
tests / windows-latest · py3.11 (push) Waiting to run
tests / windows-latest · py3.13 (push) Waiting to run
tests / macos-latest · py3.11 (push) Waiting to run
tests / macos-latest · py3.13 (push) Waiting to run
Build PCM package / build (push) Successful in 5s
tests / ubuntu-latest · py3.11 (push) Successful in 1m32s
tests / ubuntu-latest · py3.13 (push) Successful in 1m25s
tests / archlinux:latest (push) Failing after 14s
tests / debian:12 (push) Failing after 7s
tests / fedora:latest (push) Failing after 8s
tests / ubuntu:24.04 (push) Failing after 9s
tests / NixOS (FHS wrapper from docs/NIXOS.md) (push) Has been skipped
Gitea Actions matrix: ubuntu-latest (py3.11/3.13) plus Debian 12,
Ubuntu 24.04, Fedora and Arch containers, each installing the system
libraries PySide6 wheels dlopen - all four validated locally in
Docker (Debian 141/141, QtWidgets import probe green everywhere).
Windows/macOS and a NixOS-FHS job (tools/ci-fhs.nix, the library set
from docs/NIXOS.md) are gated to github.com runners so they skip
instead of queueing forever here. README gets a Platform support
table with the workflow badge; KiCad-in-the-loop stays field-tested,
CI covers the solver and fallback suite.
2026-07-23 17:25:26 +07:00
grabowski de7a4f8641 Probe QtWidgets, not QtCore: the qt backend needs the full GUI stack
QtCore's libraries (glib, icu) can load while QtWidgets/QtGui still
miss libGL - matplotlib's qt backend imports all three, so the old
probe could promise QtAgg and lose even the error figure at
switch_backend, one library layer deeper than the NixOS fix. The
regression test's mock now mirrors the real failure (bare package and
QtCore import fine, GUI modules raise): both the bare-import and the
QtCore-probe variants now fail it, verified by mutation. Also drop two
redundant f-prefixes on placeholder-less strings in report.py.
2026-07-23 17:25:19 +07:00
Berwn a4945f419c NixOS works: document the full FHS recipe, drop the nix-ld dead end
Build PCM package / build (push) Successful in 5s
First successful Linux field test (NixOS 26.05, Plasma 6, KiCad 10).
Getting there peeled three layers the error messages misattribute:
the FHS env itself (libgthread), then libs the xcb-cursor hint hides
(the xcb-util family, and libzstd for libQt6Core - in zstd.out, not
zstd's default output), and finally Plasma's QT_PLUGIN_PATH feeding
the system Qt's platform plugins to the wheel's bundled Qt, a
private-ABI mismatch that fails exactly like a missing library.

New docs/NIXOS.md carries the verified buildFHSEnv wrapper, the
steam-run quick test (now with env -u QT_PLUGIN_PATH), and a guide
for chasing future missing sonames. README upgrades Linux from
untested to works and links the doc. The in-plugin error no longer
suggests programs.nix-ld: the venv python is a Nix binary, so the
nix-ld shim never engages for it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 16:08:15 +07:00
janik 64676624e6 NixOS: steam-run alone is one library short of a working Qt
Build PCM package / build (push) Successful in 6s
Field-tested: under steam-run the PySide6 wheel loads, but Qt aborts
at platform-plugin init - the Steam runtime predates Qt 6.5, which
hard-requires libxcb-cursor0 for xcb (and its wayland stack is too
old for the wayland plugin). Document the working invocation: point
LD_LIBRARY_PATH at xcb-util-cursor on top of steam-run, or build a
proper buildFHSEnv wrapper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:32:46 +07:00
janik 7604e18587 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>
2026-07-23 15:22:22 +07:00
8 changed files with 428 additions and 14 deletions
+176
View File
@@ -0,0 +1,176 @@
# Runs the full pytest suite (solver, raster, platform fallbacks)
# against real pip wheels on every platform row advertised in the
# README's "Platform support" table. What it cannot do is launch KiCad
# itself — the plugin-inside-KiCad integration stays field-tested.
#
# Gitea/Forgejo Actions syntax is GitHub-Actions-compatible; if the
# repo is ever mirrored to github.com, copy this file to
# .github/workflows/ (GitHub does not read .gitea/). The
# windows/macos and NixOS jobs need runners that only github.com
# provides for free, so they are gated on the server URL instead of
# queueing forever on a self-hosted instance; drop the `if:` lines if
# you register your own runners with those labels.
# Third-party actions pinned to commit SHAs, matching build-pcm.yml:
# mutable tags could be repointed at malicious code.
name: tests
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
env:
UV_PYTHON: "3.12"
# Cache on the same filesystem as .venv so uv hardlinks instead of
# double-storing ~1.3 GB per job (PySide6 + scipy + matplotlib) —
# the runner host ran out of disk with the default cache location.
UV_CACHE_DIR: .uv-cache
# No test opens a window, but if one ever creates a figure the Qt
# backend must not try to reach a display on a headless runner.
QT_QPA_PLATFORM: offscreen
jobs:
linux:
name: ubuntu-latest · py${{ matrix.python }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
# Serialized: the runner host has limited disk; one dependency
# set at a time keeps peak usage at a single job's worth.
max-parallel: 1
matrix:
python: ["3.11", "3.13"]
env:
UV_PYTHON: ${{ matrix.python }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Qt runtime libraries (PySide6 wheels dlopen these)
run: >
sudo apt-get update -qq && sudo apt-get install -y -qq
libglib2.0-0 libgl1 libegl1 libopengl0 libfontconfig1
libfreetype6 libdbus-1-3 libxkbcommon0 libzstd1 libxcb1
libx11-6 libxext6 libxrender1 libsm6 libice6
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- run: uv sync --frozen
- run: uv run python -c "import PySide6.QtWidgets"
- run: uv run pytest -q
distros:
name: ${{ matrix.distro }}
runs-on: ubuntu-latest
container: ${{ matrix.distro }}
strategy:
fail-fast: false
# See the linux job: serialized for the runner host's disk.
max-parallel: 1
matrix:
include:
# git in every list: the checkout below is plain git because
# actions/checkout is a JS action and act_runner (unlike
# GitHub's runners) injects no node into job containers -
# plain distro images crash it with "node: executable file
# not found in $PATH".
- distro: debian:12
setup: >
apt-get update -qq && apt-get install -y -qq
curl ca-certificates git
libglib2.0-0 libgl1 libegl1 libopengl0 libfontconfig1
libfreetype6 libdbus-1-3 libxkbcommon0 libzstd1 libxcb1
libx11-6 libxext6 libxrender1 libsm6 libice6
- distro: ubuntu:24.04
setup: >
apt-get update -qq && apt-get install -y -qq
curl ca-certificates git
libglib2.0-0 libgl1 libegl1 libopengl0 libfontconfig1
libfreetype6 libdbus-1-3 libxkbcommon0 libzstd1 libxcb1
libx11-6 libxext6 libxrender1 libsm6 libice6
- distro: fedora:latest
setup: >
dnf install -y -q
git-core
glib2 mesa-libGL mesa-libEGL libglvnd-opengl fontconfig
freetype dbus-libs libxkbcommon libzstd libxcb libX11
libXext libXrender libSM libICE
- distro: archlinux:latest
setup: >
pacman -Syu --noconfirm --needed
curl git
glib2 libglvnd fontconfig freetype2 dbus
libxkbcommon zstd libxcb libx11 libxext libxrender
libsm libice
steps:
- name: Distro Qt runtime libraries + git
run: ${{ matrix.setup }}
# Plain-git checkout: works in any container with git alone.
# (A private repo would need github.token on the URL.) Values
# arrive via env, not ${{ }} in the script: a template expansion
# inside `run:` would let a crafted PR branch name inject shell
# commands into the runner.
- name: Checkout
env:
SERVER_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
REF: ${{ github.ref }}
SHA: ${{ github.sha }}
run: |
case "$REF" in refs/heads/*|refs/tags/*|refs/pull/*) ;; *) echo "unexpected ref: $REF"; exit 1;; esac
case "$SHA" in *[!0-9a-f]*|"") echo "unexpected sha"; exit 1;; esac
git init -q .
git remote add origin "$SERVER_URL/$REPO.git"
git fetch -q --depth 50 origin "$REF"
git checkout -q "$SHA"
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- run: uv sync --frozen
- run: uv run python -c "import PySide6.QtWidgets"
- run: uv run pytest -q
desktop:
name: ${{ matrix.os }} · py${{ matrix.python }}
# github.com only: no free Windows/macOS runners on self-hosted
# Gitea/Forgejo. Skipped (not queued) elsewhere.
if: github.server_url == 'https://github.com'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
python: ["3.11", "3.13"]
env:
UV_PYTHON: ${{ matrix.python }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- run: uv sync --frozen
- run: uv run python -c "import PySide6.QtWidgets"
- run: uv run pytest -q
nixos:
name: NixOS (FHS wrapper from docs/NIXOS.md)
# Needs unprivileged user namespaces for bubblewrap, which the
# GitHub ubuntu VM allows but a docker-based act_runner does not.
if: github.server_url == 'https://github.com'
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: DeterminateSystems/nix-installer-action@e50d5f73bfe71c2dd0aa4218de8f4afa59f8f81d # v16
- name: Build the FHS environment (docs/NIXOS.md library set)
run: nix-build tools/ci-fhs.nix -o ci-fhs
- name: Run the suite inside the FHS env
# shellcheck disable=SC2016 -- $HOME/$PATH expand inside the
# FHS bash, not the runner shell; single quotes are the point.
run: |
# shellcheck disable=SC2016
./ci-fhs/bin/fill-resistance-ci -c '
set -e
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
export UV_PROJECT_ENVIRONMENT=/tmp/venv
uv sync --frozen
uv run python -c "import PySide6.QtWidgets"
uv run pytest -q
'
+41 -8
View File
@@ -28,16 +28,37 @@ happens around the notch on F.Cu.*
Uses the KiCad **IPC API** (`kicad-python` / `kipy`), not the deprecated Uses the KiCad **IPC API** (`kicad-python` / `kipy`), not the deprecated
SWIG API. Requires KiCad **10.0.1+**. SWIG API. Requires KiCad **10.0.1+**.
## Platform support
[![tests](https://git.b4l.co.th/B4L/kicad-zone-resistance/actions/workflows/ci.yml/badge.svg?branch=main)](https://git.b4l.co.th/B4L/kicad-zone-resistance/actions)
| Platform | Status | Verified by |
|-----------------------------|:------:|-------------|
| Windows | ✅ | development platform; CI test suite |
| macOS | ✅ | field-tested in KiCad 10; CI test suite |
| NixOS | ✅ | field-tested in KiCad 10 ([setup](docs/NIXOS.md)); CI runs the suite inside the documented FHS env |
| Debian 12 | ✅ | CI test suite in container |
| Ubuntu 24.04 | ✅ | CI test suite in container |
| Fedora (latest) | ✅ | CI test suite in container |
| Arch (latest) | ✅ | CI test suite in container |
CI (`.gitea/workflows/ci.yml`) runs the full pytest suite — solver,
rasterizer, and the platform-fallback regressions — headless against
the real pip wheels of each row, including the `PySide6.QtWidgets`
import probe that decides the matplotlib backend. What CI *cannot* do
is launch KiCad itself, so "runs inside KiCad" remains field-tested
(Windows continuously, macOS and NixOS per release).
## Setup (one-time) ## Setup (one-time)
The plugin is developed and tested on **Windows**; **macOS works** The plugin is developed and tested on **Windows**; **macOS works**
(field-tested on KiCad 10 after a round of mac-specific fixes). (field-tested on KiCad 10 after a round of mac-specific fixes), and
**Linux is expected to work but is untested so far** — the code and **Linux works** (field-tested on NixOS — the hardest Linux to run pip
the dependency stack have been audited (KiCad builds the plugin a wheels on; mainstream FHS distributions should be no harder, reports
private Python venv from `requirements.txt` on every platform, from welcome). KiCad builds the plugin a private Python venv from
pre-built wheels only, no compiler needed), but nobody has run the `requirements.txt` on every platform, from pre-built wheels only, no
plugin there yet. Reports welcome! Steps 14 are the same everywhere; compiler needed. Steps 14 are the same everywhere; OS specifics are
OS specifics are spelled out per step and in *Platform notes* below. spelled out per step and in *Platform notes* below.
1. **Enable the API server**: KiCad → Preferences → Plugins → check 1. **Enable the API server**: KiCad → Preferences → Plugins → check
*Enable KiCad API*. *Enable KiCad API*.
@@ -87,11 +108,23 @@ OS specifics are spelled out per step and in *Platform notes* below.
Plot and dialog windows may open **behind** the KiCad window (they Plot and dialog windows may open **behind** the KiCad window (they
are raised best-effort) — check the Dock if nothing seems to appear are raised best-effort) — check the Dock if nothing seems to appear
after a solve. after a solve.
- **Linux** — **untested** (audited only, same caveat). The venv uses - **Linux** — **works** (field-tested on NixOS, KiCad 10; mainstream
distributions are audited but not yet field-tested). The venv uses
the system Python (3.9+), so the stack matches your distribution. the system Python (3.9+), so the stack matches your distribution.
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**: **works** (field-tested on NixOS 26.05, Plasma 6). 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 built with `buildFHSEnv`, and — on KDE Plasma — unset
`QT_PLUGIN_PATH`, which otherwise poisons the wheel's bundled Qt
with the system's Qt plugins. The tested wrapper (exact package
list incl. the non-obvious `zstd.out` and xcb-util family), a
`steam-run` quick test, and a debugging guide are in
[docs/NIXOS.md](docs/NIXOS.md).
## Usage ## Usage
+128
View File
@@ -0,0 +1,128 @@
# Running Fill Resistance on NixOS
KiCad builds the plugin a private venv from pip wheels
(`requirements.txt`). Those Linux wheels — PySide6 in particular —
`dlopen` system libraries at standard FHS paths (`/usr/lib`), which
NixOS does not provide. Nothing inside the venv can fix that; KiCad
must be **launched in an environment that supplies the libraries**.
This page gives a verified, declarative setup (field-tested on
NixOS 26.05, KiCad 10, Plasma 6 on Wayland) and maps each failure
mode to its cause, because the error messages are misleading.
## The three failure layers
You will hit these in order; each fix below removes one.
1. **`libgthread-2.0.so.0: cannot open shared object file`**
(shown in the plugin's own error figure) — no FHS environment at
all. PySide6's bundled Qt cannot load glib & friends.
2. **`qt.qpa.plugin: From 6.5.0, xcb-cursor0 or libxcb-cursor0 is
needed…` / `no Qt platform plugin could be initialized`** —
FHS present but incomplete. Beyond the obvious `xcb-util-cursor`,
the bundled Qt needs the whole **xcb-util family** (`icccm`,
`image`, `keysyms`, `render-util`, `util`) and **`libzstd`**
(a hard dependency of `libQt6Core`; note that `pkgs.zstd`'s
default output ships only the CLI — the library lives in
`zstd.out`). Qt prints the xcb-cursor hint for *any* missing
dependency of the platform plugin, so don't trust it literally.
3. **`Could not load the Qt platform plugin "wayland"/"xcb" in ""
even though it was found`** with all libraries present — the
desktop session (KDE Plasma does this) exports **`QT_PLUGIN_PATH`**
pointing at the system's Qt plugin directories. That variable takes
precedence over the wheel's bundled plugins, so the venv's PySide6
(its own Qt, e.g. 6.11.1) tries to load platform plugins built
against the system Qt (e.g. 6.11.0) — a private-ABI mismatch that
fails exactly like a missing library. The fix is to **unset
`QT_PLUGIN_PATH`** for KiCad and everything it spawns. KiCad
itself is wxWidgets/GTK and does not use the variable.
## Recommended setup: an FHS wrapper
Wrap KiCad in `buildFHSEnv` so a plain `kicad` launch carries
everything. Home-manager example (`home.nix`); for a system-wide
install put the same package in `environment.systemPackages` in
`configuration.nix` instead:
```nix
{ pkgs, ... }:
let
kicad-fhs = pkgs.buildFHSEnv {
name = "kicad";
targetPkgs = p: with p; [
kicad glib fontconfig freetype dbus libGL libxkbcommon
xcb-util-cursor wayland zlib zstd.out
xorg.libX11 xorg.libxcb xorg.libXext xorg.libXrender
xorg.libSM xorg.libICE xorg.libXrandr xorg.libXi
xorg.libXcursor xorg.libXfixes
# xcb-util family needed by PySide6's bundled xcb platform plugin
xorg.xcbutil xorg.xcbutilwm xorg.xcbutilimage
xorg.xcbutilkeysyms xorg.xcbutilrenderutil
];
# Plasma exports QT_PLUGIN_PATH (system Qt plugin dirs); the venv's
# bundled Qt chokes on those mismatched plugins. KiCad is wx/GTK,
# so dropping the variable is safe.
profile = "unset QT_PLUGIN_PATH";
runScript = "kicad";
};
in
{
home.packages = [ kicad-fhs /* replaces bare pkgs.kicad */ ];
# buildFHSEnv ships no .desktop file; restore the menu launcher.
xdg.desktopEntries.kicad = {
name = "KiCad";
exec = "kicad %F";
icon = "kicad";
categories = [ "Development" "Electronics" ];
mimeType = [ "application/x-kicad-project" ];
};
}
```
Rebuild, **fully quit any running KiCad** (a running instance keeps
its old environment), relaunch, click the Ω button. A one-line
`Could not load the Qt platform plugin "wayland"` warning may remain
if the wayland client stack is unhappy — it is harmless; Qt falls
back to xcb (XWayland) and the dialog appears.
## Quick test without a rebuild
`steam-run` provides a broad FHS that is one library short
(`libxcb-cursor`, required by Qt ≥ 6.5) and, on Plasma, still leaks
`QT_PLUGIN_PATH`:
```sh
XCBCUR=$(nix build --no-link --print-out-paths nixpkgs#xcb-util-cursor)
env -u QT_PLUGIN_PATH QT_QPA_PLATFORM=xcb \
LD_LIBRARY_PATH=$XCBCUR/lib steam-run kicad
```
(Fish: `set XCBCUR (nix build --no-link --print-out-paths
nixpkgs#xcb-util-cursor)`, then the same `env …` line.)
## Debugging further failures
If a new wheel version needs another library, reproduce the plugin's
Qt startup *outside* KiCad against the wrapper's library set — the
venv survives at
`~/.cache/kicad/10.0/python-environments/th.co.b4l.fill-resistance`:
```sh
ROOTFS=$(nix-store -qR "$(readlink -f "$(command -v kicad)")" \
| grep fhsenv-rootfs | head -1)
env -i HOME=$HOME DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY \
LD_LIBRARY_PATH=$ROOTFS/usr/lib64 QT_DEBUG_PLUGINS=1 \
~/.cache/kicad/10.0/python-environments/th.co.b4l.fill-resistance/bin/python3 \
-c 'from PySide6.QtWidgets import QApplication; \
print(QApplication([]).platformName())'
```
A missing library shows up as a plain `ImportError: libfoo.so.N:
cannot open shared object file` — map the soname to its nixpkgs
attribute (`nix-locate libfoo.so.N`, from `nix-index`) and add it to
`targetPkgs`. If instead every library loads and only the platform
plugin fails, compare the environment of the *running* KiCad
(`tr '\0' '\n' < /proc/$(pgrep -x kicad)/environ`) for Qt variables
leaking in from the session — `QT_PLUGIN_PATH` above was found
exactly this way.
+17 -2
View File
@@ -39,9 +39,24 @@ def _fail(message: str, outdir) -> None:
def main() -> None: def main() -> None:
outdir = None outdir = None
try: 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 "
f"(buildFHSEnv wrapper, or steam-run for a quick "
f"test) - see docs/NIXOS.md in the plugin repo."
)
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)
+12 -2
View File
@@ -24,10 +24,20 @@ 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 QtWidgets, not just the package or QtCore:
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"), and on a
partially provisioned system QtCore's deps (glib, icu) can be
present while QtWidgets/QtGui still miss libGL/libEGL. matplotlib's
qt backend imports QtCore, QtGui and QtWidgets, so probe the widest
one - 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 + ".QtWidgets")
return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg" return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg"
except Exception: except Exception:
continue continue
+2 -2
View File
@@ -68,7 +68,7 @@ def write_summary(outdir: Path, problem: Problem, stack: RasterStack,
f"resistivity: {problem.rho_ohm_m:.3e} ohm*m", f"resistivity: {problem.rho_ohm_m:.3e} ohm*m",
f"via plating: {problem.plating_nm / 1000:.0f} um", f"via plating: {problem.plating_nm / 1000:.0f} um",
"", "",
(f"frequency: " ("frequency: "
+ (f"{result.freq_hz:g} Hz (skin depth {result.skin_depth_um:.0f} um)" + (f"{result.freq_hz:g} Hz (skin depth {result.skin_depth_um:.0f} um)"
if result.freq_hz > 0 else "DC")), if result.freq_hz > 0 else "DC")),
f"RESISTANCE: {result.R_ohm * 1000:.6g} mOhm" f"RESISTANCE: {result.R_ohm * 1000:.6g} mOhm"
@@ -127,7 +127,7 @@ def write_summary(outdir: Path, problem: Problem, stack: RasterStack,
f"contact model: {result.contact_model}" f"contact model: {result.contact_model}"
+ (" (uniform orthogonal injection; R is the upper contact bound)" + (" (uniform orthogonal injection; R is the upper contact bound)"
if result.contact_model == "uniform" else " (ideal bonded lug)"), if result.contact_model == "uniform" else " (ideal bonded lug)"),
f"terminals:", "terminals:",
f" V+ ({len(problem.electrodes1)} injection area(s)):", f" V+ ({len(problem.electrodes1)} injection area(s)):",
*(f" {_electrode_line(e)}" for e in problem.electrodes1), *(f" {_electrode_line(e)}" for e in problem.electrodes1),
f" V- ({len(problem.electrodes2)} injection area(s)):", f" V- ({len(problem.electrodes2)} injection area(s)):",
+27
View File
@@ -20,6 +20,33 @@ 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_qt_gui_stack(monkeypatch):
# NixOS: `import PySide6` succeeds (a pure-Python __init__) while
# the native .so's cannot load the FHS system libraries pip wheels
# expect; on a partially provisioned system even QtCore loads
# (glib, icu present) while QtWidgets/QtGui still miss libGL. The
# probe must import QtWidgets and fall through - promising QtAgg
# kills even the error figure at switch_backend time, and the
# failure report with it. The mock mirrors that faithfully (bare
# package and QtCore succeed, GUI modules fail) so a probe reverted
# to `__import__(qt)` or `.QtCore` would wrongly return QtAgg here.
import builtins
import types
real_import = builtins.__import__
def broken_qt(name, *args, **kwargs):
root, _, sub = name.partition(".")
if root in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
if sub in ("", "QtCore"):
return types.ModuleType(name)
raise ImportError("libGL.so.1: 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"
+25
View File
@@ -0,0 +1,25 @@
# CI-only: the FHS environment from docs/NIXOS.md minus KiCad itself.
# The suite runs against pip wheels exactly as the plugin's venv does
# inside the wrapped KiCad: PySide6 dlopens these libraries at FHS
# paths. Keep this library list in sync with the buildFHSEnv recipe in
# docs/NIXOS.md — CI failing here means the documented recipe broke.
{ pkgs ? import <nixpkgs> { } }:
pkgs.buildFHSEnv {
name = "fill-resistance-ci";
targetPkgs = p: with p; [
bashInteractive curl cacert
glib fontconfig freetype dbus libGL libxkbcommon
xcb-util-cursor wayland zlib zstd.out
xorg.libX11 xorg.libxcb xorg.libXext xorg.libXrender
xorg.libSM xorg.libICE xorg.libXrandr xorg.libXi
xorg.libXcursor xorg.libXfixes
# xcb-util family needed by PySide6's bundled xcb platform plugin
xorg.xcbutil xorg.xcbutilwm xorg.xcbutilimage
xorg.xcbutilkeysyms xorg.xcbutilrenderutil
];
# See docs/NIXOS.md failure layer 3: the desktop's QT_PLUGIN_PATH
# must not leak into the wheel's bundled Qt.
profile = "unset QT_PLUGIN_PATH";
runScript = "bash";
}