Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cc48993af | |||
| 72bb46e9b5 | |||
| f68c14b01c | |||
| dfba5561bb | |||
| de7a4f8641 | |||
| a4945f419c | |||
| 64676624e6 | |||
| 7604e18587 |
@@ -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
|
||||||
|
'
|
||||||
@@ -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
|
||||||
|
|
||||||
|
[](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 1–4 are the same everywhere;
|
compiler needed. Steps 1–4 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
@@ -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.
|
||||||
@@ -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 "
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)):",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user